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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Avijit Sarkar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,124 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# how am i prompting? 🤔
|
|
4
|
+
|
|
5
|
+
**Spotify Wrapped for your AI coding prompts.**
|
|
6
|
+
Find out how well you *really* talk to Claude Code, Codex, Cursor & friends — and get coached to do it better.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npx howamiprompting
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
100% local · no sign-up · free & open source · uses the AI you already have
|
|
13
|
+
|
|
14
|
+
<img src="https://raw.githubusercontent.com/theavijitsarkar/howamiprompting/main/docs/wrapped-persona.png" width="49%" alt="Your prompting personality"> <img src="https://raw.githubusercontent.com/theavijitsarkar/howamiprompting/main/docs/wrapped-score.png" width="49%" alt="Your prompting score">
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## What it does
|
|
21
|
+
|
|
22
|
+
One command scans every AI coding chat on your machine, keeps **only what you typed**, and opens a beautiful local report:
|
|
23
|
+
|
|
24
|
+
- 🎬 **Your Wrapped** — prompts written, your AI crew, prime-time hour, catchphrases, rage moments, and your **prompting personality** (The Architect? The Yes-Machine? The Paste-and-Pray?)
|
|
25
|
+
- 📐 **A score across 6 skills** — Clarity · Context · Specificity · Structure · Efficiency · English
|
|
26
|
+
- ✨ **Makeovers of your real prompts** — your worst prompts, rewritten the way a senior engineer would write them
|
|
27
|
+
- ✍️ **Honest English feedback** — your actual grammar mistakes, corrected, with an estimated CEFR level (judged on your prose, never your code)
|
|
28
|
+
- 📌 **Stop repeating yourself** — finds instructions you type in every session ("use pnpm", "don't add comments") and turns them into a ready-to-paste `CLAUDE.md` / `AGENTS.md` / `.cursor/rules` / `GEMINI.md` / Copilot file
|
|
29
|
+
- 🗓️ **A personal prompt template + 7-day plan**
|
|
30
|
+
- 🎮 **Practice mode** — four timed mini-games generated from *your* weak spots (no paste, no spell-check)
|
|
31
|
+
- 📈 **Progress over time** — run it again next month and see your score move
|
|
32
|
+
- 🚀 **Share card** — a PNG with your score and persona (no prompts in it). Optional public leaderboard.
|
|
33
|
+
|
|
34
|
+
<img src="https://raw.githubusercontent.com/theavijitsarkar/howamiprompting/main/docs/report.png" alt="The report">
|
|
35
|
+
|
|
36
|
+
<img src="https://raw.githubusercontent.com/theavijitsarkar/howamiprompting/main/docs/makeovers.png" alt="Prompt makeovers">
|
|
37
|
+
|
|
38
|
+
## Supported tools
|
|
39
|
+
|
|
40
|
+
| Tool | Where we look |
|
|
41
|
+
|---|---|
|
|
42
|
+
| **Claude Code** | `~/.claude/projects/**.jsonl` |
|
|
43
|
+
| **OpenAI Codex CLI** | `~/.codex/sessions/**.jsonl` |
|
|
44
|
+
| **Gemini CLI** | `~/.gemini/tmp/*/logs.json`, `chats/` |
|
|
45
|
+
| **Cursor** (Chat, Composer, Agent) | Cursor's `state.vscdb` (read from a temp copy) |
|
|
46
|
+
| **VS Code Copilot Chat** | `workspaceStorage/*/chatSessions` |
|
|
47
|
+
| **Windsurf · Antigravity · Trae · Kiro · VSCodium** | same VS Code–family locations |
|
|
48
|
+
| **Cline · Roo Code · Kilo Code** | extension task histories |
|
|
49
|
+
| **Continue** | `~/.continue/sessions` |
|
|
50
|
+
| **OpenCode** | `~/.local/share/opencode/storage` |
|
|
51
|
+
| **Aider** | `.aider.chat.history.md` in your projects |
|
|
52
|
+
| **SpecStory** | `.specstory/history/*.md` |
|
|
53
|
+
| **Anything else** | `npx howamiprompting --include export.md` (Markdown, JSON, JSONL, text) |
|
|
54
|
+
|
|
55
|
+
Missing your tool? [Open an issue](https://github.com/theavijitsarkar/howamiprompting/issues) — or add a 30-line collector in [`cli/src/collect/`](https://github.com/theavijitsarkar/howamiprompting/tree/main/cli/src/collect) and send a PR.
|
|
56
|
+
|
|
57
|
+
## How the analysis works
|
|
58
|
+
|
|
59
|
+
1. **Collect** — read-only scan of the locations above. Assistant replies, tool output, sub-agent traffic and system messages are dropped; pasted code and logs are replaced with markers like `[pasted log: 40 lines]`.
|
|
60
|
+
2. **Redact** — API keys, tokens, JWTs, private keys, passwords and DB URLs are stripped before anything else happens.
|
|
61
|
+
3. **Measure locally** — dozens of stats (length, timing, acknowledgements, frustration, context-giving, repeated instructions, English mechanics) computed on your machine. This alone gives you the Wrapped story and a heuristic score.
|
|
62
|
+
4. **Coach with your own AI** — a stratified sample (~180 prompts) goes to the agent **you already use**, in batches, then a synthesis pass writes the report. We auto-detect, in order:
|
|
63
|
+
`claude` (Claude Code) → `codex` → `gemini` → `ANTHROPIC_API_KEY` → `OPENROUTER_API_KEY` → `OPENAI_API_KEY` → **Ollama** (fully offline)
|
|
64
|
+
|
|
65
|
+
You'll be asked before anything is sent. Use `--no-ai` for a stats-only report where nothing leaves your machine at all.
|
|
66
|
+
|
|
67
|
+
## Privacy
|
|
68
|
+
|
|
69
|
+
- No account, no telemetry, no analytics in the CLI or the report.
|
|
70
|
+
- Your prompts are **never** sent to us. With an agent, the sample goes through *your own* Claude Code / Codex / Gemini / API key / local Ollama.
|
|
71
|
+
- The report runs on `127.0.0.1` with a random per-run token.
|
|
72
|
+
- History lives in `~/.howamiprompting/` — delete it any time.
|
|
73
|
+
- The leaderboard is opt-in and only receives a nickname, scores, persona, tool names and prompt count.
|
|
74
|
+
|
|
75
|
+
## Options
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
npx howamiprompting [options]
|
|
79
|
+
|
|
80
|
+
--agent <id> claude | codex | gemini | anthropic | openrouter | openai | ollama
|
|
81
|
+
--model <name> model override for that agent (e.g. haiku, sonnet)
|
|
82
|
+
--no-ai local stats only — nothing leaves your machine
|
|
83
|
+
--sample <n> prompts sent for deep analysis (default 180)
|
|
84
|
+
--since <days> only look at the last N days
|
|
85
|
+
--include <file> also import a chat export (repeatable)
|
|
86
|
+
--roots <dir> extra folder to search for Aider / SpecStory histories
|
|
87
|
+
--last re-open your last report without rescanning
|
|
88
|
+
--json <file> also write the report as JSON
|
|
89
|
+
--no-open don't open the browser
|
|
90
|
+
-y, --yes no questions
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Requires Node.js 18.17+. Cursor history needs Node 22.5+ (built-in SQLite) **or** the `sqlite3` command (pre-installed on macOS).
|
|
94
|
+
|
|
95
|
+
## The six skills
|
|
96
|
+
|
|
97
|
+
| Skill | What great looks like |
|
|
98
|
+
|---|---|
|
|
99
|
+
| **Clarity** | One unambiguous goal. No "fix it", no "make it better". |
|
|
100
|
+
| **Context** | Current vs expected behaviour, relevant files, the *why*, errors with one line of your theory. |
|
|
101
|
+
| **Specificity** | Constraints, scope ("only touch X"), acceptance criteria, examples. |
|
|
102
|
+
| **Structure** | One task at a time, or numbered steps. Key info first. |
|
|
103
|
+
| **Efficiency** | Results without "still not working" loops and five rounds of "no, not like that". |
|
|
104
|
+
| **English** | Clear sentences, correct tense, real punctuation. Small grammar slips become ambiguous instructions. |
|
|
105
|
+
|
|
106
|
+
## Contributing
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
git clone https://github.com/theavijitsarkar/howamiprompting && cd howamiprompting/cli
|
|
110
|
+
node test/make-fixtures.js /tmp/fakehome # fake histories for every tool
|
|
111
|
+
HOME=/tmp/fakehome node bin/howamiprompting.js --no-ai
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Ideas that would be awesome: more collectors, better persona rules, translations of the report, more practice games.
|
|
115
|
+
|
|
116
|
+
## Who made this
|
|
117
|
+
|
|
118
|
+
Hi, I'm **[Avijit Sarkar](https://avijitsarkar.com)** — a founder from Kolkata building SaaS products like [GoDial](https://godial.cc) and [HyperScreenshot](https://hyperscreenshot.com). I talk to Claude Code and Codex all day, and wanted to know if I was any good at it. This is the tool I wished existed.
|
|
119
|
+
|
|
120
|
+
Say hi: [avijitsarkar.com](https://avijitsarkar.com)
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
MIT © [Avijit Sarkar](https://avijitsarkar.com). If it helped you, a ⭐ helps other developers find it.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const [major, minor] = process.versions.node.split('.').map(Number);
|
|
3
|
+
if (major < 18 || (major === 18 && minor < 17)) {
|
|
4
|
+
console.error('howamiprompting needs Node.js 18.17 or newer (you have ' + process.versions.node + ').');
|
|
5
|
+
process.exit(1);
|
|
6
|
+
}
|
|
7
|
+
import('../src/cli.js').then(m => m.main(process.argv.slice(2))).catch(e => {
|
|
8
|
+
console.error('\n Something went wrong:', e && e.message ? e.message : e);
|
|
9
|
+
console.error(' Please report it: https://github.com/theavijitsarkar/howamiprompting/issues\n');
|
|
10
|
+
process.exit(1);
|
|
11
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "howamiprompting",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Spotify Wrapped for your AI coding prompts. Scans your Claude Code, Codex, Cursor, Gemini CLI, Copilot, Cline, Aider\u2026 chats locally, scores how you prompt (and your English), and shows you how to get better.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"prompt",
|
|
7
|
+
"prompting",
|
|
8
|
+
"claude-code",
|
|
9
|
+
"codex",
|
|
10
|
+
"cursor",
|
|
11
|
+
"gemini-cli",
|
|
12
|
+
"copilot",
|
|
13
|
+
"ai",
|
|
14
|
+
"llm",
|
|
15
|
+
"developer-tools",
|
|
16
|
+
"wrapped",
|
|
17
|
+
"cli"
|
|
18
|
+
],
|
|
19
|
+
"homepage": "https://howamiprompting.com",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/theavijitsarkar/howamiprompting.git"
|
|
23
|
+
},
|
|
24
|
+
"bugs": "https://github.com/theavijitsarkar/howamiprompting/issues",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"type": "module",
|
|
27
|
+
"bin": {
|
|
28
|
+
"howamiprompting": "bin/howamiprompting.js"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"bin",
|
|
32
|
+
"src",
|
|
33
|
+
"ui",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18.17"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"start": "node bin/howamiprompting.js",
|
|
42
|
+
"fixtures": "node test/make-fixtures.js /tmp/hap-fakehome"
|
|
43
|
+
},
|
|
44
|
+
"author": {
|
|
45
|
+
"name": "Avijit Sarkar",
|
|
46
|
+
"url": "https://avijitsarkar.com"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/agents.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Use whatever AI the developer already has: Claude Code, Codex, Gemini CLI, an API key, or Ollama.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { which } from './util.js';
|
|
7
|
+
|
|
8
|
+
const CLI_TIMEOUT = 6 * 60 * 1000;
|
|
9
|
+
|
|
10
|
+
function run(cmd, args, input, { cwd, timeout = CLI_TIMEOUT } = {}) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
if (process.platform === 'win32') args = args.map(a => /[\s"]/.test(a) ? `"${a.replace(/"/g, '\\"')}"` : a); // shell:true on Windows
|
|
13
|
+
const p = spawn(cmd, args, { cwd, env: { ...process.env, NO_COLOR: '1', CI: '1' }, stdio: ['pipe', 'pipe', 'pipe'], shell: process.platform === 'win32' });
|
|
14
|
+
let out = '', err = '';
|
|
15
|
+
const t = setTimeout(() => { p.kill('SIGKILL'); reject(new Error(`${cmd} timed out`)); }, timeout);
|
|
16
|
+
p.stdout.on('data', d => out += d);
|
|
17
|
+
p.stderr.on('data', d => err += d);
|
|
18
|
+
p.on('error', e => { clearTimeout(t); reject(e); });
|
|
19
|
+
p.on('close', code => { clearTimeout(t); code === 0 ? resolve(out) : reject(new Error(`${cmd} exited ${code}: ${(err || out).slice(-400)}`)); });
|
|
20
|
+
p.stdin.on('error', () => {}); // agent may exit before reading stdin (EPIPE)
|
|
21
|
+
p.stdin.end(input);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const sandbox = () => fs.mkdtempSync(path.join(os.tmpdir(), 'hap-agent-'));
|
|
26
|
+
|
|
27
|
+
async function postJSON(url, headers, body, timeout = 180000) {
|
|
28
|
+
const ctl = new AbortController();
|
|
29
|
+
const t = setTimeout(() => ctl.abort(), timeout);
|
|
30
|
+
try {
|
|
31
|
+
const r = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json', ...headers }, body: JSON.stringify(body), signal: ctl.signal });
|
|
32
|
+
const j = await r.json().catch(() => ({}));
|
|
33
|
+
if (!r.ok) throw new Error(j.error?.message || j.error || `HTTP ${r.status}`);
|
|
34
|
+
return j;
|
|
35
|
+
} finally { clearTimeout(t); }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const AGENTS = {
|
|
39
|
+
claude: {
|
|
40
|
+
label: 'Claude Code', kind: 'cli', privacy: 'Anthropic (via your Claude Code login)',
|
|
41
|
+
detect: () => which('claude'),
|
|
42
|
+
async ask(prompt, o = {}) {
|
|
43
|
+
const dir = sandbox();
|
|
44
|
+
try {
|
|
45
|
+
const args = ['-p', '--output-format', 'json'];
|
|
46
|
+
if (o.model) args.push('--model', o.model);
|
|
47
|
+
const raw = await run('claude', args, prompt, { cwd: dir });
|
|
48
|
+
try { const j = JSON.parse(raw); if (j.is_error) throw new Error(j.result || 'Claude Code error'); return j.result ?? raw; } catch (e) { if (/Claude Code error/.test(e.message)) throw e; return raw; }
|
|
49
|
+
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
codex: {
|
|
53
|
+
label: 'Codex CLI', kind: 'cli', privacy: 'OpenAI (via your Codex login)',
|
|
54
|
+
detect: () => which('codex'),
|
|
55
|
+
async ask(prompt, o = {}) {
|
|
56
|
+
const dir = sandbox();
|
|
57
|
+
const outFile = path.join(dir, 'last.txt');
|
|
58
|
+
try {
|
|
59
|
+
const args = ['exec', '--skip-git-repo-check', '-s', 'read-only', '--color', 'never', '--output-last-message', outFile];
|
|
60
|
+
if (o.model) args.push('-m', o.model);
|
|
61
|
+
args.push('-');
|
|
62
|
+
const raw = await run('codex', args, prompt, { cwd: dir });
|
|
63
|
+
return fs.existsSync(outFile) ? fs.readFileSync(outFile, 'utf8') : raw;
|
|
64
|
+
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
gemini: {
|
|
68
|
+
label: 'Gemini CLI', kind: 'cli', privacy: 'Google (via your Gemini CLI login)',
|
|
69
|
+
detect: () => which('gemini'),
|
|
70
|
+
async ask(prompt, o = {}) {
|
|
71
|
+
const dir = sandbox();
|
|
72
|
+
try {
|
|
73
|
+
const args = ['-p', 'Follow the instructions in the input above. Reply with the JSON only.'];
|
|
74
|
+
if (o.model) args.push('-m', o.model);
|
|
75
|
+
return await run('gemini', args, prompt, { cwd: dir });
|
|
76
|
+
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
anthropic: {
|
|
80
|
+
label: 'Anthropic API', kind: 'api', privacy: 'Anthropic (your ANTHROPIC_API_KEY)',
|
|
81
|
+
detect: () => process.env.ANTHROPIC_API_KEY ? 'ANTHROPIC_API_KEY' : null,
|
|
82
|
+
async ask(prompt, o = {}) {
|
|
83
|
+
const j = await postJSON('https://api.anthropic.com/v1/messages', { 'x-api-key': process.env.ANTHROPIC_API_KEY, 'anthropic-version': '2023-06-01' },
|
|
84
|
+
{ model: o.model || 'claude-haiku-4-5', max_tokens: 8000, messages: [{ role: 'user', content: prompt }] });
|
|
85
|
+
return (j.content || []).map(c => c.text || '').join('');
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
openai: {
|
|
89
|
+
label: 'OpenAI API', kind: 'api', privacy: 'OpenAI (your OPENAI_API_KEY)',
|
|
90
|
+
detect: () => process.env.OPENAI_API_KEY ? 'OPENAI_API_KEY' : null,
|
|
91
|
+
async ask(prompt, o = {}) {
|
|
92
|
+
const j = await postJSON('https://api.openai.com/v1/chat/completions', { authorization: 'Bearer ' + process.env.OPENAI_API_KEY },
|
|
93
|
+
{ model: o.model || 'gpt-4.1-mini', response_format: { type: 'json_object' }, messages: [{ role: 'user', content: prompt }] });
|
|
94
|
+
return j.choices?.[0]?.message?.content || '';
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
openrouter: {
|
|
98
|
+
label: 'OpenRouter API', kind: 'api', privacy: 'OpenRouter (your OPENROUTER_API_KEY)',
|
|
99
|
+
detect: () => process.env.OPENROUTER_API_KEY ? 'OPENROUTER_API_KEY' : null,
|
|
100
|
+
async ask(prompt, o = {}) {
|
|
101
|
+
const j = await postJSON('https://openrouter.ai/api/v1/chat/completions', { authorization: 'Bearer ' + process.env.OPENROUTER_API_KEY, 'X-Title': 'howamiprompting' },
|
|
102
|
+
{ model: o.model || 'anthropic/claude-haiku-4.5', messages: [{ role: 'user', content: prompt }] });
|
|
103
|
+
return j.choices?.[0]?.message?.content || '';
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
ollama: {
|
|
107
|
+
label: 'Ollama (local)', kind: 'local', privacy: 'nobody — runs on this machine',
|
|
108
|
+
_model: null,
|
|
109
|
+
async detectAsync() {
|
|
110
|
+
try {
|
|
111
|
+
const r = await fetch((process.env.OLLAMA_HOST || 'http://127.0.0.1:11434') + '/api/tags', { signal: AbortSignal.timeout(1500) });
|
|
112
|
+
const j = await r.json();
|
|
113
|
+
const names = (j.models || []).map(m => m.name);
|
|
114
|
+
const pref = ['qwen3', 'qwen2.5', 'llama3.3', 'llama3.1', 'gemma3', 'mistral-small', 'mistral', 'phi4'];
|
|
115
|
+
this._model = names.find(n => pref.some(p => n.startsWith(p))) || names[0] || null;
|
|
116
|
+
return this._model ? 'model ' + this._model : null;
|
|
117
|
+
} catch { return null; }
|
|
118
|
+
},
|
|
119
|
+
async ask(prompt, o = {}) {
|
|
120
|
+
const j = await postJSON((process.env.OLLAMA_HOST || 'http://127.0.0.1:11434') + '/api/generate', {},
|
|
121
|
+
{ model: o.model || this._model, prompt, format: 'json', stream: false, options: { num_ctx: 32768, temperature: 0.3 } }, 15 * 60 * 1000);
|
|
122
|
+
return j.response || '';
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export const ORDER = ['claude', 'codex', 'gemini', 'anthropic', 'openrouter', 'openai', 'ollama'];
|
|
128
|
+
|
|
129
|
+
export async function detectAgents() {
|
|
130
|
+
const found = [];
|
|
131
|
+
for (const id of ORDER) {
|
|
132
|
+
const a = AGENTS[id];
|
|
133
|
+
const d = a.detectAsync ? await a.detectAsync() : a.detect();
|
|
134
|
+
if (d) found.push({ id, label: a.label, detail: d, privacy: a.privacy, kind: a.kind });
|
|
135
|
+
}
|
|
136
|
+
return found;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function extractJSON(text) {
|
|
140
|
+
let t = String(text || '').trim().replace(/^```(?:json)?\s*/i, '').replace(/\s*```\s*$/, '');
|
|
141
|
+
try { return JSON.parse(t); } catch {}
|
|
142
|
+
const s = t.indexOf('{'), e = t.lastIndexOf('}');
|
|
143
|
+
if (s >= 0 && e > s) { try { return JSON.parse(t.slice(s, e + 1)); } catch {} }
|
|
144
|
+
throw new Error('The agent did not return valid JSON');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Ask the agent and parse JSON, retrying once. */
|
|
148
|
+
export async function askJSON(agentId, prompt, { model, validate = () => {} } = {}) {
|
|
149
|
+
const a = AGENTS[agentId];
|
|
150
|
+
let last;
|
|
151
|
+
for (let i = 0; i < 2; i++) {
|
|
152
|
+
try {
|
|
153
|
+
const txt = await a.ask(prompt + (i ? '\n\nIMPORTANT: your previous reply was not valid JSON. Reply with ONE JSON object and nothing else.' : ''), { model });
|
|
154
|
+
const j = extractJSON(txt);
|
|
155
|
+
validate(j);
|
|
156
|
+
return j;
|
|
157
|
+
} catch (e) { last = e; }
|
|
158
|
+
}
|
|
159
|
+
throw last;
|
|
160
|
+
}
|
package/src/analyze.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Deep analysis with the developer's own AI agent. Map (chunks) → reduce (synthesis).
|
|
2
|
+
import { askJSON } from './agents.js';
|
|
3
|
+
|
|
4
|
+
export const DIMENSIONS = {
|
|
5
|
+
clarity: 'Clarity — is the ask unambiguous? One clear goal, plain words, no guessing needed.',
|
|
6
|
+
context: 'Context — does the AI get what it needs: relevant files, current vs expected behaviour, why, error output with explanation.',
|
|
7
|
+
specificity: 'Specificity — constraints, acceptance criteria, examples, scope limits ("only touch X", "keep the API the same").',
|
|
8
|
+
structure: 'Structure — one task at a time or clearly numbered steps; important info first; not a wall of text.',
|
|
9
|
+
efficiency: 'Efficiency — gets results without long correction loops, repeated asks, frustration or "still not working" spirals.',
|
|
10
|
+
english: 'English — grammar, tense, sentence boundaries, punctuation, spelling. Judge the prose only, never code.',
|
|
11
|
+
};
|
|
12
|
+
const WEIGHTS = { clarity: 0.2, context: 0.22, specificity: 0.18, structure: 0.12, efficiency: 0.13, english: 0.15 };
|
|
13
|
+
export const overallFrom = s => Math.round(Object.entries(WEIGHTS).reduce((a, [k, w]) => a + w * (Number(s[k]) || 0), 0));
|
|
14
|
+
|
|
15
|
+
const COACH = `You are the world's best coach for developers who prompt AI coding agents (Claude Code, Codex, Cursor, Copilot, Gemini CLI). You are also an expert English writing coach for non-native speakers. You are specific, honest, funny when it fits, never generic. Every claim must point at the developer's real prompts. You only judge HOW they prompt and their written English — never their code.`;
|
|
16
|
+
|
|
17
|
+
/** Pick a representative sample: spread across tools and time, skip trivial one-word acks (we already count those). */
|
|
18
|
+
export function sample(prompts, max = 180) {
|
|
19
|
+
const useful = prompts.filter(p => p.words >= 3 || p.logLines || p.codeLines);
|
|
20
|
+
if (useful.length <= max) return useful;
|
|
21
|
+
const byTool = new Map();
|
|
22
|
+
for (const p of useful) { if (!byTool.has(p.tool)) byTool.set(p.tool, []); byTool.get(p.tool).push(p); }
|
|
23
|
+
const out = [];
|
|
24
|
+
const tools = [...byTool.values()];
|
|
25
|
+
const base = Math.floor(max / tools.length);
|
|
26
|
+
let spare = max - base * tools.length;
|
|
27
|
+
// proportional with a floor so small tools are represented
|
|
28
|
+
const total = useful.length;
|
|
29
|
+
const quota = tools.map(arr => Math.max(Math.min(arr.length, 8), Math.round(max * arr.length / total)));
|
|
30
|
+
let sum = quota.reduce((a, b) => a + b, 0);
|
|
31
|
+
while (sum > max) { const i = quota.indexOf(Math.max(...quota)); quota[i]--; sum--; }
|
|
32
|
+
tools.forEach((arr, ti) => {
|
|
33
|
+
const k = Math.min(arr.length, quota[ti] + (spare-- > 0 ? 0 : 0));
|
|
34
|
+
// 60% recent half, 40% older half, evenly spaced within each
|
|
35
|
+
const recent = arr.slice(Math.floor(arr.length / 2)), older = arr.slice(0, Math.floor(arr.length / 2));
|
|
36
|
+
const kr = Math.min(recent.length, Math.ceil(k * 0.6)), ko = Math.min(older.length, k - kr);
|
|
37
|
+
for (const [src, kk] of [[older, ko], [recent, kr]]) for (let i = 0; i < kk; i++) out.push(src[Math.floor(i * src.length / kk)]);
|
|
38
|
+
});
|
|
39
|
+
return out.sort((a, b) => a.id - b.id);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function fmt(p) {
|
|
43
|
+
const ctx = p.context ? `\n ↳ (AI had just said: "${p.context.slice(-160).replace(/\s+/g, ' ')}")` : '';
|
|
44
|
+
return `[${p.id}] (${p.tool}${p.project ? ', ' + p.project : ''}) ${p.text.slice(0, 700).replace(/\n{2,}/g, '\n')}${ctx}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function chunkPrompt(chunk) {
|
|
48
|
+
return `${COACH}
|
|
49
|
+
|
|
50
|
+
TASK: analyze_prompt_batch
|
|
51
|
+
Below are ${chunk.length} prompts one developer typed to AI coding assistants. Each starts with [id] and the tool. Pasted code/logs were replaced by markers like [pasted log: 40 lines]. Secrets were redacted.
|
|
52
|
+
|
|
53
|
+
Score each dimension 0-100 for THIS batch (60 = average developer, 80 = strong, 90+ = exceptional):
|
|
54
|
+
${Object.entries(DIMENSIONS).map(([k, v]) => `- ${k}: ${v}`).join('\n')}
|
|
55
|
+
|
|
56
|
+
PROMPTS:
|
|
57
|
+
${chunk.map(fmt).join('\n')}
|
|
58
|
+
|
|
59
|
+
Return ONE JSON object, no prose:
|
|
60
|
+
{
|
|
61
|
+
"scores": {"clarity":0,"context":0,"specificity":0,"structure":0,"efficiency":0,"english":0},
|
|
62
|
+
"strengths": [{"point":"what they do well","id":123,"quote":"exact short quote"}],
|
|
63
|
+
"issues": [{"issue":"short label","category":"clarity|context|specificity|structure|efficiency|english","severity":"high|medium|low","ids":[1,2],"quote":"exact short quote (max 25 words)","why":"why this costs them","rewrite":"how that prompt should have been written"}],
|
|
64
|
+
"english_errors": [{"quote":"exact words","fix":"corrected","rule":"short rule"}],
|
|
65
|
+
"worst_ids": [ids of the 3 most improvable MEANINGFUL prompts — not one-word acks],
|
|
66
|
+
"best_ids": [ids of the 2 best prompts],
|
|
67
|
+
"notes": "2 sentences on this developer's style"
|
|
68
|
+
}
|
|
69
|
+
Limits: strengths 2-4, issues 3-6, english_errors up to 8 (empty if their English is clean).`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function synthPrompt(stats, batches, sampleById, heuristic) {
|
|
73
|
+
const avg = {};
|
|
74
|
+
for (const k of Object.keys(DIMENSIONS)) {
|
|
75
|
+
const v = batches.map(b => Number(b.scores?.[k])).filter(n => !isNaN(n));
|
|
76
|
+
avg[k] = v.length ? Math.round(v.reduce((a, b) => a + b, 0) / v.length) : heuristic[k] ?? 60;
|
|
77
|
+
}
|
|
78
|
+
const worst = [...new Set(batches.flatMap(b => b.worst_ids || []))].map(id => sampleById.get(Number(id))).filter(Boolean).slice(0, 6);
|
|
79
|
+
const best = [...new Set(batches.flatMap(b => b.best_ids || []))].map(id => sampleById.get(Number(id))).filter(Boolean).slice(0, 3);
|
|
80
|
+
const s = stats;
|
|
81
|
+
const facts = {
|
|
82
|
+
prompts: s.totals.prompts, sessions: s.totals.sessions, tools: s.tools.map(t => `${t.name} (${t.count})`).join(', '),
|
|
83
|
+
median_words: s.length.median, avg_words: s.length.avg,
|
|
84
|
+
one_word_acks_pct: s.habits.confirmPct, vague_pct: s.habits.vaguePct, frustration_pct: s.habits.frustrationPct,
|
|
85
|
+
pushback_pct: s.habits.pushbackPct, gives_context_pct: s.habits.contextPct, paste_error_without_explanation_pct: s.habits.pastePrayPct,
|
|
86
|
+
multi_ask_pct: s.habits.multiAskPct, please_pct: s.habits.pleasePct, swears: s.habits.swears, interrupts: s.totals.interrupts,
|
|
87
|
+
lowercase_start_pct: s.english.lowerStartPct, no_final_punctuation_pct: s.english.noEndPunctPct, run_on_pct: s.english.runOnPct,
|
|
88
|
+
night_owl_pct: s.time.nightPct, persona: s.persona.name,
|
|
89
|
+
detected_english_patterns: s.english.patterns.slice(0, 6).map(p => `${p.label} ×${p.count}`).join(', '),
|
|
90
|
+
};
|
|
91
|
+
return `${COACH}
|
|
92
|
+
|
|
93
|
+
TASK: synthesize_report
|
|
94
|
+
You analysed a developer's prompts in ${batches.length} batches. Merge everything into ONE final report. Be concrete, quote their real words, keep it encouraging but honest.
|
|
95
|
+
|
|
96
|
+
BATCH SCORES (average): ${JSON.stringify(avg)}
|
|
97
|
+
BATCH FINDINGS:
|
|
98
|
+
${batches.map((b, i) => `--- batch ${i + 1}\nnotes: ${b.notes || ''}\nstrengths: ${JSON.stringify(b.strengths || [])}\nissues: ${JSON.stringify(b.issues || [])}\nenglish_errors: ${JSON.stringify(b.english_errors || [])}`).join('\n')}
|
|
99
|
+
|
|
100
|
+
MEASURED FACTS FROM ALL ${s.totals.prompts} PROMPTS: ${JSON.stringify(facts)}
|
|
101
|
+
|
|
102
|
+
INSTRUCTIONS THEY REPEAT ACROSS SESSIONS (candidates for a rules file): ${JSON.stringify(s.repeated.map(r => r.text + ` (${r.sessions} sessions)`))}
|
|
103
|
+
|
|
104
|
+
THEIR MOST IMPROVABLE PROMPTS:
|
|
105
|
+
${worst.map(fmt).join('\n')}
|
|
106
|
+
|
|
107
|
+
THEIR BEST PROMPTS:
|
|
108
|
+
${best.map(fmt).join('\n')}
|
|
109
|
+
|
|
110
|
+
Return ONE JSON object:
|
|
111
|
+
{
|
|
112
|
+
"headline": "a punchy 5-10 word verdict, second person",
|
|
113
|
+
"summary": "3-4 sentences, second person, specific",
|
|
114
|
+
"scores": {"clarity":0,"context":0,"specificity":0,"structure":0,"efficiency":0,"english":0},
|
|
115
|
+
"level": "Beginner|Developing|Proficient|Advanced|Expert",
|
|
116
|
+
"strengths": [{"title":"...","detail":"1-2 sentences","example":"exact quote"}],
|
|
117
|
+
"weaknesses": [{"title":"...","category":"clarity|context|specificity|structure|efficiency|english","detail":"why it hurts their results","example":"exact quote","better":"the same prompt rewritten well","impact":"high|medium|low"}],
|
|
118
|
+
"makeovers": [{"id":123,"original":"their prompt verbatim (may shorten)","rewritten":"an excellent version of the same request","why":["3 short reasons"]}],
|
|
119
|
+
"english": {"score":0,"cefr":"A2|B1|B2|C1|C2","summary":"2 sentences","errors":[{"quote":"exact words","fix":"corrected","rule":"short rule","frequency":"often|sometimes|once"}],"tips":["3 specific tips"]},
|
|
120
|
+
"habits": [{"start":"a habit to start","stop":"the habit it replaces"}],
|
|
121
|
+
"rules_file": ["5-10 one-line rules they keep repeating or should set once in CLAUDE.md / AGENTS.md / .cursorrules"],
|
|
122
|
+
"template": "a personal fill-in-the-blanks prompt template tailored to their weaknesses (use \\n for new lines)",
|
|
123
|
+
"plan": ["7 short daily drills, one per day"],
|
|
124
|
+
"fun_facts": ["3 witty, true observations from the facts"],
|
|
125
|
+
"persona_note": "one witty line about them as '${s.persona.name}'"
|
|
126
|
+
}
|
|
127
|
+
Counts: strengths 3, weaknesses 4-5, makeovers 3 (only from the 'most improvable' list), english.errors up to 8, habits 4-5.`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Run the full analysis. onStep(label, i, total) for progress.
|
|
132
|
+
*/
|
|
133
|
+
export async function analyze(prompts, stats, { agent, model, sampleSize = 180, batchSize = 60, concurrency = 2, onStep = () => {} } = {}) {
|
|
134
|
+
const smp = sample(prompts, sampleSize);
|
|
135
|
+
const byId = new Map(smp.map(p => [p.id, p]));
|
|
136
|
+
const chunks = [];
|
|
137
|
+
for (let i = 0; i < smp.length; i += batchSize) chunks.push(smp.slice(i, i + batchSize));
|
|
138
|
+
if (!chunks.length) throw new Error('No prompts to analyse');
|
|
139
|
+
const total = chunks.length + 1;
|
|
140
|
+
let done = 0;
|
|
141
|
+
const batches = new Array(chunks.length);
|
|
142
|
+
const errors = [];
|
|
143
|
+
let next = 0;
|
|
144
|
+
async function worker() {
|
|
145
|
+
while (next < chunks.length) {
|
|
146
|
+
const i = next++;
|
|
147
|
+
try {
|
|
148
|
+
batches[i] = await askJSON(agent, chunkPrompt(chunks[i]), { model, validate: j => { if (!j.scores) throw new Error('missing scores'); } });
|
|
149
|
+
} catch (e) { errors.push(e.message); batches[i] = null; }
|
|
150
|
+
onStep(`Read batch ${++done} of ${chunks.length}`, done, total);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, worker));
|
|
154
|
+
const ok = batches.filter(Boolean);
|
|
155
|
+
if (!ok.length) throw new Error('Every batch failed: ' + errors[0]);
|
|
156
|
+
|
|
157
|
+
onStep('Writing your report', done, total);
|
|
158
|
+
const report = await askJSON(agent, synthPrompt(stats, ok, byId, stats.heuristic), {
|
|
159
|
+
model, validate: j => { if (!j.scores || !j.headline) throw new Error('incomplete report'); },
|
|
160
|
+
});
|
|
161
|
+
for (const k of Object.keys(DIMENSIONS)) report.scores[k] = Math.max(0, Math.min(100, Math.round(Number(report.scores[k]) || stats.heuristic[k] || 60)));
|
|
162
|
+
report.scores.overall = overallFrom(report.scores);
|
|
163
|
+
report.sampled = smp.length;
|
|
164
|
+
report.batchErrors = errors.length;
|
|
165
|
+
onStep('Done', total, total);
|
|
166
|
+
return report;
|
|
167
|
+
}
|