pmpt-cli 1.11.0 → 1.12.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.
@@ -1,73 +1,11 @@
1
1
  import * as p from '@clack/prompts';
2
- import { fetchProjects } from '../lib/api.js';
3
- export async function cmdBrowse() {
4
- p.intro('pmpt browse');
5
- const s = p.spinner();
6
- s.start('Loading projects...');
7
- let projects;
8
- try {
9
- const index = await fetchProjects();
10
- projects = index.projects;
11
- }
12
- catch (err) {
13
- s.stop('Failed to load');
14
- p.log.error(err instanceof Error ? err.message : 'Could not load project list.');
15
- process.exit(1);
16
- }
17
- s.stop(`${projects.length} projects`);
18
- if (projects.length === 0) {
19
- p.log.info('No published projects yet.');
20
- p.log.message(' pmpt publish — share your first project!');
21
- p.outro('');
22
- return;
23
- }
24
- // Select project
25
- const selected = await p.select({
26
- message: 'Select a project:',
27
- options: projects.map((proj) => ({
28
- value: proj.slug,
29
- label: proj.projectName,
30
- hint: `v${proj.versionCount} · @${proj.author}${proj.description ? ` — ${proj.description.slice(0, 40)}` : ''}`,
31
- })),
32
- });
33
- if (p.isCancel(selected)) {
34
- p.cancel('');
35
- process.exit(0);
36
- }
37
- const project = projects.find((p) => p.slug === selected);
38
- // Show details
39
- p.note([
40
- `Project: ${project.projectName}`,
41
- `Author: @${project.author}`,
42
- `Versions: ${project.versionCount}`,
43
- project.description ? `Description: ${project.description}` : '',
44
- project.tags.length ? `Tags: ${project.tags.join(', ')}` : '',
45
- `Published: ${project.publishedAt.slice(0, 10)}`,
46
- `Size: ${(project.fileSize / 1024).toFixed(1)} KB`,
47
- ].filter(Boolean).join('\n'), 'Project Details');
48
- // Action
49
- const action = await p.select({
50
- message: 'What would you like to do?',
51
- options: [
52
- { value: 'clone', label: 'Clone this project', hint: 'pmpt clone' },
53
- { value: 'url', label: 'Show URL', hint: 'View in browser' },
54
- { value: 'back', label: 'Go back' },
55
- ],
56
- });
57
- if (p.isCancel(action) || action === 'back') {
58
- p.outro('');
59
- return;
60
- }
61
- if (action === 'clone') {
62
- const { cmdClone } = await import('./clone.js');
63
- await cmdClone(project.slug);
64
- return;
65
- }
66
- if (action === 'url') {
67
- const url = `https://pmptwiki.com/p/${project.slug}`;
68
- p.log.info(`URL: ${url}`);
69
- p.log.message(`Download: ${project.downloadUrl}`);
70
- p.log.message(`\npmpt clone ${project.slug} — clone via terminal`);
71
- p.outro('');
72
- }
2
+ import open from 'open';
3
+ const EXPLORE_URL = 'https://pmptwiki.com/explore';
4
+ export async function cmdExplore() {
5
+ p.intro('pmpt explore');
6
+ p.log.info(`Opening ${EXPLORE_URL}`);
7
+ await open(EXPLORE_URL);
8
+ p.log.message(' Search, filter, and clone projects from the web.');
9
+ p.log.message(' Found something you like? → pmpt clone <slug>');
10
+ p.outro('');
73
11
  }
@@ -4,6 +4,7 @@ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync } from
4
4
  import { isInitialized, getConfigDir, getHistoryDir, getDocsDir, initializeProject } from '../lib/config.js';
5
5
  import { validatePmptFile, isSafeFilename } from '../lib/pmptFile.js';
6
6
  import { fetchPmptFile, trackClone } from '../lib/api.js';
7
+ import { copyToClipboard } from '../lib/clipboard.js';
7
8
  /**
8
9
  * Restore history from .pmpt data (shared with import command)
9
10
  */
