@eeshans/howiprompt 2.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 +148 -0
- package/bin/bootstrap-db.mjs +166 -0
- package/bin/cli-helpers.mjs +86 -0
- package/bin/cli.mjs +205 -0
- package/config/ml.json +47 -0
- package/data/claude_code/.gitkeep +3 -0
- package/data/codex/.gitkeep +0 -0
- package/data/reference_clusters.json +314 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +194 -0
- package/dist/index.js.map +1 -0
- package/dist/pipeline/backends.d.ts +39 -0
- package/dist/pipeline/backends.js +411 -0
- package/dist/pipeline/backends.js.map +1 -0
- package/dist/pipeline/classifiers.d.ts +17 -0
- package/dist/pipeline/classifiers.js +181 -0
- package/dist/pipeline/classifiers.js.map +1 -0
- package/dist/pipeline/config.d.ts +21 -0
- package/dist/pipeline/config.js +79 -0
- package/dist/pipeline/config.js.map +1 -0
- package/dist/pipeline/db.d.ts +41 -0
- package/dist/pipeline/db.js +130 -0
- package/dist/pipeline/db.js.map +1 -0
- package/dist/pipeline/embeddings.d.ts +15 -0
- package/dist/pipeline/embeddings.js +82 -0
- package/dist/pipeline/embeddings.js.map +1 -0
- package/dist/pipeline/exclusions.d.ts +86 -0
- package/dist/pipeline/exclusions.js +320 -0
- package/dist/pipeline/exclusions.js.map +1 -0
- package/dist/pipeline/metrics.d.ts +12 -0
- package/dist/pipeline/metrics.js +278 -0
- package/dist/pipeline/metrics.js.map +1 -0
- package/dist/pipeline/ml-config.d.ts +23 -0
- package/dist/pipeline/ml-config.js +54 -0
- package/dist/pipeline/ml-config.js.map +1 -0
- package/dist/pipeline/models.d.ts +23 -0
- package/dist/pipeline/models.js +21 -0
- package/dist/pipeline/models.js.map +1 -0
- package/dist/pipeline/nlp.d.ts +20 -0
- package/dist/pipeline/nlp.js +200 -0
- package/dist/pipeline/nlp.js.map +1 -0
- package/dist/pipeline/parsers.d.ts +11 -0
- package/dist/pipeline/parsers.js +492 -0
- package/dist/pipeline/parsers.js.map +1 -0
- package/dist/pipeline/registry.d.ts +21 -0
- package/dist/pipeline/registry.js +45 -0
- package/dist/pipeline/registry.js.map +1 -0
- package/dist/pipeline/style.d.ts +37 -0
- package/dist/pipeline/style.js +204 -0
- package/dist/pipeline/style.js.map +1 -0
- package/dist/pipeline/sync.d.ts +8 -0
- package/dist/pipeline/sync.js +52 -0
- package/dist/pipeline/sync.js.map +1 -0
- package/dist/pipeline/trends.d.ts +8 -0
- package/dist/pipeline/trends.js +226 -0
- package/dist/pipeline/trends.js.map +1 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +216 -0
- package/dist/server.js.map +1 -0
- package/frontend/dist/_astro/MethodologyModal.astro_astro_type_script_index_0_lang.jiHwSrn-.js +34 -0
- package/frontend/dist/_astro/index.Ck1ZXjve.css +1 -0
- package/frontend/dist/_astro/index.astro_astro_type_script_index_0_lang.PuBlxVje.js +37 -0
- package/frontend/dist/_astro/index.astro_astro_type_script_index_1_lang.DmQY6kFx.js +1 -0
- package/frontend/dist/_astro/theme.CbYAaQI4.js +1 -0
- package/frontend/dist/_astro/wrapped.CpzRcLjf.css +1 -0
- package/frontend/dist/_astro/wrapped.astro_astro_type_script_index_0_lang.D4GeWu2-.js +11 -0
- package/frontend/dist/_astro/wrapped.astro_astro_type_script_index_1_lang.CPAAJDh5.js +1 -0
- package/frontend/dist/favicon.svg +4 -0
- package/frontend/dist/images/card_architect.png +0 -0
- package/frontend/dist/images/card_commander.png +0 -0
- package/frontend/dist/images/card_delegator.png +0 -0
- package/frontend/dist/images/card_explorer.png +0 -0
- package/frontend/dist/images/card_partner.png +0 -0
- package/frontend/dist/images/char_architect.png +0 -0
- package/frontend/dist/images/char_commander.png +0 -0
- package/frontend/dist/images/char_delegator.png +0 -0
- package/frontend/dist/images/char_explorer.png +0 -0
- package/frontend/dist/images/char_partner.png +0 -0
- package/frontend/dist/index.html +9 -0
- package/frontend/dist/wrapped/index.html +9 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Eeshan Srivastava
|
|
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
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
> **Disclaimer:** This is an independent personal project, **not affiliated with, endorsed by, or connected to Anthropic or Claude** in any way. This is a non-commercial, open-source tool created for personal use and educational purposes only.
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
# How I Prompt
|
|
6
|
+
|
|
7
|
+
**Local-first analytics for your AI coding conversations — prompts, personas, trends, and a personal wrapped view.**
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/@eeshans/howiprompt)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
[](package.json)
|
|
12
|
+
[]()
|
|
13
|
+
[](https://howiprompt.eeshans.com)
|
|
14
|
+
|
|
15
|
+
[Live Dashboard](https://howiprompt.eeshans.com) • [Live Wrapped](https://howiprompt.eeshans.com/wrapped) • [Source](https://github.com/eeshansrivastava89/howiprompt)
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx @eeshans/howiprompt
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
> **Requirements:** [Node.js 18+](https://nodejs.org/). Works with Claude Code, Codex, Copilot Chat, Cursor, and LM Studio logs. The local package ships with analytics disabled.
|
|
22
|
+
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<br>
|
|
26
|
+
|
|
27
|
+
<p align="center">
|
|
28
|
+
<img src="https://github.com/user-attachments/assets/12c6fc86-0d08-45c1-b94b-39c3bfbff93d" alt="How I Prompt dashboard" width="900">
|
|
29
|
+
</p>
|
|
30
|
+
|
|
31
|
+
## Highlights
|
|
32
|
+
|
|
33
|
+
| | |
|
|
34
|
+
|---|---|
|
|
35
|
+
| **Local-first pipeline** | Sync, parsing, embeddings, classifier scoring, and metrics run on your machine |
|
|
36
|
+
| **Multi-source support** | Claude Code, Codex, Copilot Chat, Cursor, and LM Studio |
|
|
37
|
+
| **Two ways to explore** | Standard dashboard plus a scroll-through wrapped experience |
|
|
38
|
+
| **Metrics that feel personal** | Vibe Coder Index, Politeness, activity trends, heatmaps, and personas |
|
|
39
|
+
| **Private by default** | Raw logs stay local and the npm package ships with analytics disabled |
|
|
40
|
+
| **Fast repeat refreshes** | Incremental rebuilds reuse the local DB, caches, and exclusions |
|
|
41
|
+
|
|
42
|
+
## What it does
|
|
43
|
+
|
|
44
|
+
How I Prompt syncs local AI conversation logs, computes prompting metrics on-device, and opens a browser dashboard with both an overview page and a scroll-through wrapped experience.
|
|
45
|
+
|
|
46
|
+
That starts a local server and opens the dashboard. On first run, a setup wizard detects supported backends, lets you confirm sources, and then runs the pipeline.
|
|
47
|
+
|
|
48
|
+
### Options
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npx @eeshans/howiprompt --no-open # don't auto-open browser
|
|
52
|
+
npx @eeshans/howiprompt --port 4000 # custom port
|
|
53
|
+
npx @eeshans/howiprompt --help # usage info
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### What Happens
|
|
57
|
+
|
|
58
|
+
1. Detects supported local backends and writes setup to `~/.howiprompt/config.json`
|
|
59
|
+
2. Copies raw conversation data into `~/.howiprompt/raw/`
|
|
60
|
+
3. Parses and stores messages in a local SQLite database at `~/.howiprompt/data.db`
|
|
61
|
+
4. Runs embeddings and classifier scoring for dashboard metrics
|
|
62
|
+
5. Writes `~/.howiprompt/metrics.json` and serves the dashboard at `localhost`
|
|
63
|
+
|
|
64
|
+
Subsequent refreshes are incremental and reuse the local database, caches, and configured exclusions.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## What You Get
|
|
69
|
+
|
|
70
|
+
| Dashboard | Full Experience |
|
|
71
|
+
|-----------|-----------------|
|
|
72
|
+
| One-page overview of your stats | Scroll-through "Wrapped" presentation |
|
|
73
|
+
| [howiprompt.eeshans.com](https://howiprompt.eeshans.com) | [howiprompt.eeshans.com/wrapped](https://howiprompt.eeshans.com/wrapped) |
|
|
74
|
+
|
|
75
|
+
**Metrics include:** total prompts, conversation depth, activity heatmap, model usage, Vibe Coder Index, Politeness, persona classification (2×2: Detail Level × Communication Style), and trends.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Data Sources
|
|
80
|
+
|
|
81
|
+
| Source | Location |
|
|
82
|
+
|--------|----------|
|
|
83
|
+
| **Claude Code** | `~/.claude/projects/*.jsonl` |
|
|
84
|
+
| **Codex** | `~/.codex/history.jsonl` |
|
|
85
|
+
| **Copilot Chat** | `~/Library/Application Support/Code/User/workspaceStorage` |
|
|
86
|
+
| **Cursor** | `~/Library/Application Support/Cursor/User/workspaceStorage` |
|
|
87
|
+
| **LM Studio** | `~/.lmstudio/conversations` |
|
|
88
|
+
|
|
89
|
+
All supported sources are auto-synced into `~/.howiprompt/raw/` and reused across refreshes.
|
|
90
|
+
|
|
91
|
+
### Backend Status
|
|
92
|
+
|
|
93
|
+
- Supported today: `Claude Code`, `Codex`, `Copilot Chat`, `Cursor`, `LM Studio`
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Privacy
|
|
98
|
+
|
|
99
|
+
- **Local by default** — Sync, parsing, embeddings, classifier scoring, and metrics run on your machine
|
|
100
|
+
- **Persistent storage** — Raw copies, local DB, config, and metrics live under `~/.howiprompt/`
|
|
101
|
+
- **No prompt text leaves your machine** — The app does not upload raw logs or prompt content
|
|
102
|
+
- **No analytics in the local app** — The `npx @eeshans/howiprompt` package ships with analytics disabled. PostHog is only enabled on the hosted website
|
|
103
|
+
- **Ancillary network requests** — The dashboard loads ApexCharts from a CDN. The CLI checks npm for version updates. These do not transmit prompt data
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## The 4 Personas
|
|
108
|
+
|
|
109
|
+
Your persona is derived from two independent axes: **Detail Level** (brief → detailed) and **Communication Style** (directive → collaborative). These form a 2×2 grid validated on 21k prompts.
|
|
110
|
+
|
|
111
|
+
- **The Commander**: Brief + Directive — short, decisive instructions
|
|
112
|
+
- **The Partner**: Brief + Collaborative — quick exchanges, conversational flow
|
|
113
|
+
- **The Architect**: Detailed + Directive — specs, constraints, numbered requirements
|
|
114
|
+
- **The Explorer**: Detailed + Collaborative — context-rich, question-driven investigation
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Development
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# Install deps
|
|
122
|
+
npm install
|
|
123
|
+
|
|
124
|
+
# Build TypeScript
|
|
125
|
+
npm run build
|
|
126
|
+
|
|
127
|
+
# Run tests
|
|
128
|
+
npm test
|
|
129
|
+
|
|
130
|
+
# Build frontend
|
|
131
|
+
cd frontend && npm run build
|
|
132
|
+
|
|
133
|
+
# Build for distribution
|
|
134
|
+
npm run build:cli
|
|
135
|
+
|
|
136
|
+
# Privacy gate (run before publish)
|
|
137
|
+
npm run check:privacy
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Requirements
|
|
141
|
+
|
|
142
|
+
- Node.js 18+
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
MIT License — Not affiliated with Anthropic.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idempotent database schema bootstrap for howiprompt.
|
|
3
|
+
* Uses @libsql/client — safe to run every launch.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createClient } from "@libsql/client";
|
|
7
|
+
|
|
8
|
+
const SCHEMA_SQL = [
|
|
9
|
+
`CREATE TABLE IF NOT EXISTS messages (
|
|
10
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
11
|
+
hash TEXT UNIQUE NOT NULL,
|
|
12
|
+
timestamp TEXT NOT NULL,
|
|
13
|
+
platform TEXT NOT NULL,
|
|
14
|
+
role TEXT NOT NULL,
|
|
15
|
+
content TEXT NOT NULL,
|
|
16
|
+
conversation_id TEXT NOT NULL,
|
|
17
|
+
word_count INTEGER NOT NULL,
|
|
18
|
+
model_id TEXT,
|
|
19
|
+
model_provider TEXT,
|
|
20
|
+
local_hour INTEGER NOT NULL,
|
|
21
|
+
local_weekday INTEGER NOT NULL,
|
|
22
|
+
local_date TEXT NOT NULL,
|
|
23
|
+
source_file TEXT,
|
|
24
|
+
synced_at TEXT NOT NULL,
|
|
25
|
+
embedding BLOB
|
|
26
|
+
)`,
|
|
27
|
+
|
|
28
|
+
`CREATE TABLE IF NOT EXISTS nlp_enrichments (
|
|
29
|
+
message_id INTEGER PRIMARY KEY REFERENCES messages(id),
|
|
30
|
+
intent TEXT,
|
|
31
|
+
intent_confidence REAL,
|
|
32
|
+
complexity_score REAL,
|
|
33
|
+
complexity_confidence REAL,
|
|
34
|
+
iteration_score REAL,
|
|
35
|
+
iteration_confidence REAL
|
|
36
|
+
)`,
|
|
37
|
+
|
|
38
|
+
`CREATE TABLE IF NOT EXISTS sync_log (
|
|
39
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
40
|
+
source TEXT NOT NULL,
|
|
41
|
+
last_file TEXT,
|
|
42
|
+
last_timestamp TEXT,
|
|
43
|
+
message_count INTEGER,
|
|
44
|
+
synced_at TEXT NOT NULL
|
|
45
|
+
)`,
|
|
46
|
+
|
|
47
|
+
`CREATE INDEX IF NOT EXISTS idx_messages_platform_role ON messages(platform, role)`,
|
|
48
|
+
`CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id)`,
|
|
49
|
+
`CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages(timestamp)`,
|
|
50
|
+
`CREATE INDEX IF NOT EXISTS idx_messages_hash ON messages(hash)`,
|
|
51
|
+
`CREATE INDEX IF NOT EXISTS idx_messages_local_date ON messages(local_date)`,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Additive migrations — safe to re-run (catch duplicate column errors).
|
|
56
|
+
*/
|
|
57
|
+
const MIGRATIONS = [
|
|
58
|
+
// Phase 2: semantic classifiers
|
|
59
|
+
`ALTER TABLE nlp_enrichments ADD COLUMN hitl_score REAL`,
|
|
60
|
+
`ALTER TABLE nlp_enrichments ADD COLUMN hitl_confidence REAL`,
|
|
61
|
+
`ALTER TABLE nlp_enrichments ADD COLUMN vibe_score REAL`,
|
|
62
|
+
`ALTER TABLE nlp_enrichments ADD COLUMN vibe_confidence REAL`,
|
|
63
|
+
// Phase 6: radar axes
|
|
64
|
+
`ALTER TABLE nlp_enrichments ADD COLUMN precision_score REAL`,
|
|
65
|
+
`ALTER TABLE nlp_enrichments ADD COLUMN precision_confidence REAL`,
|
|
66
|
+
`ALTER TABLE nlp_enrichments ADD COLUMN curiosity_score REAL`,
|
|
67
|
+
`ALTER TABLE nlp_enrichments ADD COLUMN curiosity_confidence REAL`,
|
|
68
|
+
`ALTER TABLE nlp_enrichments ADD COLUMN tenacity_score REAL`,
|
|
69
|
+
`ALTER TABLE nlp_enrichments ADD COLUMN tenacity_confidence REAL`,
|
|
70
|
+
`ALTER TABLE nlp_enrichments ADD COLUMN trust_score REAL`,
|
|
71
|
+
`ALTER TABLE nlp_enrichments ADD COLUMN trust_confidence REAL`,
|
|
72
|
+
// Politeness embedding classifier
|
|
73
|
+
`ALTER TABLE nlp_enrichments ADD COLUMN politeness_score REAL`,
|
|
74
|
+
`ALTER TABLE nlp_enrichments ADD COLUMN politeness_confidence REAL`,
|
|
75
|
+
// Skill invocation flagging
|
|
76
|
+
`ALTER TABLE messages ADD COLUMN is_skill_invocation INTEGER DEFAULT 0`,
|
|
77
|
+
`ALTER TABLE messages ADD COLUMN matched_skill_id INTEGER REFERENCES skills(id)`,
|
|
78
|
+
// Skill source type (discovered from disk vs config stored in DB)
|
|
79
|
+
`ALTER TABLE skills ADD COLUMN source TEXT DEFAULT 'discovered'`,
|
|
80
|
+
// Exclusion rule reference on messages
|
|
81
|
+
`ALTER TABLE messages ADD COLUMN matched_rule_id INTEGER REFERENCES exclusion_rules(id)`,
|
|
82
|
+
// Rename flag: is_skill_invocation -> is_excluded (broader meaning)
|
|
83
|
+
`ALTER TABLE messages ADD COLUMN is_excluded INTEGER DEFAULT 0`,
|
|
84
|
+
// 2×2 style scores (replaces old persona radar axes)
|
|
85
|
+
`ALTER TABLE nlp_enrichments ADD COLUMN detail_score REAL`,
|
|
86
|
+
`ALTER TABLE nlp_enrichments ADD COLUMN style_score REAL`,
|
|
87
|
+
`ALTER TABLE nlp_enrichments ADD COLUMN quadrant TEXT`,
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* New tables added in later phases.
|
|
92
|
+
*/
|
|
93
|
+
const NEW_TABLES = [
|
|
94
|
+
`CREATE TABLE IF NOT EXISTS reference_embeddings (
|
|
95
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
96
|
+
classifier TEXT NOT NULL,
|
|
97
|
+
cluster TEXT NOT NULL,
|
|
98
|
+
prompt TEXT NOT NULL,
|
|
99
|
+
embedding F32_BLOB(384) NOT NULL
|
|
100
|
+
)`,
|
|
101
|
+
|
|
102
|
+
`CREATE TABLE IF NOT EXISTS skills (
|
|
103
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
104
|
+
platform TEXT NOT NULL,
|
|
105
|
+
skill_name TEXT NOT NULL,
|
|
106
|
+
skill_path TEXT NOT NULL,
|
|
107
|
+
invocation_pattern TEXT,
|
|
108
|
+
template_content TEXT,
|
|
109
|
+
content_hash TEXT,
|
|
110
|
+
discovered_at TEXT NOT NULL,
|
|
111
|
+
UNIQUE(platform, skill_name)
|
|
112
|
+
)`,
|
|
113
|
+
|
|
114
|
+
`CREATE TABLE IF NOT EXISTS exclusion_rules (
|
|
115
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
116
|
+
platform TEXT NOT NULL DEFAULT '*',
|
|
117
|
+
rule_type TEXT NOT NULL,
|
|
118
|
+
pattern TEXT NOT NULL,
|
|
119
|
+
match_mode TEXT NOT NULL DEFAULT 'starts_with',
|
|
120
|
+
description TEXT,
|
|
121
|
+
source TEXT NOT NULL DEFAULT 'system',
|
|
122
|
+
template_content TEXT,
|
|
123
|
+
is_active INTEGER NOT NULL DEFAULT 1,
|
|
124
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
125
|
+
UNIQUE(platform, rule_type, pattern)
|
|
126
|
+
)`,
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Bootstrap the database schema at the given path.
|
|
131
|
+
* @param {string} dbPath — absolute path to the SQLite file
|
|
132
|
+
*/
|
|
133
|
+
export async function bootstrapDb(dbPath) {
|
|
134
|
+
const client = createClient({ url: `file:${dbPath}` });
|
|
135
|
+
|
|
136
|
+
for (const sql of SCHEMA_SQL) {
|
|
137
|
+
await client.execute(sql);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// New tables
|
|
141
|
+
for (const sql of NEW_TABLES) {
|
|
142
|
+
await client.execute(sql);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Additive migrations (ignore duplicate column errors)
|
|
146
|
+
for (const sql of MIGRATIONS) {
|
|
147
|
+
try {
|
|
148
|
+
await client.execute(sql);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
if (!String(err).includes("duplicate column")) throw err;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Migrate is_skill_invocation -> is_excluded (one-time)
|
|
155
|
+
await client.execute("UPDATE messages SET is_excluded = is_skill_invocation WHERE is_excluded = 0 AND is_skill_invocation = 1");
|
|
156
|
+
|
|
157
|
+
// Legacy cleanup: remove rows from obsolete 'agent' platform.
|
|
158
|
+
// Skill invocation flagging is now handled by src/pipeline/skills.ts
|
|
159
|
+
// at runtime — messages are kept but flagged (is_skill_invocation = 1).
|
|
160
|
+
await client.execute("DELETE FROM nlp_enrichments WHERE message_id IN (SELECT id FROM messages WHERE platform = 'agent')");
|
|
161
|
+
await client.execute("DELETE FROM messages WHERE platform = 'agent'");
|
|
162
|
+
await client.execute("DELETE FROM nlp_enrichments WHERE message_id IN (SELECT id FROM messages WHERE conversation_id LIKE 'agent-%')");
|
|
163
|
+
await client.execute("DELETE FROM messages WHERE conversation_id LIKE 'agent-%'");
|
|
164
|
+
|
|
165
|
+
client.close();
|
|
166
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for CLI — testable without side effects.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import net from "node:net";
|
|
6
|
+
import http from "node:http";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { exec } from "node:child_process";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the data directory: ~/.howiprompt
|
|
13
|
+
*/
|
|
14
|
+
export function resolveDataDir() {
|
|
15
|
+
return path.join(os.homedir(), ".howiprompt");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Find a free TCP port. Tries preferred first, falls back to OS-assigned.
|
|
20
|
+
*/
|
|
21
|
+
export function findFreePort(preferred) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const srv = net.createServer();
|
|
24
|
+
srv.listen(preferred ?? 0, "127.0.0.1", () => {
|
|
25
|
+
const addr = srv.address();
|
|
26
|
+
if (addr && typeof addr === "object") {
|
|
27
|
+
const port = addr.port;
|
|
28
|
+
srv.close(() => resolve(port));
|
|
29
|
+
} else {
|
|
30
|
+
reject(new Error("Could not determine port"));
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
srv.on("error", (err) => {
|
|
34
|
+
if (preferred && err.code === "EADDRINUSE") {
|
|
35
|
+
findFreePort(null).then(resolve, reject);
|
|
36
|
+
} else {
|
|
37
|
+
reject(err);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Poll a URL until it responds with status < 500 or timeout.
|
|
45
|
+
*/
|
|
46
|
+
export function waitForServer(url, timeoutMs = 15_000) {
|
|
47
|
+
const start = Date.now();
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const check = () => {
|
|
50
|
+
if (Date.now() - start > timeoutMs) {
|
|
51
|
+
reject(new Error(`Server did not start within ${timeoutMs}ms`));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const req = http.get(url, (res) => {
|
|
55
|
+
if (res.statusCode && res.statusCode < 500) {
|
|
56
|
+
resolve();
|
|
57
|
+
} else {
|
|
58
|
+
setTimeout(check, 200);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
req.on("error", () => setTimeout(check, 200));
|
|
62
|
+
req.end();
|
|
63
|
+
};
|
|
64
|
+
check();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Parse CLI arguments.
|
|
70
|
+
*/
|
|
71
|
+
export function parseArgs(argv) {
|
|
72
|
+
const help = argv.includes("--help") || argv.includes("-h");
|
|
73
|
+
const version = argv.includes("--version") || argv.includes("-v");
|
|
74
|
+
const noOpen = argv.includes("--no-open");
|
|
75
|
+
const portIdx = argv.indexOf("--port");
|
|
76
|
+
const port = portIdx !== -1 ? Number(argv[portIdx + 1]) : null;
|
|
77
|
+
return { port, noOpen, help, version };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Open a URL in the default browser. Cross-platform.
|
|
82
|
+
*/
|
|
83
|
+
export async function openBrowser(url) {
|
|
84
|
+
const { default: open } = await import("open");
|
|
85
|
+
return open(url);
|
|
86
|
+
}
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI entry point for How I Prompt.
|
|
5
|
+
* Usage: npx @eeshans/howiprompt [--port <n>] [--no-open] [--help] [--version]
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { execSync } from "node:child_process";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
|
|
13
|
+
import { bootstrapDb } from "./bootstrap-db.mjs";
|
|
14
|
+
import { resolveDataDir, findFreePort, parseArgs, openBrowser, waitForServer } from "./cli-helpers.mjs";
|
|
15
|
+
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const pkgPath = path.join(__dirname, "..", "package.json");
|
|
18
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
19
|
+
|
|
20
|
+
// ── Color helpers ──────────────────────────────────────
|
|
21
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
22
|
+
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
23
|
+
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
24
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
25
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
26
|
+
|
|
27
|
+
// ── Parse args ─────────────────────────────────────────
|
|
28
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
29
|
+
|
|
30
|
+
if (parsed.help) {
|
|
31
|
+
console.log(`
|
|
32
|
+
${bold("howiprompt")} v${pkg.version}
|
|
33
|
+
Local-first analytics dashboard for Claude Code + Codex prompting patterns.
|
|
34
|
+
|
|
35
|
+
Usage: howiprompt [options]
|
|
36
|
+
|
|
37
|
+
Options:
|
|
38
|
+
--port <n> Use a specific port (default: auto)
|
|
39
|
+
--no-open Don't open the browser automatically
|
|
40
|
+
--help Show this help message
|
|
41
|
+
--version Show version number
|
|
42
|
+
`);
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (parsed.version) {
|
|
47
|
+
console.log(pkg.version);
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Check Node version ─────────────────────────────────
|
|
52
|
+
const [major] = process.versions.node.split(".").map(Number);
|
|
53
|
+
if (major < 18) {
|
|
54
|
+
console.error(red(`Node >= 18.0.0 required (found ${process.versions.node})`));
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Resolve data directory ─────────────────────────────
|
|
59
|
+
const dataDir = resolveDataDir();
|
|
60
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
61
|
+
fs.mkdirSync(path.join(dataDir, "raw", "claude_code"), { recursive: true });
|
|
62
|
+
fs.mkdirSync(path.join(dataDir, "raw", "codex"), { recursive: true });
|
|
63
|
+
fs.mkdirSync(path.join(dataDir, "raw", "copilot_chat"), { recursive: true });
|
|
64
|
+
fs.mkdirSync(path.join(dataDir, "raw", "cursor"), { recursive: true });
|
|
65
|
+
fs.mkdirSync(path.join(dataDir, "raw", "lmstudio"), { recursive: true });
|
|
66
|
+
|
|
67
|
+
console.log(`\n${bold("howiprompt")} v${pkg.version}\n`);
|
|
68
|
+
|
|
69
|
+
// ── Bootstrap database ─────────────────────────────────
|
|
70
|
+
const dbPath = path.join(dataDir, "data.db");
|
|
71
|
+
process.stdout.write(` Checking environment... ${dim("Node " + process.versions.node)} `);
|
|
72
|
+
console.log(green("ok"));
|
|
73
|
+
|
|
74
|
+
process.stdout.write(" Initializing database... ");
|
|
75
|
+
await bootstrapDb(dbPath);
|
|
76
|
+
console.log(green("done"));
|
|
77
|
+
|
|
78
|
+
// ── Ensure wizard shows when there's no data ──────────
|
|
79
|
+
const metricsPath = path.join(dataDir, "metrics.json");
|
|
80
|
+
if (!fs.existsSync(metricsPath)) {
|
|
81
|
+
const configPath = path.join(dataDir, "config.json");
|
|
82
|
+
try {
|
|
83
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
84
|
+
if (cfg.hasCompletedSetup) {
|
|
85
|
+
cfg.hasCompletedSetup = false;
|
|
86
|
+
fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
|
|
87
|
+
}
|
|
88
|
+
} catch { /* no config yet — wizard will show by default */ }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Auto-build if needed ──────────────────────────────
|
|
92
|
+
const projectRoot = path.join(__dirname, "..");
|
|
93
|
+
|
|
94
|
+
process.stdout.write(" Building backend... ");
|
|
95
|
+
try {
|
|
96
|
+
execSync("npm run build", { cwd: projectRoot, stdio: "pipe" });
|
|
97
|
+
console.log(green("done"));
|
|
98
|
+
} catch (e) {
|
|
99
|
+
console.log(red("failed"));
|
|
100
|
+
console.error(e.stderr?.toString() || e.message);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
process.stdout.write(" Building frontend... ");
|
|
105
|
+
try {
|
|
106
|
+
execSync("npm run build", { cwd: path.join(projectRoot, "frontend"), stdio: "pipe" });
|
|
107
|
+
console.log(green("done"));
|
|
108
|
+
} catch (e) {
|
|
109
|
+
console.log(red("failed"));
|
|
110
|
+
console.error(e.stderr?.toString() || e.message);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── Start server ───────────────────────────────────────
|
|
115
|
+
const port = await findFreePort(parsed.port);
|
|
116
|
+
|
|
117
|
+
process.stdout.write(" Starting server... ");
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const { startServer } = await import("../dist/server.js");
|
|
121
|
+
const server = await startServer({ port, dataDir, dbPath });
|
|
122
|
+
const serverUrl = `http://localhost:${port}`;
|
|
123
|
+
console.log(green(serverUrl));
|
|
124
|
+
|
|
125
|
+
console.log(`\n Dashboard ready. Press ${bold("Ctrl+C")} to stop.\n`);
|
|
126
|
+
|
|
127
|
+
// Non-blocking version check
|
|
128
|
+
let versionCheckTimeout = null;
|
|
129
|
+
(async () => {
|
|
130
|
+
try {
|
|
131
|
+
const controller = new AbortController();
|
|
132
|
+
versionCheckTimeout = setTimeout(() => controller.abort(), 3000);
|
|
133
|
+
versionCheckTimeout.unref?.();
|
|
134
|
+
const res = await fetch("https://registry.npmjs.org/howiprompt/latest", {
|
|
135
|
+
signal: controller.signal,
|
|
136
|
+
});
|
|
137
|
+
clearTimeout(versionCheckTimeout);
|
|
138
|
+
versionCheckTimeout = null;
|
|
139
|
+
if (!res.ok) return;
|
|
140
|
+
const data = await res.json();
|
|
141
|
+
const latest = data.version;
|
|
142
|
+
if (latest && latest !== pkg.version) {
|
|
143
|
+
const l = latest.split(".").map(Number);
|
|
144
|
+
const c = pkg.version.split(".").map(Number);
|
|
145
|
+
const newer =
|
|
146
|
+
l[0] > c[0] ||
|
|
147
|
+
(l[0] === c[0] && l[1] > c[1]) ||
|
|
148
|
+
(l[0] === c[0] && l[1] === c[1] && l[2] > c[2]);
|
|
149
|
+
if (newer) {
|
|
150
|
+
console.log(` ${yellow("Update available:")} ${pkg.version} → ${green(latest)}`);
|
|
151
|
+
console.log(` Run: ${bold("npx @eeshans/howiprompt@latest")}\n`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
// Silent on failure
|
|
156
|
+
} finally {
|
|
157
|
+
if (versionCheckTimeout) {
|
|
158
|
+
clearTimeout(versionCheckTimeout);
|
|
159
|
+
versionCheckTimeout = null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
})();
|
|
163
|
+
|
|
164
|
+
if (!parsed.noOpen) {
|
|
165
|
+
openBrowser(serverUrl);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let shuttingDown = false;
|
|
169
|
+
function shutdown() {
|
|
170
|
+
if (shuttingDown) return;
|
|
171
|
+
shuttingDown = true;
|
|
172
|
+
console.log("\n Shutting down...");
|
|
173
|
+
|
|
174
|
+
if (versionCheckTimeout) {
|
|
175
|
+
clearTimeout(versionCheckTimeout);
|
|
176
|
+
versionCheckTimeout = null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const hardExitTimer = setTimeout(() => {
|
|
180
|
+
process.exit(0);
|
|
181
|
+
}, 2000);
|
|
182
|
+
hardExitTimer.unref?.();
|
|
183
|
+
|
|
184
|
+
server.close((closeErr) => {
|
|
185
|
+
clearTimeout(hardExitTimer);
|
|
186
|
+
if (closeErr) {
|
|
187
|
+
console.log(red(`error during shutdown: ${closeErr.message}`));
|
|
188
|
+
process.exit(1);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
process.exitCode = 0;
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
process.once("SIGINT", shutdown);
|
|
196
|
+
process.once("SIGTERM", shutdown);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
if (err.code === "ERR_MODULE_NOT_FOUND") {
|
|
199
|
+
console.log(yellow("server not built yet — run `npm run build` first"));
|
|
200
|
+
process.exit(1);
|
|
201
|
+
} else {
|
|
202
|
+
console.log(red(`error: ${err.message}`));
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
}
|
package/config/ml.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"embedding": {
|
|
3
|
+
"model": "Xenova/bge-small-en-v1.5",
|
|
4
|
+
"dtype": "int8",
|
|
5
|
+
"dimensions": 384,
|
|
6
|
+
"batchSize": 64
|
|
7
|
+
},
|
|
8
|
+
"hitl": {
|
|
9
|
+
"weights": {
|
|
10
|
+
"course_correction": 20,
|
|
11
|
+
"architectural_decision": 18,
|
|
12
|
+
"constraint_spec": 15,
|
|
13
|
+
"scope_control": 15,
|
|
14
|
+
"review_qa": 12,
|
|
15
|
+
"tradeoff_nav": 12,
|
|
16
|
+
"passive_delegation": -10
|
|
17
|
+
},
|
|
18
|
+
"similarityThreshold": 0.55,
|
|
19
|
+
"confidenceFloor": 0.5,
|
|
20
|
+
"normalizationScale": 20
|
|
21
|
+
},
|
|
22
|
+
"vibe": {
|
|
23
|
+
"weights": {
|
|
24
|
+
"file_reference": 18,
|
|
25
|
+
"technical_spec": 15,
|
|
26
|
+
"code_sharing": 15,
|
|
27
|
+
"iterative_refinement": 12,
|
|
28
|
+
"high_level_delegation": -15,
|
|
29
|
+
"outcome_only": -12,
|
|
30
|
+
"acceptance": -10
|
|
31
|
+
},
|
|
32
|
+
"similarityThreshold": 0.50,
|
|
33
|
+
"centerPoint": 50,
|
|
34
|
+
"normalizationScale": 20
|
|
35
|
+
},
|
|
36
|
+
"politeness": {
|
|
37
|
+
"weights": {
|
|
38
|
+
"courteous": 20,
|
|
39
|
+
"warm_collaborative": 15,
|
|
40
|
+
"direct_neutral": -12,
|
|
41
|
+
"curt_dismissive": -20
|
|
42
|
+
},
|
|
43
|
+
"similarityThreshold": 0.50,
|
|
44
|
+
"centerPoint": 50,
|
|
45
|
+
"normalizationScale": 20
|
|
46
|
+
}
|
|
47
|
+
}
|
|
File without changes
|