pmpt-cli 1.11.1 → 1.12.1
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 +49 -15
- package/dist/commands/browse.js +9 -71
- package/dist/commands/clone.js +6 -3
- package/dist/commands/init.js +6 -1
- package/dist/commands/internal-seed.js +0 -1
- package/dist/commands/publish.js +146 -78
- package/dist/commands/update.js +154 -0
- package/dist/index.js +11 -6
- package/dist/lib/config.js +2 -0
- package/dist/lib/git.js +7 -0
- package/dist/lib/pmptFile.js +2 -0
- package/dist/mcp.js +247 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -2,17 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
# pmpt
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**Record and share your AI-driven product development journey.**
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/pmpt-cli)
|
|
8
8
|
[](https://github.com/pmptwiki/pmpt-cli/blob/main/LICENSE)
|
|
9
9
|
[](https://github.com/pmptwiki/pmpt-cli)
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Plan with 5 questions. Build with AI. Save every version. Share and reproduce.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
[Quick Start](#quick-start) · [Commands](#commands) · [How It Works](#how-it-works) · [Explore Projects](#explore-projects)
|
|
13
|
+
[Quick Start](#quick-start) · [Commands](#commands) · [MCP Server](#mcp-server) · [Explore Projects](#explore-projects)
|
|
16
14
|
|
|
17
15
|
</div>
|
|
18
16
|
|
|
@@ -57,7 +55,7 @@ pmpt save
|
|
|
57
55
|
pmpt login && pmpt publish
|
|
58
56
|
|
|
59
57
|
# Bonus: Explore what others are building
|
|
60
|
-
pmpt
|
|
58
|
+
pmpt explore
|
|
61
59
|
```
|
|
62
60
|
|
|
63
61
|
---
|
|
@@ -113,17 +111,51 @@ The generated prompt is **automatically copied to your clipboard**. Just paste i
|
|
|
113
111
|
| Command | Description |
|
|
114
112
|
|---------|-------------|
|
|
115
113
|
| `pmpt login` | Authenticate via GitHub (one-time) |
|
|
116
|
-
| `pmpt publish` | Publish your project (requires
|
|
117
|
-
| `pmpt
|
|
114
|
+
| `pmpt publish` | Publish your project (requires quality score ≥ 40) |
|
|
115
|
+
| `pmpt update` | Quick re-publish: update content without changing metadata |
|
|
118
116
|
| `pmpt edit` | Edit published project metadata (description, tags, category) |
|
|
119
117
|
| `pmpt unpublish` | Remove a published project from pmptwiki |
|
|
120
118
|
| `pmpt clone <slug>` | Clone and reproduce someone's project |
|
|
121
|
-
| `pmpt
|
|
119
|
+
| `pmpt explore` | Open pmptwiki.com/explore in your browser |
|
|
120
|
+
|
|
121
|
+
> Quality score below 40? pmpt copies an **AI improvement prompt** to your clipboard — paste it into your AI tool to get help improving your project.
|
|
122
122
|
|
|
123
123
|
> See the full documentation at [pmptwiki.com/docs](https://pmptwiki.com/docs)
|
|
124
124
|
|
|
125
125
|
---
|
|
126
126
|
|
|
127
|
+
## MCP Server
|
|
128
|
+
|
|
129
|
+
pmpt includes a built-in [MCP](https://modelcontextprotocol.io) server so AI tools can interact with pmpt directly. This means Claude Code, Cursor, and other MCP-compatible tools can save snapshots, check status, and review history without you typing commands.
|
|
130
|
+
|
|
131
|
+
### Setup
|
|
132
|
+
|
|
133
|
+
Add to your `.mcp.json` (or IDE MCP config):
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"mcpServers": {
|
|
138
|
+
"pmpt": {
|
|
139
|
+
"command": "pmpt-mcp"
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Available Tools
|
|
146
|
+
|
|
147
|
+
| Tool | Description |
|
|
148
|
+
|------|-------------|
|
|
149
|
+
| `pmpt_save` | Save a snapshot after completing features, fixes, or milestones |
|
|
150
|
+
| `pmpt_status` | Check tracked files, snapshot count, and quality score |
|
|
151
|
+
| `pmpt_history` | View version history with git commit info |
|
|
152
|
+
| `pmpt_diff` | Compare two versions, or a version against working copy |
|
|
153
|
+
| `pmpt_quality` | Check quality score and publish readiness |
|
|
154
|
+
|
|
155
|
+
All tools accept an optional `projectPath` parameter (defaults to cwd).
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
127
159
|
## How It Works
|
|
128
160
|
|
|
129
161
|
```
|
|
@@ -158,7 +190,8 @@ your-project/
|
|
|
158
190
|
├── config.json # Project configuration
|
|
159
191
|
├── docs/ # Generated documents
|
|
160
192
|
│ ├── plan.md # Product plan (features checklist)
|
|
161
|
-
│
|
|
193
|
+
│ ├── pmpt.md # Progress tracking & decisions
|
|
194
|
+
│ └── pmpt.ai.md # AI-ready prompt (project context & instructions)
|
|
162
195
|
└── .history/ # Auto-saved version history
|
|
163
196
|
├── v1-2024-02-20/
|
|
164
197
|
├── v2-2024-02-21/
|
|
@@ -186,7 +219,8 @@ A single portable file containing your entire development journey:
|
|
|
186
219
|
},
|
|
187
220
|
"docs": {
|
|
188
221
|
"plan.md": "...",
|
|
189
|
-
"pmpt.md": "..."
|
|
222
|
+
"pmpt.md": "...",
|
|
223
|
+
"pmpt.ai.md": "..."
|
|
190
224
|
},
|
|
191
225
|
"history": [
|
|
192
226
|
{ "version": 1, "timestamp": "...", "files": {} },
|
|
@@ -210,11 +244,11 @@ Share it. Clone it. Reproduce it.
|
|
|
210
244
|
|
|
211
245
|
## Explore Projects
|
|
212
246
|
|
|
213
|
-
Don't know what to build?
|
|
247
|
+
Don't know what to build? See what others have created with AI.
|
|
214
248
|
|
|
215
249
|
```bash
|
|
216
|
-
#
|
|
217
|
-
pmpt
|
|
250
|
+
# Open the explore page
|
|
251
|
+
pmpt explore
|
|
218
252
|
|
|
219
253
|
# Found something interesting? Clone it and make it yours
|
|
220
254
|
pmpt clone budget-tracker-app
|
|
@@ -222,7 +256,7 @@ pmpt clone budget-tracker-app
|
|
|
222
256
|
|
|
223
257
|
**[Explore Projects on pmptwiki.com →](https://pmptwiki.com/explore)**
|
|
224
258
|
|
|
225
|
-
|
|
259
|
+
Clone any project to see how it was planned, what prompts were used, and how it evolved step by step. The clone output shows the product idea, tech stack, and full version history.
|
|
226
260
|
|
|
227
261
|
---
|
|
228
262
|
|
package/dist/commands/browse.js
CHANGED
|
@@ -1,73 +1,11 @@
|
|
|
1
1
|
import * as p from '@clack/prompts';
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
}
|
package/dist/commands/clone.js
CHANGED
|
@@ -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
|
-
|
|
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 ?
|
|
90
|
-
|
|
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({
|
package/dist/commands/init.js
CHANGED
|
@@ -2,7 +2,7 @@ import * as p from '@clack/prompts';
|
|
|
2
2
|
import { existsSync, readFileSync } from 'fs';
|
|
3
3
|
import { resolve } from 'path';
|
|
4
4
|
import { initializeProject, isInitialized } from '../lib/config.js';
|
|
5
|
-
import { isGitRepo, getGitInfo, formatGitInfo } from '../lib/git.js';
|
|
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
8
|
import { savePlanDocuments, initPlanProgress, savePlanProgress } from '../lib/plan.js';
|
|
@@ -84,12 +84,17 @@ export async function cmdInit(path, options) {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
+
// Detect project origin
|
|
88
|
+
const gitCommits = isGit ? getCommitCount(projectPath) : 0;
|
|
89
|
+
const origin = gitCommits >= 10 ? 'adopted' : 'new';
|
|
87
90
|
const s = p.spinner();
|
|
88
91
|
s.start('Initializing project...');
|
|
89
92
|
try {
|
|
90
93
|
const config = initializeProject(projectPath, {
|
|
91
94
|
repo: repoUrl,
|
|
92
95
|
trackGit: isGit,
|
|
96
|
+
origin,
|
|
97
|
+
gitCommitsAtInit: isGit ? gitCommits : undefined,
|
|
93
98
|
});
|
|
94
99
|
s.stop('Initialized');
|
|
95
100
|
// Build folder structure display
|
|
@@ -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,
|
package/dist/commands/publish.js
CHANGED
|
@@ -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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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.
|
|
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
|
|
@@ -189,89 +224,120 @@ export async function cmdPublish(path, options) {
|
|
|
189
224
|
}
|
|
190
225
|
}
|
|
191
226
|
else {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
p.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (p.isCancel(descriptionInput)) {
|
|
213
|
-
p.cancel('Cancelled');
|
|
214
|
-
process.exit(0);
|
|
215
|
-
}
|
|
216
|
-
description = descriptionInput;
|
|
217
|
-
const tagsInput = await p.text({
|
|
218
|
-
message: 'Tags (comma-separated):',
|
|
219
|
-
placeholder: 'react, saas, mvp',
|
|
220
|
-
defaultValue: existing?.tags?.join(', ') || '',
|
|
221
|
-
});
|
|
222
|
-
if (p.isCancel(tagsInput)) {
|
|
223
|
-
p.cancel('Cancelled');
|
|
224
|
-
process.exit(0);
|
|
225
|
-
}
|
|
226
|
-
tags = normalizeTags(tagsInput);
|
|
227
|
-
const categoryInput = await p.select({
|
|
228
|
-
message: 'Project category:',
|
|
229
|
-
initialValue: existing?.category || 'other',
|
|
230
|
-
options: CATEGORY_OPTIONS,
|
|
231
|
-
});
|
|
232
|
-
if (p.isCancel(categoryInput)) {
|
|
233
|
-
p.cancel('Cancelled');
|
|
234
|
-
process.exit(0);
|
|
227
|
+
// If previously published, offer to reuse settings
|
|
228
|
+
let reuseExisting = false;
|
|
229
|
+
if (existing && savedSlug) {
|
|
230
|
+
const categoryLabel = CATEGORY_OPTIONS.find((o) => o.value === existing.category)?.label ?? existing.category ?? '';
|
|
231
|
+
p.note([
|
|
232
|
+
`Slug: ${savedSlug}`,
|
|
233
|
+
`Description: ${existing.description || '(none)'}`,
|
|
234
|
+
existing.tags?.length ? `Tags: ${existing.tags.join(', ')}` : '',
|
|
235
|
+
categoryLabel ? `Category: ${categoryLabel}` : '',
|
|
236
|
+
existing.productUrl ? `Product: ${existing.productUrl}` : '',
|
|
237
|
+
].filter(Boolean).join('\n'), 'Previous Settings');
|
|
238
|
+
const reuse = await p.confirm({
|
|
239
|
+
message: 'Publish with same settings?',
|
|
240
|
+
initialValue: true,
|
|
241
|
+
});
|
|
242
|
+
if (p.isCancel(reuse)) {
|
|
243
|
+
p.cancel('Cancelled');
|
|
244
|
+
process.exit(0);
|
|
245
|
+
}
|
|
246
|
+
reuseExisting = !!reuse;
|
|
235
247
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
{ value: 'git', label: 'Git Repository' },
|
|
244
|
-
{ value: 'url', label: 'Website / URL' },
|
|
245
|
-
],
|
|
246
|
-
});
|
|
247
|
-
if (p.isCancel(linkTypeInput)) {
|
|
248
|
-
p.cancel('Cancelled');
|
|
249
|
-
process.exit(0);
|
|
248
|
+
if (reuseExisting && existing && savedSlug) {
|
|
249
|
+
slug = savedSlug;
|
|
250
|
+
description = existing.description || '';
|
|
251
|
+
tags = existing.tags || [];
|
|
252
|
+
category = existing.category || 'other';
|
|
253
|
+
productUrl = existing.productUrl || '';
|
|
254
|
+
productUrlType = existing.productUrlType || '';
|
|
250
255
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
? `https://github.com/${auth.username}/${slug}`
|
|
257
|
-
: 'https://...',
|
|
258
|
-
defaultValue: existing?.productUrl || '',
|
|
256
|
+
else {
|
|
257
|
+
const slugInput = await p.text({
|
|
258
|
+
message: 'Project slug (used in URL):',
|
|
259
|
+
placeholder: defaultSlug,
|
|
260
|
+
defaultValue: savedSlug || '',
|
|
259
261
|
validate: (v) => {
|
|
260
|
-
if (
|
|
261
|
-
return '
|
|
262
|
-
try {
|
|
263
|
-
new URL(v);
|
|
264
|
-
}
|
|
265
|
-
catch {
|
|
266
|
-
return 'Invalid URL format.';
|
|
262
|
+
if (!/^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$/.test(v)) {
|
|
263
|
+
return '3-50 chars, lowercase letters, numbers, and hyphens only.';
|
|
267
264
|
}
|
|
268
265
|
},
|
|
269
266
|
});
|
|
270
|
-
if (p.isCancel(
|
|
267
|
+
if (p.isCancel(slugInput)) {
|
|
271
268
|
p.cancel('Cancelled');
|
|
272
269
|
process.exit(0);
|
|
273
270
|
}
|
|
274
|
-
|
|
271
|
+
slug = slugInput;
|
|
272
|
+
const descriptionInput = await p.text({
|
|
273
|
+
message: 'Project description (brief):',
|
|
274
|
+
placeholder: existing?.description || planProgress?.answers?.productIdea?.slice(0, 100) || '',
|
|
275
|
+
defaultValue: existing?.description || planProgress?.answers?.productIdea?.slice(0, 200) || '',
|
|
276
|
+
});
|
|
277
|
+
if (p.isCancel(descriptionInput)) {
|
|
278
|
+
p.cancel('Cancelled');
|
|
279
|
+
process.exit(0);
|
|
280
|
+
}
|
|
281
|
+
description = descriptionInput;
|
|
282
|
+
const tagsInput = await p.text({
|
|
283
|
+
message: 'Tags (comma-separated):',
|
|
284
|
+
placeholder: 'react, saas, mvp',
|
|
285
|
+
defaultValue: existing?.tags?.join(', ') || '',
|
|
286
|
+
});
|
|
287
|
+
if (p.isCancel(tagsInput)) {
|
|
288
|
+
p.cancel('Cancelled');
|
|
289
|
+
process.exit(0);
|
|
290
|
+
}
|
|
291
|
+
tags = normalizeTags(tagsInput);
|
|
292
|
+
const categoryInput = await p.select({
|
|
293
|
+
message: 'Project category:',
|
|
294
|
+
initialValue: existing?.category || 'other',
|
|
295
|
+
options: CATEGORY_OPTIONS,
|
|
296
|
+
});
|
|
297
|
+
if (p.isCancel(categoryInput)) {
|
|
298
|
+
p.cancel('Cancelled');
|
|
299
|
+
process.exit(0);
|
|
300
|
+
}
|
|
301
|
+
category = categoryInput;
|
|
302
|
+
// Product link (optional)
|
|
303
|
+
const linkTypeInput = await p.select({
|
|
304
|
+
message: 'Product link (optional):',
|
|
305
|
+
initialValue: existing?.productUrlType || 'none',
|
|
306
|
+
options: [
|
|
307
|
+
{ value: 'none', label: 'No link' },
|
|
308
|
+
{ value: 'git', label: 'Git Repository' },
|
|
309
|
+
{ value: 'url', label: 'Website / URL' },
|
|
310
|
+
],
|
|
311
|
+
});
|
|
312
|
+
if (p.isCancel(linkTypeInput)) {
|
|
313
|
+
p.cancel('Cancelled');
|
|
314
|
+
process.exit(0);
|
|
315
|
+
}
|
|
316
|
+
if (linkTypeInput !== 'none') {
|
|
317
|
+
productUrlType = linkTypeInput;
|
|
318
|
+
const productUrlInput = await p.text({
|
|
319
|
+
message: 'Product URL:',
|
|
320
|
+
placeholder: linkTypeInput === 'git'
|
|
321
|
+
? `https://github.com/${auth.username}/${slug}`
|
|
322
|
+
: 'https://...',
|
|
323
|
+
defaultValue: existing?.productUrl || '',
|
|
324
|
+
validate: (v) => {
|
|
325
|
+
if (!v.trim())
|
|
326
|
+
return 'URL is required when link type is selected.';
|
|
327
|
+
try {
|
|
328
|
+
new URL(v);
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
return 'Invalid URL format.';
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
if (p.isCancel(productUrlInput)) {
|
|
336
|
+
p.cancel('Cancelled');
|
|
337
|
+
process.exit(0);
|
|
338
|
+
}
|
|
339
|
+
productUrl = productUrlInput;
|
|
340
|
+
}
|
|
275
341
|
}
|
|
276
342
|
}
|
|
277
343
|
// Build .pmpt content (resolve from optimized snapshots)
|
|
@@ -288,6 +354,8 @@ export async function cmdPublish(path, options) {
|
|
|
288
354
|
description: description,
|
|
289
355
|
createdAt: config?.createdAt || new Date().toISOString(),
|
|
290
356
|
exportedAt: new Date().toISOString(),
|
|
357
|
+
origin: config?.origin,
|
|
358
|
+
gitCommitsAtInit: config?.gitCommitsAtInit,
|
|
291
359
|
};
|
|
292
360
|
const planAnswers = planProgress?.answers
|
|
293
361
|
? {
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { resolve, basename } from 'path';
|
|
3
|
+
import { readFileSync, existsSync } from 'fs';
|
|
4
|
+
import { isInitialized, loadConfig, saveConfig, getDocsDir } from '../lib/config.js';
|
|
5
|
+
import { getAllSnapshots, resolveFullSnapshot } from '../lib/history.js';
|
|
6
|
+
import { getPlanProgress } from '../lib/plan.js';
|
|
7
|
+
import { createPmptFile } from '../lib/pmptFile.js';
|
|
8
|
+
import { loadAuth } from '../lib/auth.js';
|
|
9
|
+
import { publishProject, fetchProjects } from '../lib/api.js';
|
|
10
|
+
import { computeQuality } from '../lib/quality.js';
|
|
11
|
+
import glob from 'fast-glob';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
function readDocsFolder(docsDir) {
|
|
14
|
+
const files = {};
|
|
15
|
+
if (!existsSync(docsDir))
|
|
16
|
+
return files;
|
|
17
|
+
const mdFiles = glob.sync('**/*.md', { cwd: docsDir });
|
|
18
|
+
for (const file of mdFiles) {
|
|
19
|
+
try {
|
|
20
|
+
files[file] = readFileSync(join(docsDir, file), 'utf-8');
|
|
21
|
+
}
|
|
22
|
+
catch { /* skip */ }
|
|
23
|
+
}
|
|
24
|
+
return files;
|
|
25
|
+
}
|
|
26
|
+
export async function cmdUpdate(path) {
|
|
27
|
+
const projectPath = path ? resolve(path) : process.cwd();
|
|
28
|
+
if (!isInitialized(projectPath)) {
|
|
29
|
+
p.log.error('Project not initialized. Run `pmpt init` first.');
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
const auth = loadAuth();
|
|
33
|
+
if (!auth?.token || !auth?.username) {
|
|
34
|
+
p.log.error('Login required. Run `pmpt login` first.');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
p.intro('pmpt update');
|
|
38
|
+
const config = loadConfig(projectPath);
|
|
39
|
+
const savedSlug = config?.lastPublishedSlug;
|
|
40
|
+
if (!savedSlug) {
|
|
41
|
+
p.log.error('No previously published project found.');
|
|
42
|
+
p.log.info('Run `pmpt publish` first to publish your project.');
|
|
43
|
+
p.outro('');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
// Find existing project on platform
|
|
47
|
+
let existing;
|
|
48
|
+
try {
|
|
49
|
+
const index = await fetchProjects();
|
|
50
|
+
existing = index.projects.find((proj) => proj.slug === savedSlug && proj.author === auth.username);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
p.log.error('Failed to fetch project info. Check your internet connection.');
|
|
54
|
+
p.outro('');
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
if (!existing) {
|
|
58
|
+
p.log.error(`Project "${savedSlug}" not found on platform.`);
|
|
59
|
+
p.log.info('Run `pmpt publish` to publish it first.');
|
|
60
|
+
p.outro('');
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
const snapshots = getAllSnapshots(projectPath);
|
|
64
|
+
const planProgress = getPlanProgress(projectPath);
|
|
65
|
+
if (snapshots.length === 0) {
|
|
66
|
+
p.log.warn('No snapshots found. Run `pmpt save` first.');
|
|
67
|
+
p.outro('');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// Quality gate
|
|
71
|
+
const docsDir = getDocsDir(projectPath);
|
|
72
|
+
const aiMdPath = join(docsDir, 'pmpt.ai.md');
|
|
73
|
+
const aiMdContent = existsSync(aiMdPath) ? readFileSync(aiMdPath, 'utf-8').trim() : '';
|
|
74
|
+
const trackedFiles = glob.sync('**/*.md', { cwd: docsDir });
|
|
75
|
+
const hasGit = snapshots.some((s) => !!s.git);
|
|
76
|
+
const quality = computeQuality({
|
|
77
|
+
pmptAiMd: aiMdContent || null,
|
|
78
|
+
planAnswers: planProgress?.answers ?? null,
|
|
79
|
+
versionCount: snapshots.length,
|
|
80
|
+
docFiles: trackedFiles,
|
|
81
|
+
hasGit,
|
|
82
|
+
});
|
|
83
|
+
if (!quality.passesMinimum) {
|
|
84
|
+
p.log.warn(`Quality score ${quality.score}/100 is below minimum (40).`);
|
|
85
|
+
p.log.info('Run `pmpt publish` for detailed quality breakdown and improvement tips.');
|
|
86
|
+
p.outro('');
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
// Build .pmpt content
|
|
90
|
+
const projectName = planProgress?.answers?.projectName || basename(projectPath);
|
|
91
|
+
const history = snapshots.map((snapshot, i) => ({
|
|
92
|
+
version: snapshot.version,
|
|
93
|
+
timestamp: snapshot.timestamp,
|
|
94
|
+
files: resolveFullSnapshot(snapshots, i),
|
|
95
|
+
git: snapshot.git,
|
|
96
|
+
}));
|
|
97
|
+
const docs = readDocsFolder(docsDir);
|
|
98
|
+
const meta = {
|
|
99
|
+
projectName,
|
|
100
|
+
author: auth.username,
|
|
101
|
+
description: existing.description,
|
|
102
|
+
createdAt: config?.createdAt || new Date().toISOString(),
|
|
103
|
+
exportedAt: new Date().toISOString(),
|
|
104
|
+
origin: config?.origin,
|
|
105
|
+
gitCommitsAtInit: config?.gitCommitsAtInit,
|
|
106
|
+
};
|
|
107
|
+
const planAnswers = planProgress?.answers
|
|
108
|
+
? {
|
|
109
|
+
projectName: planProgress.answers.projectName,
|
|
110
|
+
productIdea: planProgress.answers.productIdea,
|
|
111
|
+
additionalContext: planProgress.answers.additionalContext,
|
|
112
|
+
coreFeatures: planProgress.answers.coreFeatures,
|
|
113
|
+
techStack: planProgress.answers.techStack,
|
|
114
|
+
}
|
|
115
|
+
: undefined;
|
|
116
|
+
const pmptContent = createPmptFile(meta, planAnswers, docs, history);
|
|
117
|
+
// Show summary
|
|
118
|
+
p.note([
|
|
119
|
+
`Slug: ${savedSlug}`,
|
|
120
|
+
`Versions: ${snapshots.length}`,
|
|
121
|
+
`Size: ${(pmptContent.length / 1024).toFixed(1)} KB`,
|
|
122
|
+
`Quality: ${quality.score}/100 (${quality.grade})`,
|
|
123
|
+
].join('\n'), 'Update Preview');
|
|
124
|
+
// Upload
|
|
125
|
+
const s = p.spinner();
|
|
126
|
+
s.start('Uploading...');
|
|
127
|
+
try {
|
|
128
|
+
const result = await publishProject(auth.token, {
|
|
129
|
+
slug: savedSlug,
|
|
130
|
+
pmptContent,
|
|
131
|
+
description: existing.description,
|
|
132
|
+
tags: existing.tags || [],
|
|
133
|
+
category: existing.category,
|
|
134
|
+
...(existing.productUrl && { productUrl: existing.productUrl, productUrlType: existing.productUrlType }),
|
|
135
|
+
});
|
|
136
|
+
s.stop('Updated!');
|
|
137
|
+
if (config) {
|
|
138
|
+
config.lastPublished = new Date().toISOString();
|
|
139
|
+
saveConfig(projectPath, config);
|
|
140
|
+
}
|
|
141
|
+
p.note([
|
|
142
|
+
`URL: ${result.url}`,
|
|
143
|
+
'',
|
|
144
|
+
'Content updated. Metadata unchanged.',
|
|
145
|
+
'To change metadata, use `pmpt edit`.',
|
|
146
|
+
].join('\n'), 'Updated!');
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
s.stop('Update failed');
|
|
150
|
+
p.log.error(err instanceof Error ? err.message : 'Failed to update.');
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
p.outro('');
|
|
154
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -34,10 +34,11 @@ import { cmdExport } from './commands/export.js';
|
|
|
34
34
|
import { cmdImport } from './commands/import.js';
|
|
35
35
|
import { cmdLogin } from './commands/login.js';
|
|
36
36
|
import { cmdPublish } from './commands/publish.js';
|
|
37
|
+
import { cmdUpdate } from './commands/update.js';
|
|
37
38
|
import { cmdEdit } from './commands/edit.js';
|
|
38
39
|
import { cmdUnpublish } from './commands/unpublish.js';
|
|
39
40
|
import { cmdClone } from './commands/clone.js';
|
|
40
|
-
import {
|
|
41
|
+
import { cmdExplore } from './commands/browse.js';
|
|
41
42
|
import { cmdRecover } from './commands/recover.js';
|
|
42
43
|
import { cmdDiff } from './commands/diff.js';
|
|
43
44
|
import { cmdInternalSeed } from './commands/internal-seed.js';
|
|
@@ -63,8 +64,9 @@ Examples:
|
|
|
63
64
|
$ pmpt import <file.pmpt> Import from .pmpt file
|
|
64
65
|
$ pmpt login Authenticate with pmptwiki
|
|
65
66
|
$ pmpt publish Publish project to pmptwiki
|
|
67
|
+
$ pmpt update Quick re-publish (content only)
|
|
66
68
|
$ pmpt clone <slug> Clone a project from pmptwiki
|
|
67
|
-
$ pmpt
|
|
69
|
+
$ pmpt explore Explore projects on pmptwiki.com
|
|
68
70
|
$ pmpt recover Recover damaged pmpt.md via AI
|
|
69
71
|
|
|
70
72
|
Documentation: https://pmptwiki.com
|
|
@@ -133,7 +135,6 @@ program
|
|
|
133
135
|
program
|
|
134
136
|
.command('publish [path]')
|
|
135
137
|
.description('Publish project to pmptwiki platform')
|
|
136
|
-
.option('--force', 'Publish even if quality score is below minimum')
|
|
137
138
|
.option('--non-interactive', 'Run without interactive prompts')
|
|
138
139
|
.option('--meta-file <file>', 'JSON file with slug, description, tags, category')
|
|
139
140
|
.option('--slug <slug>', 'Project slug')
|
|
@@ -144,6 +145,10 @@ program
|
|
|
144
145
|
.option('--product-url-type <type>', 'Product link type: git or url')
|
|
145
146
|
.option('--yes', 'Skip confirmation prompt')
|
|
146
147
|
.action(cmdPublish);
|
|
148
|
+
program
|
|
149
|
+
.command('update [path]')
|
|
150
|
+
.description('Quick re-publish: update content without changing metadata')
|
|
151
|
+
.action(cmdUpdate);
|
|
147
152
|
program
|
|
148
153
|
.command('edit')
|
|
149
154
|
.description('Edit published project metadata (description, tags, category)')
|
|
@@ -157,9 +162,9 @@ program
|
|
|
157
162
|
.description('Clone a project from pmptwiki platform')
|
|
158
163
|
.action(cmdClone);
|
|
159
164
|
program
|
|
160
|
-
.command('
|
|
161
|
-
.description('
|
|
162
|
-
.action(
|
|
165
|
+
.command('explore')
|
|
166
|
+
.description('Open pmptwiki.com to explore and search projects')
|
|
167
|
+
.action(cmdExplore);
|
|
163
168
|
program
|
|
164
169
|
.command('recover [path]')
|
|
165
170
|
.description('Generate a recovery prompt to regenerate pmpt.md via AI')
|
package/dist/lib/config.js
CHANGED
|
@@ -35,6 +35,8 @@ export function initializeProject(projectPath, options) {
|
|
|
35
35
|
createdAt: new Date().toISOString(),
|
|
36
36
|
repo: options?.repo,
|
|
37
37
|
trackGit: options?.trackGit ?? true,
|
|
38
|
+
origin: options?.origin,
|
|
39
|
+
gitCommitsAtInit: options?.gitCommitsAtInit,
|
|
38
40
|
};
|
|
39
41
|
saveConfig(projectPath, config);
|
|
40
42
|
// Create README.md if it doesn't exist
|
package/dist/lib/git.js
CHANGED
|
@@ -84,6 +84,13 @@ export function isCommitMatch(path, expectedCommit) {
|
|
|
84
84
|
currentFull.startsWith(expectedCommit) ||
|
|
85
85
|
expectedCommit.startsWith(currentShort));
|
|
86
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Count total commits in the repository
|
|
89
|
+
*/
|
|
90
|
+
export function getCommitCount(path) {
|
|
91
|
+
const count = git(path, 'rev-list --count HEAD');
|
|
92
|
+
return count ? parseInt(count, 10) : 0;
|
|
93
|
+
}
|
|
87
94
|
/**
|
|
88
95
|
* Convert git info to human-readable string
|
|
89
96
|
*/
|
package/dist/lib/pmptFile.js
CHANGED
|
@@ -65,6 +65,8 @@ const MetaSchema = z.object({
|
|
|
65
65
|
description: z.string().optional(),
|
|
66
66
|
createdAt: z.string(),
|
|
67
67
|
exportedAt: z.string(),
|
|
68
|
+
origin: z.enum(['new', 'adopted']).optional(),
|
|
69
|
+
gitCommitsAtInit: z.number().optional(),
|
|
68
70
|
});
|
|
69
71
|
// Full .pmpt file schema
|
|
70
72
|
export const PmptFileSchema = z.object({
|
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.
|
|
3
|
+
"version": "1.12.1",
|
|
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",
|