@@ -81,12 +82,15 @@ export async function cmdClone(slug) {
81
82
  const pmptData = validation.data;
82
83
  s.stop('Download complete');
83
84
  // Show summary
84
- p.note([
85
+ const infoLines = [
85
86
  `Project: ${pmptData.meta.projectName}`,
86
87
  `Versions: ${pmptData.history.length}`,
87
88
  pmptData.meta.author ? `Author: @${pmptData.meta.author}` : '',
88
- pmptData.meta.description ? `Description: ${pmptData.meta.description.slice(0, 80)}` : '',
89
- ].filter(Boolean).join('\n'), 'Project Info');
89
+ pmptData.meta.description ? `\n${pmptData.meta.description}` : '',
90
+ pmptData.plan?.productIdea ? `\n💡 ${pmptData.plan.productIdea.slice(0, 120)}` : '',
91
+ pmptData.plan?.techStack ? `🛠 ${pmptData.plan.techStack.slice(0, 80)}` : '',
92
+ ];
93
+ p.note(infoLines.filter(Boolean).join('\n'), 'Project Info');
90
94
  const projectPath = process.cwd();
91
95
  if (isInitialized(projectPath)) {
92
96
  const overwrite = await p.confirm({
@@ -147,6 +151,19 @@ export async function cmdClone(slug) {
147
151
  '',
148
152
  '---',
149
153
  '',
154
+ `## Documentation Rule`,
155
+ '',
156
+ `**Important:** When you make progress, update \`.pmpt/docs/pmpt.md\` (the human-facing project document) at these moments:`,
157
+ `- When architecture or tech decisions are finalized`,
158
+ `- When a feature is implemented (mark as done)`,
159
+ `- When a development phase is completed`,
160
+ `- When requirements change or new decisions are made`,
161
+ '',
162
+ `Keep the Progress and Snapshot Log sections in pmpt.md up to date.`,
163
+ `After significant milestones, run \`pmpt save\` to create a snapshot.`,
164
+ '',
165
+ '---',
166
+ '',
150
167
  originalAiMd,
151
168
  ].join('\n');
152
169
  writeFileSync(aiMdPath, cloneGuide, 'utf-8');
@@ -170,9 +187,38 @@ export async function cmdClone(slug) {
170
187
  `Versions: ${versionCount}`,
171
188
  `Location: ${pmptDir}`,
172
189
  ].join('\n'), 'Clone Summary');
173
- p.log.info('Next steps:');
190
+ // Copy AI prompt to clipboard
191
+ const aiContent = readFileSync(aiMdPath, 'utf-8');
192
+ const copied = copyToClipboard(aiContent);
193
+ if (copied) {
194
+ p.log.message('');
195
+ p.log.success('AI prompt copied to clipboard!');
196
+ p.log.message('');
197
+ const banner = [
198
+ '',
199
+ '┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓',
200
+ '┃ ┃',
201
+ '┃ 📋 NEXT STEP ┃',
202
+ '┃ ┃',
203
+ '┃ Open your AI coding tool and press: ┃',
204
+ '┃ ┃',
205
+ '┃ ⌘ + V (Mac) ┃',
206
+ '┃ Ctrl + V (Windows/Linux) ┃',
207
+ '┃ ┃',
208
+ '┃ Your cloned project context is ready! 🚀 ┃',
209
+ '┃ ┃',
210
+ '┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛',
211
+ '',
212
+ ];
213
+ console.log(banner.join('\n'));
214
+ }
215
+ else {
216
+ p.log.warn('Could not copy to clipboard.');
217
+ p.log.info(`Read it at: ${aiMdPath}`);
218
+ }
219
+ p.log.info('Tips:');
174
220
  p.log.message(' pmpt history — view version history');
175
- p.log.message(' pmpt plan — view AI prompt');
221
+ p.log.message(' pmpt plan — view or edit AI prompt');
176
222
  p.log.message(' pmpt save — save a new snapshot');
177
223
  p.outro('Project cloned!');
178
224
  }
@@ -79,6 +79,47 @@ export async function cmdEdit() {
79
79
  p.cancel('Cancelled');
80
80
  process.exit(0);
81
81
  }
82
+ // Product link (optional)
83
+ const linkTypeInput = await p.select({
84
+ message: 'Product link (optional):',
85
+ initialValue: project.productUrlType || 'none',
86
+ options: [
87
+ { value: 'none', label: 'No link' },
88
+ { value: 'git', label: 'Git Repository' },
89
+ { value: 'url', label: 'Website / URL' },
90
+ ],
91
+ });
92
+ if (p.isCancel(linkTypeInput)) {
93
+ p.cancel('Cancelled');
94
+ process.exit(0);
95
+ }
96
+ let productUrl = '';
97
+ let productUrlType = '';
98
+ if (linkTypeInput !== 'none') {
99
+ productUrlType = linkTypeInput;
100
+ const productUrlInput = await p.text({
101
+ message: 'Product URL:',
102
+ placeholder: linkTypeInput === 'git'
103
+ ? `https://github.com/${auth.username}/${slug}`
104
+ : 'https://...',
105
+ defaultValue: project.productUrl || '',
106
+ validate: (v) => {
107
+ if (!v.trim())
108
+ return 'URL is required when link type is selected.';
109
+ try {
110
+ new URL(v);
111
+ }
112
+ catch {
113
+ return 'Invalid URL format.';
114
+ }
115
+ },
116
+ });
117
+ if (p.isCancel(productUrlInput)) {
118
+ p.cancel('Cancelled');
119
+ process.exit(0);
120
+ }
121
+ productUrl = productUrlInput;
122
+ }
82
123
  const s2 = p.spinner();
83
124
  s2.start('Updating...');
84
125
  try {
@@ -86,6 +127,8 @@ export async function cmdEdit() {
86
127
  description: description,
87
128
  tags,
88
129
  category: category,
130
+ productUrl,
131
+ productUrlType,
89
132
  });
90
133
  s2.stop('Updated!');
91
134
  p.log.success(`Project "${slug}" has been updated.`);
@@ -75,8 +75,12 @@ export function cmdHistory(path, options) {
75
75
  if (snapshot.git.dirty)
76
76
  header += ' (dirty)';
77
77
  }
78
- const files = snapshot.files.map((f) => ` - ${f}`).join('\n');
79
- p.note(files || ' (no files)', header);
78
+ const lines = [];
79
+ if (snapshot.note) {
80
+ lines.push(` ${snapshot.note}`, '');
81
+ }
82
+ lines.push(...snapshot.files.map((f) => ` - ${f}`));
83
+ p.note(lines.join('\n') || ' (no files)', header);
80
84
  }
81
85
  if (options?.compact && hiddenVersions.length > 0) {
82
86
  p.log.info(`Hidden versions (minor changes): ${hiddenVersions.map(v => `v${v}`).join(', ')}`);
@@ -84,13 +84,12 @@ export async function cmdInternalSeed(options) {
84
84
  const content = readFileSync(resolve(specDir, fromPath), 'utf-8');
85
85
  writeDocFile(docsDir, fileName, content);
86
86
  }
87
- const entry = createFullSnapshot(projectPath);
88
- const note = step.saveNote ? ` — ${step.saveNote}` : '';
89
- p.log.success(`v${entry.version} saved${note}`);
87
+ const entry = createFullSnapshot(projectPath, { note: step.saveNote });
88
+ const noteStr = entry.note ? ` — ${entry.note}` : '';
89
+ p.log.success(`v${entry.version} saved${noteStr}`);
90
90
  }
91
91
  if (spec.publish?.enabled) {
92
92
  await cmdPublish(projectPath, {
93
- force: spec.publish.force ?? false,
94
93
  nonInteractive: true,
95
94
  yes: spec.publish.yes ?? true,
96
95
  metaFile: spec.publish.metaFile ? resolve(specDir, spec.publish.metaFile) : undefined,
@@ -8,6 +8,7 @@ import { createPmptFile } from '../lib/pmptFile.js';
8
8
  import { loadAuth } from '../lib/auth.js';
9
9
  import { publishProject, fetchProjects } from '../lib/api.js';
10
10
  import { computeQuality } from '../lib/quality.js';
11
+ import { copyToClipboard } from '../lib/clipboard.js';
11
12
  import pc from 'picocolors';
12
13
  import glob from 'fast-glob';
13
14
  import { join } from 'path';
@@ -22,6 +23,29 @@ const CATEGORY_OPTIONS = [
22
23
  { value: 'other', label: 'Other' },
23
24
  ];
24
25
  const VALID_CATEGORIES = new Set(CATEGORY_OPTIONS.map((o) => o.value));
26
+ function generateImprovementPrompt(quality) {
27
+ const missing = [];
28
+ for (const item of quality.details) {
29
+ if (item.score < item.maxScore && item.tip) {
30
+ missing.push(`- ${item.label}: ${item.tip}`);
31
+ }
32
+ }
33
+ return [
34
+ `My pmpt project scored ${quality.score}/100 (Grade ${quality.grade}) and needs at least 40 to publish.`,
35
+ '',
36
+ 'Areas to improve:',
37
+ ...missing,
38
+ '',
39
+ 'Please help me improve the project quality:',
40
+ '',
41
+ '1. Read `.pmpt/docs/pmpt.ai.md` and `.pmpt/docs/pmpt.md`',
42
+ '2. Expand pmpt.ai.md to 500+ characters with clear project context, architecture, and instructions for AI',
43
+ '3. Make sure pmpt.md has progress tracking, decisions, and a snapshot log',
44
+ '4. If plan.md is missing, create it with product overview',
45
+ '5. After improving, run `pmpt save` to create a new snapshot',
46
+ '6. Then try `pmpt publish` again',
47
+ ].join('\n');
48
+ }
25
49
  function normalizeTags(value) {
26
50
  if (Array.isArray(value)) {
27
51
  return value
@@ -124,11 +148,22 @@ export async function cmdPublish(path, options) {
124
148
  if (tips.length > 0) {
125
149
  p.log.info('How to improve:\n' + tips.join('\n'));
126
150
  }
127
- if (!options?.force) {
128
- p.log.error('Use `pmpt publish --force` to publish anyway.');
129
- process.exit(1);
151
+ // Generate and copy AI improvement prompt
152
+ const improvementPrompt = generateImprovementPrompt(quality);
153
+ const copied = copyToClipboard(improvementPrompt);
154
+ if (copied) {
155
+ p.log.message('');
156
+ p.log.success('AI improvement prompt copied to clipboard!');
157
+ p.log.message(' Paste it into Claude Code, Cursor, or any AI tool to improve your project.');
158
+ p.log.message(' After improving, run `pmpt save` then `pmpt publish` again.');
159
+ }
160
+ else {
161
+ p.log.message('');
162
+ p.note(improvementPrompt, 'AI Improvement Prompt');
163
+ p.log.message(' Copy the prompt above and paste into your AI tool.');
130
164
  }
131
- p.log.warn('Publishing with --force despite low quality score.');
165
+ p.outro('');
166
+ process.exit(1);
132
167
  }
133
168
  const projectName = planProgress?.answers?.projectName || basename(projectPath);
134
169
  // Try to load existing published data for prefill
@@ -38,6 +38,12 @@ export async function cmdSave(fileOrPath) {
38
38
  msg += ` (${changedCount} changed, ${unchangedCount} skipped)`;
39
39
  }
40
40
  p.log.success(msg);
41
+ // Warn if pmpt.md was not updated since last save
42
+ if (entry.version > 1 && entry.changedFiles && !entry.changedFiles.includes('pmpt.md')) {
43
+ p.log.message('');
44
+ p.log.warn('pmpt.md has not been updated since the last save.');
45
+ p.log.message(' Tip: Mark completed features and update the Snapshot Log before saving.');
46
+ }
41
47
  p.log.message('');
42
48
  p.log.info('Files included:');
43
49
  for (const file of entry.files) {
package/dist/index.js CHANGED
@@ -37,7 +37,7 @@ import { cmdPublish } from './commands/publish.js';
37
37
  import { cmdEdit } from './commands/edit.js';
38
38
  import { cmdUnpublish } from './commands/unpublish.js';
39
39
  import { cmdClone } from './commands/clone.js';
40
- import { cmdBrowse } from './commands/browse.js';
40
+ import { cmdExplore } from './commands/browse.js';
41
41
  import { cmdRecover } from './commands/recover.js';
42
42
  import { cmdDiff } from './commands/diff.js';
43
43
  import { cmdInternalSeed } from './commands/internal-seed.js';
@@ -64,7 +64,7 @@ Examples:
64
64
  $ pmpt login Authenticate with pmptwiki
65
65
  $ pmpt publish Publish project to pmptwiki
66
66
  $ pmpt clone <slug> Clone a project from pmptwiki
67
- $ pmpt browse Browse published projects
67
+ $ pmpt explore Explore projects on pmptwiki.com
68
68
  $ pmpt recover Recover damaged pmpt.md via AI
69
69
 
70
70
  Documentation: https://pmptwiki.com
@@ -133,7 +133,6 @@ program
133
133
  program
134
134
  .command('publish [path]')
135
135
  .description('Publish project to pmptwiki platform')
136
- .option('--force', 'Publish even if quality score is below minimum')
137
136
  .option('--non-interactive', 'Run without interactive prompts')
138
137
  .option('--meta-file <file>', 'JSON file with slug, description, tags, category')
139
138
  .option('--slug <slug>', 'Project slug')
@@ -157,9 +156,9 @@ program
157
156
  .description('Clone a project from pmptwiki platform')
158
157
  .action(cmdClone);
159
158
  program
160
- .command('browse')
161
- .description('Browse and search published projects')
162
- .action(cmdBrowse);
159
+ .command('explore')
160
+ .description('Open pmptwiki.com to explore and search projects')
161
+ .action(cmdExplore);
163
162
  program
164
163
  .command('recover [path]')
165
164
  .description('Generate a recovery prompt to regenerate pmpt.md via AI')
@@ -37,6 +37,11 @@ export function initializeProject(projectPath, options) {
37
37
  trackGit: options?.trackGit ?? true,
38
38
  };
39
39
  saveConfig(projectPath, config);
40
+ // Create README.md if it doesn't exist
41
+ const readmePath = join(configDir, 'README.md');
42
+ if (!existsSync(readmePath)) {
43
+ writeFileSync(readmePath, PMPT_README, 'utf-8');
44
+ }
40
45
  return config;
41
46
  }
42
47
  export function loadConfig(projectPath) {
@@ -54,3 +59,55 @@ export function saveConfig(projectPath, config) {
54
59
  const configPath = join(getConfigDir(projectPath), CONFIG_FILE);
55
60
  writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
56
61
  }
62
+ const PMPT_README = `# .pmpt — Your Project's Development Journal
63
+
64
+ This folder is managed by [pmpt](https://pmptwiki.com). It records your product development journey with AI.
65
+
66
+ ## What's Inside
67
+
68
+ \`\`\`
69
+ .pmpt/
70
+ ├── config.json ← Project settings (auto-generated)
71
+ ├── docs/
72
+ │ ├── pmpt.md ← Human-facing project document (YOU update this)
73
+ │ ├── pmpt.ai.md ← AI-facing prompt (paste into your AI tool)
74
+ │ └── plan.md ← Original plan from pmpt plan
75
+ └── .history/ ← Version snapshots (auto-managed)
76
+ \`\`\`
77
+
78
+ ## Quick Reference
79
+
80
+ | Command | What it does |
81
+ |---------|-------------|
82
+ | \`pmpt plan\` | Create or view your AI prompt |
83
+ | \`pmpt save\` | Save a snapshot of current docs |
84
+ | \`pmpt history\` | View version history |
85
+ | \`pmpt diff\` | Compare versions side by side |
86
+ | \`pmpt publish\` | Share your journey on pmptwiki.com |
87
+
88
+ ## How to Get the Most Out of pmpt
89
+
90
+ 1. **Paste \`pmpt.ai.md\` into your AI tool** to start building
91
+ 2. **Update \`pmpt.md\` as you go** — mark features done, log decisions
92
+ 3. **Run \`pmpt save\` at milestones** — after setup, after each feature, after big changes
93
+ 4. **Publish when ready** — others can clone your journey and learn from it
94
+
95
+ ## When Things Go Wrong
96
+
97
+ | Problem | Solution |
98
+ |---------|----------|
99
+ | Lost your AI prompt | \`pmpt plan\` to regenerate or view it |
100
+ | Messed up docs | \`pmpt history\` → \`pmpt diff\` to find the good version |
101
+ | Need to start over | \`pmpt recover\` rebuilds context from history |
102
+ | Accidentally deleted .pmpt | Re-clone from pmptwiki.com if published |
103
+
104
+ ## One Request
105
+
106
+ Please keep \`pmpt.md\` updated as you build. It's the human-readable record of your journey — what you tried, what worked, what you decided. When you publish, this is what others will learn from.
107
+
108
+ Your snapshots tell a story. Make it a good one.
109
+
110
+ ---
111
+
112
+ *Learn more at [pmptwiki.com](https://pmptwiki.com)*
113
+ `;
@@ -3,18 +3,31 @@ import { basename, join, relative } from 'path';
3
3
  import { getHistoryDir, getDocsDir, loadConfig } from './config.js';
4
4
  import { getGitInfo, isGitRepo } from './git.js';
5
5
  import glob from 'fast-glob';
6
+ /** Generate compact timestamp for snapshot dir names: 20260225T163000 */
7
+ function compactTimestamp() {
8
+ return new Date().toISOString().replace(/[-:\.]/g, '').slice(0, 15);
9
+ }
10
+ /** Parse snapshot dir timestamp (compact or legacy) to ISO string */
11
+ function parseTimestamp(raw) {
12
+ // Compact: 20260225T163000
13
+ if (/^\d{8}T\d{6}$/.test(raw)) {
14
+ return `${raw.slice(0, 4)}-${raw.slice(4, 6)}-${raw.slice(6, 8)}T${raw.slice(9, 11)}:${raw.slice(11, 13)}:${raw.slice(13, 15)}`;
15
+ }
16
+ // Legacy: 2026-02-25T16-30-00
17
+ return raw.replace(/T(.+)$/, (_, time) => 'T' + time.replace(/-/g, ':'));
18
+ }
6
19
  /**
7
20
  * Save .pmpt/docs MD files as snapshot
8
21
  * Copy only changed files to optimize storage
9
22
  */
10
- export function createFullSnapshot(projectPath) {
23
+ export function createFullSnapshot(projectPath, options) {
11
24
  const historyDir = getHistoryDir(projectPath);
12
25
  const docsDir = getDocsDir(projectPath);
13
26
  mkdirSync(historyDir, { recursive: true });
14
27
  // Find next version number
15
28
  const existing = getAllSnapshots(projectPath);
16
29
  const version = existing.length + 1;
17
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
30
+ const timestamp = compactTimestamp();
18
31
  const snapshotName = `v${version}-${timestamp}`;
19
32
  const snapshotDir = join(historyDir, snapshotName);
20
33
  mkdirSync(snapshotDir, { recursive: true });
@@ -63,12 +76,14 @@ export function createFullSnapshot(projectPath) {
63
76
  }
64
77
  }
65
78
  // Save metadata
79
+ const note = options?.note;
66
80
  const metaPath = join(snapshotDir, '.meta.json');
67
81
  writeFileSync(metaPath, JSON.stringify({
68
82
  version,
69
83
  timestamp,
70
84
  files,
71
85
  changedFiles,
86
+ ...(note ? { note } : {}),
72
87
  git: gitData,
73
88
  }, null, 2), 'utf-8');
74
89
  return {
@@ -77,6 +92,7 @@ export function createFullSnapshot(projectPath) {
77
92
  snapshotDir,
78
93
  files,
79
94
  changedFiles,
95
+ note,
80
96
  git: gitData,
81
97
  };
82
98
  }
@@ -98,7 +114,7 @@ export function createSnapshot(projectPath, filePath) {
98
114
  }
99
115
  function createSingleFileSnapshot(projectPath, filePath, relPath) {
100
116
  const historyDir = getHistoryDir(projectPath);
101
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
117
+ const timestamp = compactTimestamp();
102
118
  // Check existing version count for this file
103
119
  const existing = getFileHistory(projectPath, relPath);
104
120
  const version = existing.length + 1;
@@ -168,10 +184,11 @@ export function getAllSnapshots(projectPath) {
168
184
  }
169
185
  entries.push({
170
186
  version: parseInt(match[1], 10),
171
- timestamp: match[2].replace(/-/g, ':'),
187
+ timestamp: parseTimestamp(match[2]),
172
188
  snapshotDir,
173
189
  files: meta.files || [],
174
190
  changedFiles: meta.changedFiles,
191
+ note: meta.note,
175
192
  git: meta.git,
176
193
  });
177
194
  }
@@ -210,7 +227,7 @@ export function getFileHistory(projectPath, relPath) {
210
227
  }
211
228
  entries.push({
212
229
  version: parseInt(match[1], 10),
213
- timestamp: match[2].replace(/-/g, ':'),
230
+ timestamp: parseTimestamp(match[2]),
214
231
  filePath: relPath,
215
232
  historyPath: filePath,
216
233
  git: gitData,
package/dist/lib/plan.js CHANGED
@@ -88,6 +88,12 @@ I'll confirm progress at each step before moving to the next.
88
88
 
89
89
  Keep the Progress and Snapshot Log sections in pmpt.md up to date.
90
90
  After significant milestones, run \`pmpt save\` to create a snapshot.
91
+
92
+ ### Per-Feature Checklist
93
+ After completing each feature above:
94
+ 1. Mark the feature done in \`.pmpt/docs/pmpt.md\` (change \`- [ ]\` to \`- [x]\`)
95
+ 2. Add a brief note to the Snapshot Log section
96
+ 3. Run \`pmpt save\` in terminal
91
97
  `;
92
98
  }
93
99
  // Generate human-facing project document (pmpt.md)
package/dist/mcp.js ADDED
@@ -0,0 +1,247 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * pmpt MCP Server
4
+ *
5
+ * Exposes pmpt functionality as MCP tools so AI tools
6
+ * (Claude Code, Cursor, etc.) can interact with pmpt directly.
7
+ */
8
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
10
+ import { z } from 'zod';
11
+ import { resolve, join } from 'path';
12
+ import { existsSync, readFileSync } from 'fs';
13
+ import glob from 'fast-glob';
14
+ import { createRequire } from 'module';
15
+ import { isInitialized, loadConfig, getDocsDir } from './lib/config.js';
16
+ import { createFullSnapshot, getAllSnapshots, getTrackedFiles, resolveFullSnapshot } from './lib/history.js';
17
+ import { computeQuality } from './lib/quality.js';
18
+ import { getPlanProgress } from './lib/plan.js';
19
+ import { isGitRepo } from './lib/git.js';
20
+ import { diffSnapshots } from './lib/diff.js';
21
+ const require = createRequire(import.meta.url);
22
+ const { version } = require('../package.json');
23
+ // ── Server ──────────────────────────────────────────
24
+ const server = new McpServer({
25
+ name: 'pmpt',
26
+ version,
27
+ });
28
+ // ── Helpers ─────────────────────────────────────────
29
+ function resolveProjectPath(projectPath) {
30
+ return projectPath ? resolve(projectPath) : process.cwd();
31
+ }
32
+ function assertInitialized(pp) {
33
+ if (!isInitialized(pp)) {
34
+ throw new Error(`Project not initialized at ${pp}. Run \`pmpt init\` first.`);
35
+ }
36
+ }
37
+ function readWorkingCopy(pp) {
38
+ const docsDir = getDocsDir(pp);
39
+ const files = {};
40
+ if (!existsSync(docsDir))
41
+ return files;
42
+ const mdFiles = glob.sync('**/*.md', { cwd: docsDir });
43
+ for (const file of mdFiles) {
44
+ try {
45
+ files[file] = readFileSync(join(docsDir, file), 'utf-8');
46
+ }
47
+ catch { /* skip */ }
48
+ }
49
+ return files;
50
+ }
51
+ function buildQualityInput(pp) {
52
+ const docsDir = getDocsDir(pp);
53
+ const aiMdPath = join(docsDir, 'pmpt.ai.md');
54
+ const pmptAiMd = existsSync(aiMdPath) ? readFileSync(aiMdPath, 'utf-8') : null;
55
+ const planProgress = getPlanProgress(pp);
56
+ const tracked = getTrackedFiles(pp);
57
+ const snapshots = getAllSnapshots(pp);
58
+ const hasGit = snapshots.some((s) => !!s.git) || isGitRepo(pp);
59
+ return {
60
+ pmptAiMd,
61
+ planAnswers: planProgress?.answers ?? null,
62
+ versionCount: snapshots.length,
63
+ docFiles: tracked,
64
+ hasGit,
65
+ };
66
+ }
67
+ function formatDiffs(diffs) {
68
+ if (diffs.length === 0)
69
+ return 'No differences found.';
70
+ const lines = [];
71
+ const modified = diffs.filter((d) => d.status === 'modified').length;
72
+ const added = diffs.filter((d) => d.status === 'added').length;
73
+ const removed = diffs.filter((d) => d.status === 'removed').length;
74
+ const parts = [];
75
+ if (modified > 0)
76
+ parts.push(`${modified} modified`);
77
+ if (added > 0)
78
+ parts.push(`${added} added`);
79
+ if (removed > 0)
80
+ parts.push(`${removed} removed`);
81
+ lines.push(`${diffs.length} file(s) changed: ${parts.join(', ')}`);
82
+ lines.push('');
83
+ for (const fd of diffs) {
84
+ const icon = fd.status === 'added' ? 'A' : fd.status === 'removed' ? 'D' : 'M';
85
+ lines.push(`[${icon}] ${fd.fileName}`);
86
+ for (const hunk of fd.hunks) {
87
+ lines.push(`@@ -${hunk.oldStart},${hunk.oldCount} +${hunk.newStart},${hunk.newCount} @@`);
88
+ for (const line of hunk.lines) {
89
+ const prefix = line.type === 'add' ? '+' : line.type === 'remove' ? '-' : ' ';
90
+ lines.push(`${prefix}${line.content}`);
91
+ }
92
+ }
93
+ lines.push('');
94
+ }
95
+ return lines.join('\n');
96
+ }
97
+ // ── Tools ───────────────────────────────────────────
98
+ server.tool('pmpt_save', 'Save a snapshot of .pmpt/docs/ files. Call after completing features, fixes, or milestones.', { projectPath: z.string().optional().describe('Project root path. Defaults to cwd.') }, async ({ projectPath }) => {
99
+ try {
100
+ const pp = resolveProjectPath(projectPath);
101
+ assertInitialized(pp);
102
+ const tracked = getTrackedFiles(pp);
103
+ if (tracked.length === 0) {
104
+ return { content: [{ type: 'text', text: 'No files to save. Add .md files to .pmpt/docs/ first.' }] };
105
+ }
106
+ const entry = createFullSnapshot(pp);
107
+ const changedCount = entry.changedFiles?.length ?? entry.files.length;
108
+ return {
109
+ content: [{
110
+ type: 'text',
111
+ text: [
112
+ `Snapshot v${entry.version} saved (${changedCount} changed, ${entry.files.length - changedCount} unchanged).`,
113
+ '',
114
+ `Files: ${entry.files.join(', ')}`,
115
+ entry.changedFiles ? `Changed: ${entry.changedFiles.join(', ')}` : '',
116
+ entry.git ? `Git: ${entry.git.commit} (${entry.git.branch}${entry.git.dirty ? ', dirty' : ''})` : '',
117
+ ].filter(Boolean).join('\n'),
118
+ }],
119
+ };
120
+ }
121
+ catch (error) {
122
+ return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
123
+ }
124
+ });
125
+ server.tool('pmpt_status', 'Check pmpt project status: tracked files, snapshot count, and quality score.', { projectPath: z.string().optional().describe('Project root path. Defaults to cwd.') }, async ({ projectPath }) => {
126
+ try {
127
+ const pp = resolveProjectPath(projectPath);
128
+ if (!isInitialized(pp)) {
129
+ return { content: [{ type: 'text', text: 'Project not initialized. Run `pmpt init` to start.' }] };
130
+ }
131
+ const config = loadConfig(pp);
132
+ const tracked = getTrackedFiles(pp);
133
+ const snapshots = getAllSnapshots(pp);
134
+ const quality = computeQuality(buildQualityInput(pp));
135
+ const lines = [
136
+ `pmpt status: ${tracked.length} file(s), ${snapshots.length} snapshot(s), quality ${quality.score}/100 (${quality.grade})`,
137
+ `Files: ${tracked.join(', ') || '(none)'}`,
138
+ config?.lastPublished ? `Last published: ${config.lastPublished.slice(0, 10)}` : '',
139
+ '',
140
+ ];
141
+ for (const d of quality.details) {
142
+ const icon = d.score === d.maxScore ? '[PASS]' : '[FAIL]';
143
+ lines.push(`${icon} ${d.label}: ${d.score}/${d.maxScore}${d.tip ? ` — ${d.tip}` : ''}`);
144
+ }
145
+ return { content: [{ type: 'text', text: lines.filter(Boolean).join('\n') }] };
146
+ }
147
+ catch (error) {
148
+ return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
149
+ }
150
+ });
151
+ server.tool('pmpt_history', 'View version history of pmpt snapshots.', {
152
+ projectPath: z.string().optional().describe('Project root path. Defaults to cwd.'),
153
+ limit: z.number().optional().describe('Max snapshots to return (most recent). Defaults to all.'),
154
+ }, async ({ projectPath, limit }) => {
155
+ try {
156
+ const pp = resolveProjectPath(projectPath);
157
+ assertInitialized(pp);
158
+ const snapshots = getAllSnapshots(pp);
159
+ if (snapshots.length === 0) {
160
+ return { content: [{ type: 'text', text: 'No snapshots yet. Run `pmpt save` to create one.' }] };
161
+ }
162
+ let display = snapshots;
163
+ if (limit && limit > 0 && limit < snapshots.length) {
164
+ display = snapshots.slice(-limit);
165
+ }
166
+ const lines = [`${snapshots.length} snapshot(s)${limit ? `, showing last ${display.length}` : ''}:`, ''];
167
+ for (const s of display) {
168
+ const changed = s.changedFiles?.length ?? s.files.length;
169
+ const git = s.git ? ` [${s.git.commit}]` : '';
170
+ lines.push(`v${s.version} — ${s.timestamp.slice(0, 16)} — ${changed} changed, ${s.files.length} total${git}`);
171
+ }
172
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
173
+ }
174
+ catch (error) {
175
+ return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
176
+ }
177
+ });
178
+ server.tool('pmpt_diff', 'Compare two versions, or a version against the current working copy.', {
179
+ projectPath: z.string().optional().describe('Project root path. Defaults to cwd.'),
180
+ v1: z.number().describe('First version number (e.g. 1 for v1).'),
181
+ v2: z.number().optional().describe('Second version. If omitted, compares against working copy.'),
182
+ }, async ({ projectPath, v1, v2 }) => {
183
+ try {
184
+ const pp = resolveProjectPath(projectPath);
185
+ assertInitialized(pp);
186
+ const snapshots = getAllSnapshots(pp);
187
+ const fromIndex = snapshots.findIndex((s) => s.version === v1);
188
+ if (fromIndex === -1) {
189
+ return { content: [{ type: 'text', text: `Version v${v1} not found.` }], isError: true };
190
+ }
191
+ const oldFiles = resolveFullSnapshot(snapshots, fromIndex);
192
+ let newFiles;
193
+ let targetLabel;
194
+ if (v2 !== undefined) {
195
+ const toIndex = snapshots.findIndex((s) => s.version === v2);
196
+ if (toIndex === -1) {
197
+ return { content: [{ type: 'text', text: `Version v${v2} not found.` }], isError: true };
198
+ }
199
+ newFiles = resolveFullSnapshot(snapshots, toIndex);
200
+ targetLabel = `v${v2}`;
201
+ }
202
+ else {
203
+ newFiles = readWorkingCopy(pp);
204
+ targetLabel = 'working copy';
205
+ }
206
+ const diffs = diffSnapshots(oldFiles, newFiles);
207
+ return {
208
+ content: [
209
+ { type: 'text', text: `Diff: v${v1} → ${targetLabel}` },
210
+ { type: 'text', text: formatDiffs(diffs) },
211
+ ],
212
+ };
213
+ }
214
+ catch (error) {
215
+ return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
216
+ }
217
+ });
218
+ server.tool('pmpt_quality', 'Check project quality score and publish readiness.', { projectPath: z.string().optional().describe('Project root path. Defaults to cwd.') }, async ({ projectPath }) => {
219
+ try {
220
+ const pp = resolveProjectPath(projectPath);
221
+ assertInitialized(pp);
222
+ const quality = computeQuality(buildQualityInput(pp));
223
+ const lines = [
224
+ `Quality: ${quality.score}/100 (Grade ${quality.grade})`,
225
+ `Publish ready: ${quality.passesMinimum ? 'Yes' : 'No (minimum 40 required)'}`,
226
+ '',
227
+ ];
228
+ for (const item of quality.details) {
229
+ const icon = item.score === item.maxScore ? '[PASS]' : '[FAIL]';
230
+ lines.push(`${icon} ${item.label}: ${item.score}/${item.maxScore}${item.tip ? ` — ${item.tip}` : ''}`);
231
+ }
232
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
233
+ }
234
+ catch (error) {
235
+ return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
236
+ }
237
+ });
238
+ // ── Start ───────────────────────────────────────────
239
+ async function main() {
240
+ const transport = new StdioServerTransport();
241
+ await server.connect(transport);
242
+ console.error('pmpt MCP server running on stdio');
243
+ }
244
+ main().catch((error) => {
245
+ console.error('Fatal error:', error);
246
+ process.exit(1);
247
+ });
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "pmpt-cli",
3
- "version": "1.11.0",
3
+ "version": "1.12.0",
4
4
  "description": "Record and share your AI-driven product development journey",
5
5
  "type": "module",
6
6
  "bin": {
7
- "pmpt": "./dist/index.js"
7
+ "pmpt": "./dist/index.js",
8
+ "pmpt-mcp": "./dist/mcp.js"
8
9
  },
9
10
  "files": [
10
11
  "dist"
@@ -35,6 +36,7 @@
35
36
  },
36
37
  "dependencies": {
37
38
  "@clack/prompts": "^0.7.0",
39
+ "@modelcontextprotocol/sdk": "^1.27.1",
38
40
  "chokidar": "^3.6.0",
39
41
  "commander": "^12.0.0",
40
42
  "fast-glob": "^3.3.0",