@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.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +148 -0
  3. package/bin/bootstrap-db.mjs +166 -0
  4. package/bin/cli-helpers.mjs +86 -0
  5. package/bin/cli.mjs +205 -0
  6. package/config/ml.json +47 -0
  7. package/data/claude_code/.gitkeep +3 -0
  8. package/data/codex/.gitkeep +0 -0
  9. package/data/reference_clusters.json +314 -0
  10. package/dist/index.d.ts +18 -0
  11. package/dist/index.js +194 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/pipeline/backends.d.ts +39 -0
  14. package/dist/pipeline/backends.js +411 -0
  15. package/dist/pipeline/backends.js.map +1 -0
  16. package/dist/pipeline/classifiers.d.ts +17 -0
  17. package/dist/pipeline/classifiers.js +181 -0
  18. package/dist/pipeline/classifiers.js.map +1 -0
  19. package/dist/pipeline/config.d.ts +21 -0
  20. package/dist/pipeline/config.js +79 -0
  21. package/dist/pipeline/config.js.map +1 -0
  22. package/dist/pipeline/db.d.ts +41 -0
  23. package/dist/pipeline/db.js +130 -0
  24. package/dist/pipeline/db.js.map +1 -0
  25. package/dist/pipeline/embeddings.d.ts +15 -0
  26. package/dist/pipeline/embeddings.js +82 -0
  27. package/dist/pipeline/embeddings.js.map +1 -0
  28. package/dist/pipeline/exclusions.d.ts +86 -0
  29. package/dist/pipeline/exclusions.js +320 -0
  30. package/dist/pipeline/exclusions.js.map +1 -0
  31. package/dist/pipeline/metrics.d.ts +12 -0
  32. package/dist/pipeline/metrics.js +278 -0
  33. package/dist/pipeline/metrics.js.map +1 -0
  34. package/dist/pipeline/ml-config.d.ts +23 -0
  35. package/dist/pipeline/ml-config.js +54 -0
  36. package/dist/pipeline/ml-config.js.map +1 -0
  37. package/dist/pipeline/models.d.ts +23 -0
  38. package/dist/pipeline/models.js +21 -0
  39. package/dist/pipeline/models.js.map +1 -0
  40. package/dist/pipeline/nlp.d.ts +20 -0
  41. package/dist/pipeline/nlp.js +200 -0
  42. package/dist/pipeline/nlp.js.map +1 -0
  43. package/dist/pipeline/parsers.d.ts +11 -0
  44. package/dist/pipeline/parsers.js +492 -0
  45. package/dist/pipeline/parsers.js.map +1 -0
  46. package/dist/pipeline/registry.d.ts +21 -0
  47. package/dist/pipeline/registry.js +45 -0
  48. package/dist/pipeline/registry.js.map +1 -0
  49. package/dist/pipeline/style.d.ts +37 -0
  50. package/dist/pipeline/style.js +204 -0
  51. package/dist/pipeline/style.js.map +1 -0
  52. package/dist/pipeline/sync.d.ts +8 -0
  53. package/dist/pipeline/sync.js +52 -0
  54. package/dist/pipeline/sync.js.map +1 -0
  55. package/dist/pipeline/trends.d.ts +8 -0
  56. package/dist/pipeline/trends.js +226 -0
  57. package/dist/pipeline/trends.js.map +1 -0
  58. package/dist/server.d.ts +7 -0
  59. package/dist/server.js +216 -0
  60. package/dist/server.js.map +1 -0
  61. package/frontend/dist/_astro/MethodologyModal.astro_astro_type_script_index_0_lang.jiHwSrn-.js +34 -0
  62. package/frontend/dist/_astro/index.Ck1ZXjve.css +1 -0
  63. package/frontend/dist/_astro/index.astro_astro_type_script_index_0_lang.PuBlxVje.js +37 -0
  64. package/frontend/dist/_astro/index.astro_astro_type_script_index_1_lang.DmQY6kFx.js +1 -0
  65. package/frontend/dist/_astro/theme.CbYAaQI4.js +1 -0
  66. package/frontend/dist/_astro/wrapped.CpzRcLjf.css +1 -0
  67. package/frontend/dist/_astro/wrapped.astro_astro_type_script_index_0_lang.D4GeWu2-.js +11 -0
  68. package/frontend/dist/_astro/wrapped.astro_astro_type_script_index_1_lang.CPAAJDh5.js +1 -0
  69. package/frontend/dist/favicon.svg +4 -0
  70. package/frontend/dist/images/card_architect.png +0 -0
  71. package/frontend/dist/images/card_commander.png +0 -0
  72. package/frontend/dist/images/card_delegator.png +0 -0
  73. package/frontend/dist/images/card_explorer.png +0 -0
  74. package/frontend/dist/images/card_partner.png +0 -0
  75. package/frontend/dist/images/char_architect.png +0 -0
  76. package/frontend/dist/images/char_commander.png +0 -0
  77. package/frontend/dist/images/char_delegator.png +0 -0
  78. package/frontend/dist/images/char_explorer.png +0 -0
  79. package/frontend/dist/images/char_partner.png +0 -0
  80. package/frontend/dist/index.html +9 -0
  81. package/frontend/dist/wrapped/index.html +9 -0
  82. 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
+ [![npm](https://img.shields.io/npm/v/@eeshans/howiprompt)](https://www.npmjs.com/package/@eeshans/howiprompt)
10
+ [![license](https://img.shields.io/github/license/eeshansrivastava89/howiprompt)](LICENSE)
11
+ [![node](https://img.shields.io/node/v/@eeshans/howiprompt)](package.json)
12
+ [![platform](https://img.shields.io/badge/platform-macOS%20%7C%20Linux-blue)]()
13
+ [![demo](https://img.shields.io/badge/demo-live-cb9f6a)](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
+ }
@@ -0,0 +1,3 @@
1
+ # Place your Claude Code data here
2
+ # Copy from: ~/.claude/projects/
3
+ # Raw Claude Code copies are written here by the Node pipeline
File without changes