pmpt-cli 1.14.21 → 1.17.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/README.md CHANGED
@@ -10,6 +10,8 @@
10
10
 
11
11
  Plan with 5 questions. Build with AI. Save every version. Share and reproduce.
12
12
 
13
+ Every session, every decision, every constraint — accumulated in one file your AI can always read.
14
+
13
15
  [Quick Start](#quick-start) · [Commands](#commands) · [MCP Server](#mcp-server) · [Explore Projects](#explore-projects)
14
16
 
15
17
  </div>
@@ -65,6 +67,7 @@ pmpt explore
65
67
  | | Without pmpt | With pmpt |
66
68
  |---|---|---|
67
69
  | **Planning** | Stare at blank screen, write vague prompts | Answer 5 guided questions, get structured AI prompt |
70
+ | **Context** | Re-explain the same decisions every AI session | AI reads pmpt.md and knows why things are the way they are |
68
71
  | **Tracking** | Lose track of what you built and how | Every version auto-saved with full history |
69
72
  | **Sharing** | Share finished code only | Share the entire journey — others can reproduce it |
70
73
 
@@ -92,8 +95,13 @@ The generated prompt is **automatically copied to your clipboard**. Just paste i
92
95
 
93
96
  | Command | Description |
94
97
  |---------|-------------|
95
- | `pmpt init` | Initialize project and start tracking |
98
+ | `pmpt init` | Initialize project sets up `.pmpt/`, selects AI tool, creates entry points |
96
99
  | `pmpt plan` | 5 questions → AI prompt (auto-copied to clipboard) |
100
+ | `pmpt plan --template` | Generate a fillable `answers.json` (Windows/PowerShell friendly) |
101
+ | `pmpt plan --answers-file <f>` | Run plan non-interactively from a JSON file |
102
+ | `pmpt constraint add "<rule>"` | Add an architecture rule to pmpt.md |
103
+ | `pmpt constraint list` | List all constraints |
104
+ | `pmpt constraint remove <n>` | Remove a constraint by index |
97
105
  | `pmpt save` | Save current state as a snapshot |
98
106
  | `pmpt watch` | Auto-detect file changes and save versions |
99
107
  | `pmpt status` | Check project status, tracked files, and quality score |
@@ -213,18 +221,37 @@ All tools accept an optional `projectPath` parameter (defaults to cwd).
213
221
 
214
222
  ```
215
223
  your-project/
224
+ ├── CLAUDE.md # Claude Code entry point → points to .pmpt/index.md
225
+ ├── .cursorrules # Cursor entry point → points to .pmpt/index.md
226
+ ├── AGENTS.md # Codex entry point → points to .pmpt/index.md
216
227
  └── .pmpt/
228
+ ├── index.md # AI context map (entry point for all tools)
217
229
  ├── config.json # Project configuration
218
- ├── docs/ # Generated documents
230
+ ├── docs/
231
+ │ ├── pmpt.md # Single source of truth — architecture, constraints,
232
+ │ │ # decisions, lessons, progress, snapshot log
219
233
  │ ├── plan.md # Product plan (features checklist)
220
- ├── pmpt.md # Progress tracking & decisions
221
- │ └── pmpt.ai.md # AI-ready prompt (project context & instructions)
234
+ └── pmpt.ai.md # AI-ready prompt (paste into your AI tool)
222
235
  └── .history/ # Auto-saved version history
223
236
  ├── v1-2024-02-20/
224
237
  ├── v2-2024-02-21/
225
238
  └── ...
226
239
  ```
227
240
 
241
+ ### pmpt.md — Single Source of Truth
242
+
243
+ `pmpt.md` is the living document that your AI reads at the start of every session. It accumulates everything the AI needs to understand your project:
244
+
245
+ | Section | What to record |
246
+ |---------|---------------|
247
+ | `## Architecture` | High-level structure — update as it evolves |
248
+ | `## Active Work` | What's currently being built (clear when done) |
249
+ | `## Decisions` | WHY, not just WHAT — include what led to each decision |
250
+ | `## Constraints` | Platform/library limitations and workarounds |
251
+ | `## Lessons` | Anti-patterns: what failed → root cause → fix applied |
252
+
253
+ The more you fill this in, the less you re-explain to AI every session.
254
+
228
255
  ---
229
256
 
230
257
  ## .pmpt File Format
@@ -0,0 +1,92 @@
1
+ import * as p from '@clack/prompts';
2
+ import { resolve } from 'path';
3
+ import { isInitialized } from '../lib/config.js';
4
+ import { readConstraints, addConstraint, removeConstraint } from '../lib/harness.js';
5
+ export async function cmdConstraint(action, options = {}) {
6
+ const projectPath = resolve(process.cwd());
7
+ if (!isInitialized(projectPath)) {
8
+ p.intro('pmpt constraint');
9
+ p.log.error('Project not initialized. Run `pmpt init` first.');
10
+ process.exit(1);
11
+ }
12
+ if (action === 'list') {
13
+ const constraints = readConstraints(projectPath);
14
+ p.intro('pmpt constraint list');
15
+ if (constraints.length === 0) {
16
+ p.log.info('No constraints defined yet.');
17
+ p.log.message(` pmpt constraint add "<rule>"`);
18
+ }
19
+ else {
20
+ p.log.message('');
21
+ constraints.forEach((rule, i) => {
22
+ p.log.message(` ${i + 1}. ${rule}`);
23
+ });
24
+ p.log.message('');
25
+ p.log.info(`${constraints.length} constraint(s) in .pmpt/docs/constraints.md`);
26
+ }
27
+ p.outro('');
28
+ return;
29
+ }
30
+ if (action === 'add') {
31
+ p.intro('pmpt constraint add');
32
+ let rule = options.rule;
33
+ if (!rule) {
34
+ const input = await p.text({
35
+ message: 'Enter constraint rule:',
36
+ placeholder: 'e.g., UI does not import Service directly',
37
+ validate: (v) => (!v ? 'Rule cannot be empty' : undefined),
38
+ });
39
+ if (p.isCancel(input)) {
40
+ p.cancel('Cancelled');
41
+ process.exit(0);
42
+ }
43
+ rule = input;
44
+ }
45
+ const ok = addConstraint(projectPath, rule);
46
+ if (!ok) {
47
+ p.log.error('Could not find ## Constraints section in .pmpt/docs/pmpt.md');
48
+ process.exit(1);
49
+ }
50
+ p.log.success(`Added: ${rule}`);
51
+ p.log.info('File: .pmpt/docs/pmpt.md');
52
+ p.outro('');
53
+ return;
54
+ }
55
+ if (action === 'remove') {
56
+ p.intro('pmpt constraint remove');
57
+ const constraints = readConstraints(projectPath);
58
+ if (constraints.length === 0) {
59
+ p.log.warn('No constraints to remove.');
60
+ p.outro('');
61
+ return;
62
+ }
63
+ let idx;
64
+ if (options.index) {
65
+ idx = parseInt(options.index, 10);
66
+ }
67
+ else {
68
+ // Interactive selection
69
+ const choice = await p.select({
70
+ message: 'Select constraint to remove:',
71
+ options: constraints.map((rule, i) => ({
72
+ value: String(i + 1),
73
+ label: `${i + 1}. ${rule}`,
74
+ })),
75
+ });
76
+ if (p.isCancel(choice)) {
77
+ p.cancel('Cancelled');
78
+ process.exit(0);
79
+ }
80
+ idx = parseInt(choice, 10);
81
+ }
82
+ const removed = removeConstraint(projectPath, idx);
83
+ if (removed === null) {
84
+ p.log.error(`No constraint at index ${idx}.`);
85
+ }
86
+ else {
87
+ p.log.success(`Removed: ${removed}`);
88
+ }
89
+ p.outro('');
90
+ return;
91
+ }
92
+ }
@@ -0,0 +1,220 @@
1
+ import * as p from '@clack/prompts';
2
+ import { execSync } from 'child_process';
3
+ import { existsSync, readFileSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { homedir } from 'os';
6
+ import { detectPmptMcpPath, getWrapperStatus } from './mcp-setup.js';
7
+ function checkNodeVersion() {
8
+ const version = process.versions.node;
9
+ const major = parseInt(version.split('.')[0], 10);
10
+ if (major >= 18) {
11
+ return { label: 'Node.js', status: 'ok', detail: `v${version}` };
12
+ }
13
+ return {
14
+ label: 'Node.js',
15
+ status: 'fail',
16
+ detail: `v${version} (requires ≥18)`,
17
+ fix: 'Update Node.js: https://nodejs.org',
18
+ };
19
+ }
20
+ function checkNvm() {
21
+ const nvmDir = process.env.NVM_DIR || join(homedir(), '.nvm');
22
+ const isNvm = !!process.env.NVM_DIR || process.argv[1]?.includes('.nvm/');
23
+ if (!isNvm && !existsSync(nvmDir)) {
24
+ return { label: 'nvm', status: 'ok', detail: 'Not using nvm (no PATH issues)' };
25
+ }
26
+ // Check if default alias is set
27
+ let defaultAlias = '';
28
+ try {
29
+ const aliasFile = join(nvmDir, 'alias', 'default');
30
+ if (existsSync(aliasFile)) {
31
+ defaultAlias = readFileSync(aliasFile, 'utf-8').trim();
32
+ }
33
+ }
34
+ catch { /* ignore */ }
35
+ if (defaultAlias) {
36
+ return {
37
+ label: 'nvm',
38
+ status: 'warn',
39
+ detail: `Active (default: ${defaultAlias}). MCP clients may not load nvm in non-interactive shells.`,
40
+ fix: 'Use "pmpt mcp-setup" to create a stable wrapper, or consider volta/fnm.',
41
+ };
42
+ }
43
+ return {
44
+ label: 'nvm',
45
+ status: 'warn',
46
+ detail: 'Active, no default alias set. MCP path may break across sessions.',
47
+ fix: 'Run "nvm alias default <version>" and "pmpt mcp-setup".',
48
+ };
49
+ }
50
+ function checkPmptMcpBinary() {
51
+ const path = detectPmptMcpPath();
52
+ if (path) {
53
+ return { label: 'pmpt-mcp binary', status: 'ok', detail: path };
54
+ }
55
+ return {
56
+ label: 'pmpt-mcp binary',
57
+ status: 'fail',
58
+ detail: 'Not found in PATH or sibling directory',
59
+ fix: 'Run "npm install -g pmpt-cli" to install.',
60
+ };
61
+ }
62
+ function checkWrapper() {
63
+ const wrapper = getWrapperStatus();
64
+ if (!wrapper.exists) {
65
+ const isNvm = !!process.env.NVM_DIR || process.argv[1]?.includes('.nvm/');
66
+ if (isNvm) {
67
+ return {
68
+ label: 'Wrapper script',
69
+ status: 'warn',
70
+ detail: 'Not created (recommended for nvm users)',
71
+ fix: 'Run "pmpt mcp-setup" to create ~/.pmpt/bin/pmpt-mcp wrapper.',
72
+ };
73
+ }
74
+ return { label: 'Wrapper script', status: 'ok', detail: 'Not needed (not using nvm)' };
75
+ }
76
+ if (wrapper.targetValid) {
77
+ return { label: 'Wrapper script', status: 'ok', detail: `OK → ${wrapper.targetPath}` };
78
+ }
79
+ return {
80
+ label: 'Wrapper script',
81
+ status: 'fail',
82
+ detail: `Stale — target binary missing: ${wrapper.targetPath}`,
83
+ fix: 'Run "pmpt mcp-setup" to update the wrapper after Node version change.',
84
+ };
85
+ }
86
+ function checkMcpClients() {
87
+ const results = [];
88
+ // Claude Code
89
+ try {
90
+ execSync('which claude', { stdio: ['pipe', 'pipe', 'pipe'] });
91
+ try {
92
+ const list = execSync('claude mcp list', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
93
+ if (list.includes('pmpt')) {
94
+ results.push({ label: 'Claude Code MCP', status: 'ok', detail: 'pmpt registered' });
95
+ }
96
+ else {
97
+ results.push({
98
+ label: 'Claude Code MCP',
99
+ status: 'warn',
100
+ detail: 'Claude Code found but pmpt not registered',
101
+ fix: 'Run "pmpt mcp-setup" and select Claude Code.',
102
+ });
103
+ }
104
+ }
105
+ catch {
106
+ results.push({
107
+ label: 'Claude Code MCP',
108
+ status: 'warn',
109
+ detail: 'Claude Code found but "claude mcp list" failed',
110
+ fix: 'Make sure Claude Code is up to date.',
111
+ });
112
+ }
113
+ }
114
+ catch {
115
+ // Claude Code not installed — skip
116
+ }
117
+ // Cursor
118
+ const cursorConfig = join(homedir(), '.cursor', 'mcp.json');
119
+ if (existsSync(join(homedir(), '.cursor'))) {
120
+ try {
121
+ if (existsSync(cursorConfig)) {
122
+ const content = JSON.parse(readFileSync(cursorConfig, 'utf-8'));
123
+ if (content?.mcpServers?.pmpt) {
124
+ const cmd = content.mcpServers.pmpt.command;
125
+ if (existsSync(cmd)) {
126
+ results.push({ label: 'Cursor MCP', status: 'ok', detail: `pmpt → ${cmd}` });
127
+ }
128
+ else {
129
+ results.push({
130
+ label: 'Cursor MCP',
131
+ status: 'fail',
132
+ detail: `pmpt registered but binary missing: ${cmd}`,
133
+ fix: 'Run "pmpt mcp-setup" to update the path.',
134
+ });
135
+ }
136
+ }
137
+ else {
138
+ results.push({
139
+ label: 'Cursor MCP',
140
+ status: 'warn',
141
+ detail: 'Cursor found but pmpt not in mcp.json',
142
+ fix: 'Run "pmpt mcp-setup" and select Cursor.',
143
+ });
144
+ }
145
+ }
146
+ }
147
+ catch {
148
+ results.push({ label: 'Cursor MCP', status: 'warn', detail: 'Could not parse ~/.cursor/mcp.json' });
149
+ }
150
+ }
151
+ // Project .mcp.json
152
+ const projectMcp = join(process.cwd(), '.mcp.json');
153
+ if (existsSync(projectMcp)) {
154
+ try {
155
+ const content = JSON.parse(readFileSync(projectMcp, 'utf-8'));
156
+ if (content?.mcpServers?.pmpt) {
157
+ const cmd = content.mcpServers.pmpt.command;
158
+ if (existsSync(cmd)) {
159
+ results.push({ label: '.mcp.json', status: 'ok', detail: `pmpt → ${cmd}` });
160
+ }
161
+ else {
162
+ results.push({
163
+ label: '.mcp.json',
164
+ status: 'fail',
165
+ detail: `pmpt registered but binary missing: ${cmd}`,
166
+ fix: 'Run "pmpt mcp-setup" to update the path.',
167
+ });
168
+ }
169
+ }
170
+ }
171
+ catch {
172
+ results.push({ label: '.mcp.json', status: 'warn', detail: 'Could not parse .mcp.json' });
173
+ }
174
+ }
175
+ if (results.length === 0) {
176
+ results.push({
177
+ label: 'MCP clients',
178
+ status: 'warn',
179
+ detail: 'No MCP client configuration found',
180
+ fix: 'Run "pmpt mcp-setup" to configure an AI tool.',
181
+ });
182
+ }
183
+ return results;
184
+ }
185
+ const icons = { ok: '✓', warn: '⚠', fail: '✗' };
186
+ const colors = { ok: '\x1b[32m', warn: '\x1b[33m', fail: '\x1b[31m' };
187
+ const reset = '\x1b[0m';
188
+ export async function cmdDoctor() {
189
+ p.intro('pmpt doctor');
190
+ const s = p.spinner();
191
+ s.start('Running diagnostics...');
192
+ const checks = [
193
+ checkNodeVersion(),
194
+ checkNvm(),
195
+ checkPmptMcpBinary(),
196
+ checkWrapper(),
197
+ ...checkMcpClients(),
198
+ ];
199
+ s.stop('Diagnostics complete');
200
+ // Display results
201
+ const lines = [];
202
+ let hasIssues = false;
203
+ for (const check of checks) {
204
+ const icon = icons[check.status];
205
+ const color = colors[check.status];
206
+ lines.push(`${color}${icon}${reset} ${check.label}: ${check.detail}`);
207
+ if (check.fix) {
208
+ lines.push(` → ${check.fix}`);
209
+ hasIssues = true;
210
+ }
211
+ }
212
+ p.note(lines.join('\n'), 'Diagnostic Results');
213
+ if (hasIssues) {
214
+ p.log.warn('Some issues found. Run the suggested fixes above.');
215
+ }
216
+ else {
217
+ p.log.success('Everything looks good!');
218
+ }
219
+ p.outro('');
220
+ }
@@ -5,8 +5,9 @@ import { initializeProject, isInitialized, getDocsDir, ensurePmptClaudeMd, ensur
5
5
  import { isGitRepo, getGitInfo, formatGitInfo, getCommitCount } from '../lib/git.js';
6
6
  import { cmdPlan } from './plan.js';
7
7
  import { scanProject, scanResultToAnswers } from '../lib/scanner.js';
8
- import { savePlanDocuments, initPlanProgress, savePlanProgress } from '../lib/plan.js';
8
+ import { savePlanDocuments, initPlanProgress, savePlanProgress, generateAnswersTemplate } from '../lib/plan.js';
9
9
  import { copyToClipboard } from '../lib/clipboard.js';
10
+ import { setupHarnessForTools } from '../lib/harness.js';
10
11
  export async function cmdInit(path, options) {
11
12
  p.intro('pmpt init');
12
13
  const projectPath = path ? resolve(path) : process.cwd();
@@ -100,14 +101,34 @@ export async function cmdInit(path, options) {
100
101
  // Add pmpt MCP instructions to CLAUDE.md and register .mcp.json for Claude Code
101
102
  ensurePmptClaudeMd(projectPath);
102
103
  ensureMcpJson(projectPath);
104
+ // Ask which AI tool to set up harness for
105
+ const toolChoice = await p.select({
106
+ message: 'Which AI coding tool do you use?',
107
+ options: [
108
+ { value: 'claude', label: 'Claude Code', hint: 'CLAUDE.md + .pmpt/index.md' },
109
+ { value: 'cursor', label: 'Cursor', hint: '.cursorrules + .pmpt/index.md' },
110
+ { value: 'codex', label: 'Codex', hint: 'AGENTS.md + .pmpt/index.md' },
111
+ { value: 'all', label: 'All of the above', hint: 'Create all entry points' },
112
+ { value: 'skip', label: 'Skip', hint: 'Set up later with `pmpt harness`' },
113
+ ],
114
+ });
115
+ if (p.isCancel(toolChoice)) {
116
+ p.cancel('Cancelled');
117
+ process.exit(0);
118
+ }
119
+ if (toolChoice !== 'skip') {
120
+ setupHarnessForTools(projectPath, basename(projectPath), toolChoice);
121
+ }
103
122
  // Build folder structure display
104
123
  const notes = [
105
124
  `Path: ${config.projectPath}`,
106
125
  '',
107
126
  'Folder structure:',
108
127
  ` .pmpt/`,
128
+ ` ├── index.md AI entry point`,
109
129
  ` ├── config.json Config`,
110
- ` ├── docs/ Your docs`,
130
+ ` ├── docs/`,
131
+ ` │ └── pmpt.md Source of truth (architecture, constraints, decisions)`,
111
132
  ` └── .history/ Snapshots`,
112
133
  ];
113
134
  if (config.repo) {
@@ -155,6 +176,7 @@ export async function cmdInit(path, options) {
155
176
  options: [
156
177
  { value: 'auto', label: 'Auto-generate plan', hint: 'Recommended — instant AI prompt from project analysis' },
157
178
  { value: 'manual', label: 'Manual planning', hint: '5 questions interactive flow' },
179
+ { value: 'file', label: 'Fill in answers file', hint: 'Windows/PowerShell friendly — edit a JSON file then run pmpt plan --answers-file' },
158
180
  { value: 'skip', label: 'Skip for now' },
159
181
  ],
160
182
  });
@@ -162,6 +184,17 @@ export async function cmdInit(path, options) {
162
184
  p.cancel('Cancelled');
163
185
  process.exit(0);
164
186
  }
187
+ if (scanChoice === 'file') {
188
+ const outFile = 'answers.json';
189
+ const template = generateAnswersTemplate();
190
+ writeFileSync(resolve(projectPath, outFile), JSON.stringify(template, null, 2), 'utf-8');
191
+ p.log.success(`Template created: ${outFile}`);
192
+ p.log.info('Next steps:');
193
+ p.log.message(` 1. Open and fill in ${outFile}`);
194
+ p.log.message(` 2. pmpt plan --answers-file ${outFile}`);
195
+ p.outro('Ready!');
196
+ return;
197
+ }
165
198
  if (scanChoice === 'auto') {
166
199
  // Ask for project description
167
200
  const defaultDesc = scanResult.readmeDescription
@@ -236,23 +269,41 @@ export async function cmdInit(path, options) {
236
269
  }
237
270
  else {
238
271
  ensureMinimalDocs(projectPath);
272
+ p.log.info('Tip: On Windows/PowerShell, use a template file to avoid paste issues:');
273
+ p.log.message(' pmpt plan --template # creates answers.json');
274
+ p.log.message(' pmpt plan --answers-file answers.json');
239
275
  p.outro('Ready! Run `pmpt plan` when you want to start.');
240
276
  }
241
277
  }
242
278
  else {
243
279
  // New/empty project — original flow
244
- const startPlan = await p.confirm({
245
- message: 'Start planning? (Generate AI prompt with 5 quick questions)',
246
- initialValue: true,
280
+ const startPlan = await p.select({
281
+ message: 'Start planning?',
282
+ options: [
283
+ { value: 'yes', label: 'Yes — answer 5 questions now', hint: 'Interactive flow' },
284
+ { value: 'file', label: 'Fill in answers file', hint: 'Windows/PowerShell friendly — edit a JSON file then run pmpt plan --answers-file' },
285
+ { value: 'no', label: 'Skip for now' },
286
+ ],
247
287
  });
248
- if (!p.isCancel(startPlan) && startPlan) {
249
- p.log.message('');
250
- await cmdPlan(projectPath);
251
- }
252
- else {
288
+ if (p.isCancel(startPlan) || startPlan === 'no') {
253
289
  ensureMinimalDocs(projectPath);
254
290
  p.outro('Ready! Run `pmpt plan` when you want to start.');
255
291
  }
292
+ else if (startPlan === 'file') {
293
+ const outFile = 'answers.json';
294
+ const template = generateAnswersTemplate();
295
+ writeFileSync(resolve(projectPath, outFile), JSON.stringify(template, null, 2), 'utf-8');
296
+ ensureMinimalDocs(projectPath);
297
+ p.log.success(`Template created: ${outFile}`);
298
+ p.log.info('Next steps:');
299
+ p.log.message(` 1. Open and fill in ${outFile}`);
300
+ p.log.message(` 2. pmpt plan --answers-file ${outFile}`);
301
+ p.outro('Ready!');
302
+ }
303
+ else {
304
+ p.log.message('');
305
+ await cmdPlan(projectPath);
306
+ }
256
307
  }
257
308
  }
258
309
  catch (error) {
@@ -271,16 +322,25 @@ function ensureMinimalDocs(projectPath) {
271
322
  const skeleton = [
272
323
  `# ${name}`,
273
324
  '',
325
+ '## Architecture',
326
+ '<!-- High-level structure. Update as it evolves. -->',
327
+ '',
328
+ '## Active Work',
329
+ '<!-- What\'s currently being built. Clear when done, move to Snapshot Log. -->',
330
+ '',
274
331
  '## Progress',
275
332
  '- Project initialized',
276
333
  '',
277
334
  '## Snapshot Log',
278
335
  '',
279
336
  '## Decisions',
337
+ '<!-- WHY, not just WHAT. Format: - [Decision] → [Reason] -->',
280
338
  '',
281
339
  '## Constraints',
340
+ '<!-- Platform/library limitations. Format: - [Tool]: limitation → workaround -->',
282
341
  '',
283
342
  '## Lessons',
343
+ '<!-- Anti-patterns. Format: - [What failed] → [Root cause] → [Fix applied] -->',
284
344
  '',
285
345
  ].join('\n');
286
346
  writeFileSync(pmptMdPath, skeleton, 'utf-8');
@@ -1,9 +1,12 @@
1
1
  import * as p from '@clack/prompts';
2
2
  import { execSync } from 'child_process';
3
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
3
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'fs';
4
4
  import { join, dirname } from 'path';
5
5
  import { homedir } from 'os';
6
- function detectPmptMcpPath() {
6
+ /** Stable wrapper path that survives nvm version switches */
7
+ const WRAPPER_DIR = join(homedir(), '.pmpt', 'bin');
8
+ const WRAPPER_PATH = join(WRAPPER_DIR, 'pmpt-mcp');
9
+ export function detectPmptMcpPath() {
7
10
  // Strategy 1: sibling to the current pmpt binary (same bin directory)
8
11
  const pmptBin = process.argv[1];
9
12
  const siblingPath = join(dirname(pmptBin), 'pmpt-mcp');
@@ -24,6 +27,51 @@ function detectPmptMcpPath() {
24
27
  }
25
28
  return null;
26
29
  }
30
+ /** Detect if nvm is in use */
31
+ function isNvmEnvironment() {
32
+ return !!process.env.NVM_DIR || process.argv[1]?.includes('.nvm/');
33
+ }
34
+ /**
35
+ * Create a wrapper script at ~/.pmpt/bin/pmpt-mcp that:
36
+ * 1. Loads nvm if available
37
+ * 2. Executes the real pmpt-mcp binary
38
+ * This makes the MCP path stable across Node version changes.
39
+ */
40
+ function createWrapperScript(realBinaryPath) {
41
+ mkdirSync(WRAPPER_DIR, { recursive: true });
42
+ const nvmDir = process.env.NVM_DIR || join(homedir(), '.nvm');
43
+ const script = `#!/bin/bash
44
+ # pmpt-mcp wrapper — loads nvm then runs the real binary
45
+ # Auto-generated by pmpt mcp-setup. Re-run "pmpt mcp-setup" to update.
46
+
47
+ export NVM_DIR="${nvmDir}"
48
+ [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
49
+
50
+ exec "${realBinaryPath}" "$@"
51
+ `;
52
+ writeFileSync(WRAPPER_PATH, script, { mode: 0o755 });
53
+ chmodSync(WRAPPER_PATH, 0o755);
54
+ return WRAPPER_PATH;
55
+ }
56
+ /** Check if wrapper exists and the target binary it points to is still valid */
57
+ export function getWrapperStatus() {
58
+ if (!existsSync(WRAPPER_PATH)) {
59
+ return { exists: false, targetValid: false, targetPath: null };
60
+ }
61
+ try {
62
+ const content = readFileSync(WRAPPER_PATH, 'utf-8');
63
+ const match = content.match(/exec "(.+?)"/);
64
+ const targetPath = match?.[1] || null;
65
+ return {
66
+ exists: true,
67
+ targetValid: targetPath ? existsSync(targetPath) : false,
68
+ targetPath,
69
+ };
70
+ }
71
+ catch {
72
+ return { exists: true, targetValid: false, targetPath: null };
73
+ }
74
+ }
27
75
  function isCommandAvailable(cmd) {
28
76
  try {
29
77
  const which = process.platform === 'win32' ? 'where' : 'which';
@@ -55,14 +103,15 @@ function isJsonConfigured(configPath) {
55
103
  }
56
104
  }
57
105
  function configureClaudeCode(mcpBinaryPath) {
58
- // Remove existing entry first (ignore errors if not found)
59
- try {
60
- execSync('claude mcp remove pmpt', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
61
- }
62
- catch {
63
- // not configured yet that's fine
106
+ // Remove existing entry at any scope first (ignore errors if not found)
107
+ for (const scope of ['user', 'local', 'project']) {
108
+ try {
109
+ execSync(`claude mcp remove --scope ${scope} pmpt`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
110
+ }
111
+ catch { /* not configured at this scope */ }
64
112
  }
65
- execSync(`claude mcp add --transport stdio pmpt -- "${mcpBinaryPath}"`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
113
+ // Register at user scope so it works across all projects
114
+ execSync(`claude mcp add --scope user --transport stdio pmpt -- "${mcpBinaryPath}"`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
66
115
  }
67
116
  function configureJsonFile(configPath, mcpBinaryPath) {
68
117
  let config = {};
@@ -93,6 +142,21 @@ export async function cmdMcpSetup() {
93
142
  let mcpPath = detectPmptMcpPath();
94
143
  if (mcpPath) {
95
144
  s.stop(`Found: ${mcpPath}`);
145
+ // If nvm environment, offer to create a stable wrapper
146
+ if (isNvmEnvironment()) {
147
+ p.log.warn('nvm detected — the binary path may break when you switch Node versions.');
148
+ const useWrapper = await p.confirm({
149
+ message: 'Create a stable wrapper at ~/.pmpt/bin/pmpt-mcp? (recommended for nvm users)',
150
+ initialValue: true,
151
+ });
152
+ if (!p.isCancel(useWrapper) && useWrapper) {
153
+ const wrapperPath = createWrapperScript(mcpPath);
154
+ p.log.success(`Wrapper created: ${wrapperPath}`);
155
+ p.log.info(`Points to: ${mcpPath}`);
156
+ p.log.info('After switching Node versions, run "pmpt mcp-setup" to update the wrapper.');
157
+ mcpPath = wrapperPath;
158
+ }
159
+ }
96
160
  }
97
161
  else {
98
162
  s.stop('Could not auto-detect pmpt-mcp path');
@@ -1,10 +1,10 @@
1
1
  import * as p from '@clack/prompts';
2
2
  import { resolve } from 'path';
3
- import { existsSync, readFileSync } from 'fs';
3
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
4
4
  import { isInitialized, loadConfig } from '../lib/config.js';
5
5
  import { copyToClipboard } from '../lib/clipboard.js';
6
6
  import { cmdWatch } from './watch.js';
7
- import { PLAN_QUESTIONS, getPlanProgress, initPlanProgress, savePlanProgress, savePlanDocuments, } from '../lib/plan.js';
7
+ import { PLAN_QUESTIONS, getPlanProgress, initPlanProgress, savePlanProgress, savePlanDocuments, generateAnswersTemplate, } from '../lib/plan.js';
8
8
  function loadAnswersFromFile(projectPath, inputPath) {
9
9
  const filePath = resolve(projectPath, inputPath);
10
10
  if (!existsSync(filePath)) {
@@ -27,6 +27,16 @@ function loadAnswersFromFile(projectPath, inputPath) {
27
27
  }
28
28
  export async function cmdPlan(path, options) {
29
29
  const projectPath = path ? resolve(path) : process.cwd();
30
+ // Template generation (no init check needed)
31
+ if (options?.template) {
32
+ const outFile = resolve(projectPath, options.template);
33
+ const template = generateAnswersTemplate();
34
+ writeFileSync(outFile, JSON.stringify(template, null, 2), 'utf-8');
35
+ p.log.success(`Template created: ${outFile}`);
36
+ p.log.info('Fill in the fields and run:');
37
+ p.log.message(` pmpt plan --answers-file ${options.template}`);
38
+ return;
39
+ }
30
40
  // Check initialization
31
41
  if (!isInitialized(projectPath)) {
32
42
  p.intro('pmpt plan');
package/dist/index.js CHANGED
@@ -45,6 +45,8 @@ import { cmdRecover } from './commands/recover.js';
45
45
  import { cmdDiff } from './commands/diff.js';
46
46
  import { cmdInternalSeed } from './commands/internal-seed.js';
47
47
  import { cmdMcpSetup } from './commands/mcp-setup.js';
48
+ import { cmdDoctor } from './commands/doctor.js';
49
+ import { cmdConstraint } from './commands/constraint.js';
48
50
  import { trackCommand } from './lib/api.js';
49
51
  import { checkForUpdates } from './lib/update-check.js';
50
52
  import { createRequire } from 'module';
@@ -85,6 +87,7 @@ Examples:
85
87
  $ pmpt graduate Graduate a project (Hall of Fame)
86
88
  $ pmpt recover Recover damaged pmpt.md via AI
87
89
  $ pmpt mcp-setup Configure MCP for AI tools
90
+ $ pmpt doctor (doc) Diagnose MCP & environment issues
88
91
  $ pmpt feedback (fb) Share ideas or report bugs
89
92
 
90
93
  Workflow:
@@ -146,7 +149,28 @@ program
146
149
  .description('Quick product planning with 5 questions — auto-generate AI prompt')
147
150
  .option('--reset', 'Restart plan from scratch')
148
151
  .option('--answers-file <file>', 'Load plan answers from JSON file (non-interactive)')
149
- .action(cmdPlan);
152
+ .option('--template [file]', 'Generate a fillable answers template (default: answers.json)')
153
+ .action((path, options) => cmdPlan(path, {
154
+ ...options,
155
+ template: options.template === true ? 'answers.json' : options.template,
156
+ }));
157
+ program
158
+ .command('constraint <action>')
159
+ .description('Manage architecture constraints — add, list, or remove rules')
160
+ .option('-r, --rule <rule>', 'Rule text (for add)')
161
+ .option('-i, --index <n>', 'Rule index (for remove)')
162
+ .addHelpText('after', `
163
+ Actions:
164
+ add Add a constraint rule
165
+ list List all constraints
166
+ remove Remove a constraint by index
167
+
168
+ Examples:
169
+ pmpt constraint add "UI does not import Service directly"
170
+ pmpt constraint list
171
+ pmpt constraint remove 2
172
+ `)
173
+ .action((action, options) => cmdConstraint(action, options));
150
174
  program
151
175
  .command('logout')
152
176
  .description('Clear saved GitHub authentication')
@@ -214,6 +238,11 @@ program
214
238
  .command('mcp-setup')
215
239
  .description('Configure pmpt MCP server for AI tools (Claude Code, Cursor, etc.)')
216
240
  .action(cmdMcpSetup);
241
+ program
242
+ .command('doctor')
243
+ .alias('doc')
244
+ .description('Diagnose MCP connection, PATH issues, and environment health')
245
+ .action(cmdDoctor);
217
246
  program
218
247
  .command('feedback')
219
248
  .alias('fb')
@@ -0,0 +1,179 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { getConfigDir, getDocsDir } from './config.js';
4
+ // ─── index.md — lightweight map pointing to pmpt.md ───────────────
5
+ export function generateIndexMd(projectName) {
6
+ return `# ${projectName} — Project Context
7
+
8
+ > AI entry point. Read this first, then follow the links below.
9
+
10
+ ## Map
11
+
12
+ | File | Purpose |
13
+ |------|---------|
14
+ | [.pmpt/docs/pmpt.md](.pmpt/docs/pmpt.md) | **Source of truth** — architecture, constraints, decisions, progress |
15
+ | [.pmpt/docs/pmpt.ai.md](.pmpt/docs/pmpt.ai.md) | Full AI prompt — paste into your AI tool to get started |
16
+
17
+ ## Rules for AI
18
+
19
+ 1. Read \`.pmpt/docs/pmpt.md\` before making any code changes
20
+ 2. Follow every rule listed in the **## Constraints** section of pmpt.md
21
+ 3. After milestones, call \`mcp__pmpt__pmpt_save\` (or \`pmpt save\` in terminal)
22
+ 4. Update pmpt.md when architecture, constraints, or features change
23
+ `;
24
+ }
25
+ // ─── Tool-specific entry points ────────────────────────────────────
26
+ export function generateCursorRules() {
27
+ return `# Project Rules
28
+
29
+ See \`.pmpt/index.md\` for the project context map.
30
+
31
+ The single source of truth is \`.pmpt/docs/pmpt.md\`.
32
+ Always check the **## Constraints** section before making code changes.
33
+ After completing milestones, run \`pmpt save\` in terminal.
34
+ `;
35
+ }
36
+ export function generateAgentsMd(projectName) {
37
+ return `# ${projectName} — Agent Instructions
38
+
39
+ See \`.pmpt/index.md\` for the project context map.
40
+
41
+ The single source of truth is \`.pmpt/docs/pmpt.md\`.
42
+ Always check the **## Constraints** section before making code changes.
43
+ After completing milestones, run \`pmpt save\` in terminal.
44
+ `;
45
+ }
46
+ // ─── Setup functions ───────────────────────────────────────────────
47
+ export function ensureIndexMd(projectPath, projectName) {
48
+ const configDir = getConfigDir(projectPath);
49
+ mkdirSync(configDir, { recursive: true });
50
+ const indexPath = join(configDir, 'index.md');
51
+ if (!existsSync(indexPath)) {
52
+ writeFileSync(indexPath, generateIndexMd(projectName), 'utf-8');
53
+ }
54
+ }
55
+ export function ensureCursorRules(projectPath) {
56
+ const path = join(projectPath, '.cursorrules');
57
+ if (!existsSync(path)) {
58
+ writeFileSync(path, generateCursorRules(), 'utf-8');
59
+ }
60
+ }
61
+ export function ensureAgentsMd(projectPath, projectName) {
62
+ const path = join(projectPath, 'AGENTS.md');
63
+ if (!existsSync(path)) {
64
+ writeFileSync(path, generateAgentsMd(projectName), 'utf-8');
65
+ }
66
+ }
67
+ export function setupHarnessForTools(projectPath, projectName, tools) {
68
+ // index.md is always created — it's the lightweight map
69
+ ensureIndexMd(projectPath, projectName);
70
+ if (tools === 'cursor' || tools === 'all') {
71
+ ensureCursorRules(projectPath);
72
+ }
73
+ if (tools === 'codex' || tools === 'all') {
74
+ ensureAgentsMd(projectPath, projectName);
75
+ }
76
+ if (tools === 'claude' || tools === 'all') {
77
+ ensureClaudeMdIndexRef(projectPath);
78
+ }
79
+ }
80
+ function ensureClaudeMdIndexRef(projectPath) {
81
+ const claudeMdPath = join(projectPath, 'CLAUDE.md');
82
+ const marker = '<!-- pmpt-index -->';
83
+ const section = `${marker}\nSee \`.pmpt/index.md\` for project context. Single source of truth: \`.pmpt/docs/pmpt.md\`.\n<!-- /pmpt-index -->`;
84
+ if (!existsSync(claudeMdPath))
85
+ return;
86
+ const content = readFileSync(claudeMdPath, 'utf-8');
87
+ if (content.includes(marker))
88
+ return;
89
+ const pmptMarker = '<!-- pmpt -->';
90
+ if (content.includes(pmptMarker)) {
91
+ writeFileSync(claudeMdPath, content.replace(pmptMarker, section + '\n\n' + pmptMarker), 'utf-8');
92
+ }
93
+ else {
94
+ writeFileSync(claudeMdPath, section + '\n\n' + content, 'utf-8');
95
+ }
96
+ }
97
+ // ─── Constraint management — reads/writes pmpt.md ## Constraints ──
98
+ export function readConstraints(projectPath) {
99
+ const pmptMdPath = join(getDocsDir(projectPath), 'pmpt.md');
100
+ if (!existsSync(pmptMdPath))
101
+ return [];
102
+ const lines = readFileSync(pmptMdPath, 'utf-8').split('\n');
103
+ const inSection = { value: false };
104
+ const rules = [];
105
+ for (const line of lines) {
106
+ if (line.trim() === '## Constraints') {
107
+ inSection.value = true;
108
+ continue;
109
+ }
110
+ if (inSection.value && line.startsWith('## '))
111
+ break;
112
+ if (inSection.value && line.trimStart().startsWith('- ') && !line.includes('<!--')) {
113
+ rules.push(line.trimStart().slice(2).trim());
114
+ }
115
+ }
116
+ return rules;
117
+ }
118
+ export function addConstraint(projectPath, rule) {
119
+ const pmptMdPath = join(getDocsDir(projectPath), 'pmpt.md');
120
+ if (!existsSync(pmptMdPath))
121
+ return false;
122
+ const lines = readFileSync(pmptMdPath, 'utf-8').split('\n');
123
+ let sectionEnd = -1;
124
+ let inSection = false;
125
+ for (let i = 0; i < lines.length; i++) {
126
+ if (lines[i].trim() === '## Constraints') {
127
+ inSection = true;
128
+ continue;
129
+ }
130
+ if (inSection && lines[i].startsWith('## ')) {
131
+ sectionEnd = i;
132
+ break;
133
+ }
134
+ }
135
+ const newLine = `- ${rule}`;
136
+ if (sectionEnd !== -1) {
137
+ // Insert before next section, after any existing content
138
+ lines.splice(sectionEnd, 0, newLine);
139
+ }
140
+ else if (inSection) {
141
+ lines.push(newLine);
142
+ }
143
+ else {
144
+ return false;
145
+ }
146
+ writeFileSync(pmptMdPath, lines.join('\n'), 'utf-8');
147
+ return true;
148
+ }
149
+ export function removeConstraint(projectPath, index) {
150
+ const pmptMdPath = join(getDocsDir(projectPath), 'pmpt.md');
151
+ if (!existsSync(pmptMdPath))
152
+ return null;
153
+ const lines = readFileSync(pmptMdPath, 'utf-8').split('\n');
154
+ let inSection = false;
155
+ let ruleCount = 0;
156
+ let removedRule = null;
157
+ const newLines = lines.filter((line) => {
158
+ if (line.trim() === '## Constraints') {
159
+ inSection = true;
160
+ return true;
161
+ }
162
+ if (inSection && line.startsWith('## ')) {
163
+ inSection = false;
164
+ return true;
165
+ }
166
+ if (inSection && line.trimStart().startsWith('- ') && !line.includes('<!--')) {
167
+ ruleCount++;
168
+ if (ruleCount === index) {
169
+ removedRule = line.trimStart().slice(2).trim();
170
+ return false;
171
+ }
172
+ }
173
+ return true;
174
+ });
175
+ if (removedRule === null)
176
+ return null;
177
+ writeFileSync(pmptMdPath, newLines.join('\n'), 'utf-8');
178
+ return removedRule;
179
+ }
package/dist/lib/plan.js CHANGED
@@ -118,17 +118,27 @@ After completing each feature above:
118
118
 
119
119
  ### What to Record in pmpt.md
120
120
 
121
- **## Decisions** Record WHY, not just WHAT. Include the data or observation that led to the decision.
122
- - Bad: "Set minimum description length to 150 chars"
123
- - Good: "Set minimum description length to 150 chars (50-char threshold caused 60% low-quality entries)"
121
+ pmpt.md is the **single source of truth** for this project. AI tools read it to understand context before every session. Keep it accurate.
124
122
 
125
- **## Constraints** — Record platform/library limitations discovered during development.
126
- - Examples: DB quirks, API limits, framework restrictions, version incompatibilities
127
- - Format: \`- [Platform]: what doesn't work workaround used\`
123
+ **## Architecture** — High-level structure. Update when the architecture changes.
124
+ - Example: \`Next.js (SSG) Cloudflare Workers API D1 database\`
125
+ - Include the WHY if the stack choice was non-obvious
128
126
 
129
- **## Lessons** — Record anti-patterns and "we tried X, it broke because Y" discoveries.
130
- - Examples: wrong deletion order causing FK errors, caching issues, race conditions
127
+ **## Active Work** — What's currently being built. One or two items max.
128
+ - Clear this section when done, then move to Snapshot Log
129
+ - Example: \`- Implementing user auth (started 2025-03-17)\`
130
+
131
+ **## Decisions** — Record WHY, not just WHAT. Include what led to the decision.
132
+ - Bad: "Switched to SQLite"
133
+ - Good: "Switched SQLite → Postgres: deploy target moved to serverless, needed connection pooling"
134
+
135
+ **## Constraints** — Platform or library limitations discovered during development.
136
+ - Format: \`- [Platform/Tool]: what doesn't work → workaround used\`
137
+ - Example: \`- Cloudflare Workers: no native fs access → use KV for file storage\`
138
+
139
+ **## Lessons** — Anti-patterns and "tried X, broke because Y" discoveries.
131
140
  - Format: \`- [What failed] → [Root cause] → [Fix applied]\`
141
+ - Example: \`- JWT refresh on mobile broke → tokens expired before retry → added sliding expiry\`
132
142
  `;
133
143
  }
134
144
  // Generate human-facing project document (pmpt.md)
@@ -153,24 +163,38 @@ ${contextSection}
153
163
  ## Features
154
164
  ${features}
155
165
  ${techSection}
166
+ ## Architecture
167
+ <!-- High-level structure. Update as it evolves. -->
168
+ <!-- Example: "Next.js frontend → Express API → PostgreSQL" -->
169
+
170
+ ## Active Work
171
+ <!-- What's currently being built. Clear when done, move to Snapshot Log. -->
172
+
156
173
  ## Progress
157
174
  - [ ] Project setup
158
175
  - [ ] Core features implementation
159
176
  - [ ] Testing & polish
160
177
 
161
178
  ## Snapshot Log
162
- ### v1 - Initial Setup
179
+ ### v1 Initial Setup
163
180
  - Project initialized with pmpt
164
181
 
165
182
  ## Decisions
183
+ <!-- WHY, not just WHAT. Include what led to the decision. -->
184
+ <!-- Format: - [Decision] → [Reason / data that led to it] -->
166
185
 
167
186
  ## Constraints
187
+ <!-- Platform or library limitations discovered during development. -->
188
+ <!-- Format: - [Platform/Tool]: what doesn't work → workaround used -->
168
189
 
169
190
  ## Lessons
191
+ <!-- Anti-patterns and "tried X, broke because Y" discoveries. -->
192
+ <!-- Format: - [What failed] → [Root cause] → [Fix applied] -->
170
193
 
171
194
  ---
172
- *This document tracks your project progress. Update it as you build.*
173
- *AI instructions are in \`pmpt.ai.md\` paste that into your AI tool.*
195
+ *This document is the single source of truth for this project.*
196
+ *AI tools read this to understand context, constraints, and current state.*
197
+ *AI instructions are in \`pmpt.ai.md\` — paste that into your AI tool to get started.*
174
198
  `;
175
199
  }
176
200
  // Generate plan document
@@ -200,6 +224,19 @@ ${techSection}
200
224
  *Generated by pmpt plan*
201
225
  `;
202
226
  }
227
+ export function generateAnswersTemplate() {
228
+ const questions = {};
229
+ const fields = {};
230
+ for (const q of PLAN_QUESTIONS) {
231
+ questions[q.key] = `[${PLAN_QUESTIONS.indexOf(q) + 1}/${PLAN_QUESTIONS.length}] ${q.question}`;
232
+ fields[q.key] = '';
233
+ }
234
+ return {
235
+ _help: 'Fill in the fields below, then run: pmpt plan --answers-file <this-file>',
236
+ _questions: questions,
237
+ ...fields,
238
+ };
239
+ }
203
240
  export function getPlanProgress(projectPath) {
204
241
  const planPath = join(getConfigDir(projectPath), PLAN_FILE);
205
242
  if (!existsSync(planPath))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmpt-cli",
3
- "version": "1.14.21",
3
+ "version": "1.17.0",
4
4
  "description": "Record and share your AI-driven product development journey",
5
5
  "type": "module",
6
6
  "bin": {