pmpt-cli 1.11.1 → 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
  }
@@ -82,12 +82,15 @@ export async function cmdClone(slug) {
82
82
  const pmptData = validation.data;
83
83
  s.stop('Download complete');
84
84
  // Show summary
85
- p.note([
85
+ const infoLines = [
86
86
  `Project: ${pmptData.meta.projectName}`,
87
87
  `Versions: ${pmptData.history.length}`,
88
88
  pmptData.meta.author ? `Author: @${pmptData.meta.author}` : '',
89
- pmptData.meta.description ? `Description: ${pmptData.meta.description.slice(0, 80)}` : '',
90
- ].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');
91
94
  const projectPath = process.cwd();
92
95
  if (isInitialized(projectPath)) {
93
96
  const overwrite = await p.confirm({
@@ -90,7 +90,6 @@ export async function cmdInternalSeed(options) {
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
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')
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.1",
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",