pmpt-cli 1.12.4 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -130,6 +130,22 @@ pmpt includes a built-in [MCP](https://modelcontextprotocol.io) server so AI too
130
130
 
131
131
  ### Setup
132
132
 
133
+ `pmpt-mcp` is included when you install pmpt — no separate installation needed.
134
+
135
+ ```bash
136
+ npm install -g pmpt # pmpt + pmpt-mcp both installed
137
+ ```
138
+
139
+ #### Automatic Setup (Recommended)
140
+
141
+ ```bash
142
+ pmpt mcp-setup
143
+ ```
144
+
145
+ Auto-detects the `pmpt-mcp` binary path, lets you choose your AI tool (Claude Code, Cursor, or `.mcp.json`), and writes the config. Solves PATH issues common with nvm.
146
+
147
+ #### Manual Setup
148
+
133
149
  Add to your `.mcp.json` (or IDE MCP config):
134
150
 
135
151
  ```json
@@ -142,6 +158,8 @@ Add to your `.mcp.json` (or IDE MCP config):
142
158
  }
143
159
  ```
144
160
 
161
+ > **nvm users**: If your AI tool can't find `pmpt-mcp`, use `pmpt mcp-setup` or replace `"pmpt-mcp"` with the absolute path (run `which pmpt-mcp` to find it).
162
+
145
163
  ### Available Tools
146
164
 
147
165
  | Tool | Description |
@@ -0,0 +1,82 @@
1
+ import * as p from '@clack/prompts';
2
+ import { loadAuth } from '../lib/auth.js';
3
+ import { fetchProjects, graduateProject } from '../lib/api.js';
4
+ export async function cmdGraduate() {
5
+ const auth = loadAuth();
6
+ if (!auth?.token || !auth?.username) {
7
+ p.log.error('Login required. Run `pmpt login` first.');
8
+ process.exit(1);
9
+ }
10
+ p.intro('pmpt graduate');
11
+ const s = p.spinner();
12
+ s.start('Loading your projects...');
13
+ let myProjects;
14
+ try {
15
+ const index = await fetchProjects();
16
+ myProjects = index.projects
17
+ .filter((proj) => proj.author === auth.username)
18
+ .filter((proj) => !proj.graduated);
19
+ }
20
+ catch (err) {
21
+ s.stop('Failed to load projects');
22
+ p.log.error(err instanceof Error ? err.message : 'Failed to fetch projects.');
23
+ process.exit(1);
24
+ }
25
+ s.stop('Projects loaded');
26
+ if (myProjects.length === 0) {
27
+ p.log.warn('No eligible projects found. All projects may already be graduated.');
28
+ p.outro('');
29
+ return;
30
+ }
31
+ const slug = await p.select({
32
+ message: 'Select a project to graduate:',
33
+ options: myProjects.map((proj) => ({
34
+ value: proj.slug,
35
+ label: proj.slug,
36
+ hint: proj.description?.slice(0, 50) || '',
37
+ })),
38
+ });
39
+ if (p.isCancel(slug)) {
40
+ p.cancel('Cancelled');
41
+ process.exit(0);
42
+ }
43
+ p.note([
44
+ 'Graduating a project means:',
45
+ ' - The project is archived (no more updates)',
46
+ ' - A graduation badge (🎓) is displayed',
47
+ ' - It appears in the Hall of Fame',
48
+ ' - The .pmpt file remains downloadable',
49
+ '',
50
+ 'This action is intentionally hard to reverse.',
51
+ ].join('\n'), 'What is graduation?');
52
+ const confirm = await p.confirm({
53
+ message: `Graduate "${slug}"? This will archive the project permanently.`,
54
+ initialValue: false,
55
+ });
56
+ if (p.isCancel(confirm) || !confirm) {
57
+ p.cancel('Cancelled');
58
+ process.exit(0);
59
+ }
60
+ const note = await p.text({
61
+ message: 'Graduation note (optional):',
62
+ placeholder: 'e.g., "Reached 1000 users!" or "Acquired by company"',
63
+ });
64
+ if (p.isCancel(note)) {
65
+ p.cancel('Cancelled');
66
+ process.exit(0);
67
+ }
68
+ const s2 = p.spinner();
69
+ s2.start('Graduating...');
70
+ try {
71
+ await graduateProject(auth.token, slug, note || undefined);
72
+ s2.stop('Graduated!');
73
+ p.log.success(`Project "${slug}" has graduated! 🎓`);
74
+ p.log.info('View in Hall of Fame: https://pmptwiki.com/hall-of-fame');
75
+ }
76
+ catch (err) {
77
+ s2.stop('Graduation failed');
78
+ p.log.error(err instanceof Error ? err.message : 'Failed to graduate project.');
79
+ process.exit(1);
80
+ }
81
+ p.outro('');
82
+ }
@@ -1,59 +1,9 @@
1
1
  import * as p from '@clack/prompts';
2
- import { resolve, join, dirname, sep } from 'path';
3
- import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, rmSync } from 'fs';
2
+ import { resolve, join } from 'path';
3
+ import { existsSync, readFileSync, writeFileSync, readdirSync, rmSync } from 'fs';
4
4
  import { isInitialized, getConfigDir, getHistoryDir, getDocsDir, initializeProject } from '../lib/config.js';
5
- import { validatePmptFile, isSafeFilename } from '../lib/pmptFile.js';
6
- /**
7
- * Restore history from .pmpt file
8
- */
9
- function restoreHistory(historyDir, history) {
10
- mkdirSync(historyDir, { recursive: true });
11
- for (const version of history) {
12
- const timestamp = version.timestamp.replace(/[:.]/g, '-').slice(0, 19);
13
- const snapshotName = `v${version.version}-${timestamp}`;
14
- const snapshotDir = join(historyDir, snapshotName);
15
- mkdirSync(snapshotDir, { recursive: true });
16
- // Write files (with path traversal protection)
17
- for (const [filename, content] of Object.entries(version.files)) {
18
- if (!isSafeFilename(filename))
19
- continue;
20
- const filePath = join(snapshotDir, filename);
21
- if (!resolve(filePath).startsWith(resolve(snapshotDir) + sep))
22
- continue;
23
- const fileDir = dirname(filePath);
24
- if (fileDir !== snapshotDir) {
25
- mkdirSync(fileDir, { recursive: true });
26
- }
27
- writeFileSync(filePath, content, 'utf-8');
28
- }
29
- // Write metadata
30
- const metaPath = join(snapshotDir, '.meta.json');
31
- writeFileSync(metaPath, JSON.stringify({
32
- version: version.version,
33
- timestamp: version.timestamp,
34
- files: Object.keys(version.files),
35
- git: version.git,
36
- }, null, 2), 'utf-8');
37
- }
38
- }
39
- /**
40
- * Restore docs from .pmpt file
41
- */
42
- function restoreDocs(docsDir, docs) {
43
- mkdirSync(docsDir, { recursive: true });
44
- for (const [filename, content] of Object.entries(docs)) {
45
- if (!isSafeFilename(filename))
46
- continue;
47
- const filePath = join(docsDir, filename);
48
- if (!resolve(filePath).startsWith(resolve(docsDir) + sep))
49
- continue;
50
- const fileDir = dirname(filePath);
51
- if (fileDir !== docsDir) {
52
- mkdirSync(fileDir, { recursive: true });
53
- }
54
- writeFileSync(filePath, content, 'utf-8');
55
- }
56
- }
5
+ import { validatePmptFile } from '../lib/pmptFile.js';
6
+ import { restoreHistory, restoreDocs } from './clone.js';
57
7
  export async function cmdImport(pmptFile, options) {
58
8
  if (!pmptFile) {
59
9
  p.log.error('Please provide a .pmpt file path.');
@@ -0,0 +1,205 @@
1
+ import * as p from '@clack/prompts';
2
+ import { execSync } from 'child_process';
3
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
4
+ import { join, dirname } from 'path';
5
+ import { homedir } from 'os';
6
+ function detectPmptMcpPath() {
7
+ // Strategy 1: sibling to the current pmpt binary (same bin directory)
8
+ const pmptBin = process.argv[1];
9
+ const siblingPath = join(dirname(pmptBin), 'pmpt-mcp');
10
+ if (existsSync(siblingPath)) {
11
+ return siblingPath;
12
+ }
13
+ // Strategy 2: which / where command
14
+ try {
15
+ const cmd = process.platform === 'win32' ? 'where pmpt-mcp' : 'which pmpt-mcp';
16
+ const result = execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
17
+ const firstLine = result.split('\n')[0].trim();
18
+ if (firstLine && existsSync(firstLine)) {
19
+ return firstLine;
20
+ }
21
+ }
22
+ catch {
23
+ // not found in PATH
24
+ }
25
+ return null;
26
+ }
27
+ function isCommandAvailable(cmd) {
28
+ try {
29
+ const which = process.platform === 'win32' ? 'where' : 'which';
30
+ execSync(`${which} ${cmd}`, { stdio: ['pipe', 'pipe', 'pipe'] });
31
+ return true;
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
37
+ function isClaudeAlreadyConfigured() {
38
+ try {
39
+ const result = execSync('claude mcp list', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
40
+ return result.includes('pmpt');
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ function isJsonConfigured(configPath) {
47
+ try {
48
+ if (!existsSync(configPath))
49
+ return false;
50
+ const content = JSON.parse(readFileSync(configPath, 'utf-8'));
51
+ return !!content?.mcpServers?.pmpt;
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ }
57
+ function configureClaudeCode(mcpBinaryPath) {
58
+ // Remove existing entry first (ignore errors if not found)
59
+ try {
60
+ execSync('claude mcp remove pmpt', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
61
+ }
62
+ catch {
63
+ // not configured yet — that's fine
64
+ }
65
+ execSync(`claude mcp add --transport stdio pmpt -- "${mcpBinaryPath}"`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
66
+ }
67
+ function configureJsonFile(configPath, mcpBinaryPath) {
68
+ let config = {};
69
+ if (existsSync(configPath)) {
70
+ try {
71
+ config = JSON.parse(readFileSync(configPath, 'utf-8'));
72
+ }
73
+ catch {
74
+ config = {};
75
+ }
76
+ }
77
+ else {
78
+ mkdirSync(dirname(configPath), { recursive: true });
79
+ }
80
+ if (!config.mcpServers || typeof config.mcpServers !== 'object') {
81
+ config.mcpServers = {};
82
+ }
83
+ config.mcpServers.pmpt = {
84
+ command: mcpBinaryPath,
85
+ };
86
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
87
+ }
88
+ export async function cmdMcpSetup() {
89
+ p.intro('pmpt mcp-setup');
90
+ // Step 1: Detect pmpt-mcp absolute path
91
+ const s = p.spinner();
92
+ s.start('Detecting pmpt-mcp location...');
93
+ let mcpPath = detectPmptMcpPath();
94
+ if (mcpPath) {
95
+ s.stop(`Found: ${mcpPath}`);
96
+ }
97
+ else {
98
+ s.stop('Could not auto-detect pmpt-mcp path');
99
+ p.log.warn('pmpt-mcp binary not found automatically.');
100
+ p.log.info('Install globally if not yet: npm install -g pmpt');
101
+ const manualPath = await p.text({
102
+ message: 'Enter the absolute path to pmpt-mcp:',
103
+ placeholder: '/usr/local/bin/pmpt-mcp',
104
+ validate: (value) => {
105
+ if (!value.trim())
106
+ return 'Path is required';
107
+ if (!existsSync(value.trim()))
108
+ return `File not found: ${value.trim()}`;
109
+ return undefined;
110
+ },
111
+ });
112
+ if (p.isCancel(manualPath)) {
113
+ p.cancel('Cancelled');
114
+ process.exit(0);
115
+ }
116
+ mcpPath = manualPath.trim();
117
+ }
118
+ // Step 2: Detect available MCP clients
119
+ const claudeAvailable = isCommandAvailable('claude');
120
+ const claudeConfigured = claudeAvailable && isClaudeAlreadyConfigured();
121
+ const cursorConfigPath = join(homedir(), '.cursor', 'mcp.json');
122
+ const cursorDirExists = existsSync(join(homedir(), '.cursor'));
123
+ const cursorConfigured = isJsonConfigured(cursorConfigPath);
124
+ const mcpJsonPath = join(process.cwd(), '.mcp.json');
125
+ const mcpJsonConfigured = isJsonConfigured(mcpJsonPath);
126
+ const options = [];
127
+ if (claudeAvailable) {
128
+ options.push({
129
+ value: 'claude-code',
130
+ label: 'Claude Code',
131
+ hint: claudeConfigured ? 'Already configured — will reconfigure' : 'claude CLI detected',
132
+ configPath: null,
133
+ alreadyConfigured: claudeConfigured,
134
+ });
135
+ }
136
+ if (cursorDirExists) {
137
+ options.push({
138
+ value: 'cursor',
139
+ label: 'Cursor',
140
+ hint: cursorConfigured ? 'Already configured — will reconfigure' : '~/.cursor/mcp.json',
141
+ configPath: cursorConfigPath,
142
+ alreadyConfigured: cursorConfigured,
143
+ });
144
+ }
145
+ options.push({
146
+ value: 'mcp-json',
147
+ label: '.mcp.json (project root)',
148
+ hint: mcpJsonConfigured ? 'Already configured — will reconfigure' : 'Works with any MCP-compatible tool',
149
+ configPath: mcpJsonPath,
150
+ alreadyConfigured: mcpJsonConfigured,
151
+ });
152
+ // Step 3: Select client
153
+ const selected = await p.select({
154
+ message: 'Which MCP client do you want to configure?',
155
+ options: options.map(o => ({
156
+ value: o.value,
157
+ label: o.label,
158
+ hint: o.hint,
159
+ })),
160
+ });
161
+ if (p.isCancel(selected)) {
162
+ p.cancel('Cancelled');
163
+ process.exit(0);
164
+ }
165
+ const client = options.find(o => o.value === selected);
166
+ // Step 4: Configure
167
+ const s2 = p.spinner();
168
+ s2.start(`Configuring ${client.label}...`);
169
+ try {
170
+ switch (client.value) {
171
+ case 'claude-code':
172
+ configureClaudeCode(mcpPath);
173
+ break;
174
+ case 'cursor':
175
+ configureJsonFile(client.configPath, mcpPath);
176
+ break;
177
+ case 'mcp-json':
178
+ configureJsonFile(client.configPath, mcpPath);
179
+ break;
180
+ }
181
+ s2.stop(`${client.label} configured!`);
182
+ }
183
+ catch (err) {
184
+ s2.stop('Configuration failed');
185
+ p.log.error(err instanceof Error ? err.message : 'Failed to write configuration.');
186
+ process.exit(1);
187
+ }
188
+ // Step 5: Summary
189
+ p.log.success(`MCP server registered for ${client.label}`);
190
+ if (client.value === 'claude-code') {
191
+ p.log.info('Registered via claude CLI.');
192
+ }
193
+ else {
194
+ p.log.info(`Config written to: ${client.configPath}`);
195
+ }
196
+ p.note([
197
+ `Binary: ${mcpPath}`,
198
+ `Client: ${client.label}`,
199
+ '',
200
+ 'Available MCP tools:',
201
+ ' pmpt_save, pmpt_status, pmpt_history, pmpt_diff,',
202
+ ' pmpt_quality, pmpt_plan, pmpt_read_context, pmpt_publish',
203
+ ].join('\n'), 'MCP Setup Complete');
204
+ p.outro('Restart your AI tool to activate the MCP server.');
205
+ }
@@ -1,15 +1,99 @@
1
1
  import * as p from '@clack/prompts';
2
- import { resolve, join } from 'path';
3
- import { existsSync, rmSync, writeFileSync, readFileSync } from 'fs';
2
+ import { resolve, join, basename } from 'path';
3
+ import { existsSync, rmSync, renameSync, writeFileSync, readFileSync } from 'fs';
4
4
  import { isInitialized, getHistoryDir } from '../lib/config.js';
5
5
  import { getAllSnapshots } from '../lib/history.js';
6
- export async function cmdSquash(from, to, path) {
7
- const projectPath = path ? resolve(path) : process.cwd();
6
+ export async function cmdSquash(from, to, opts) {
7
+ const projectPath = opts?.path ? resolve(opts.path) : process.cwd();
8
8
  if (!isInitialized(projectPath)) {
9
9
  p.log.error('Project not initialized. Run `pmpt init` first.');
10
10
  process.exit(1);
11
11
  }
12
- // Parse version numbers
12
+ const snapshots = getAllSnapshots(projectPath);
13
+ if (snapshots.length === 0) {
14
+ p.log.error('No snapshots found.');
15
+ process.exit(1);
16
+ }
17
+ if (opts?.auto) {
18
+ return autoSquash(projectPath, snapshots);
19
+ }
20
+ // Manual squash: require from and to
21
+ if (!from || !to) {
22
+ p.log.error('Usage: pmpt squash v2 v5 or pmpt squash --auto');
23
+ process.exit(1);
24
+ }
25
+ return manualSquash(projectPath, snapshots, from, to);
26
+ }
27
+ /**
28
+ * Renumber remaining snapshots to v1, v2, v3... sequentially.
29
+ * Renames directories and updates .meta.json version fields.
30
+ */
31
+ function renumberSnapshots(projectPath) {
32
+ const historyDir = getHistoryDir(projectPath);
33
+ const remaining = getAllSnapshots(projectPath); // sorted by version
34
+ for (let i = 0; i < remaining.length; i++) {
35
+ const snap = remaining[i];
36
+ const newVersion = i + 1;
37
+ if (snap.version === newVersion)
38
+ continue; // already correct
39
+ // Rename directory: v5-20260228T164006 → v2-20260228T164006
40
+ const dirName = basename(snap.snapshotDir);
41
+ const newDirName = dirName.replace(/^v\d+/, `v${newVersion}`);
42
+ const newDir = join(historyDir, newDirName);
43
+ renameSync(snap.snapshotDir, newDir);
44
+ // Update .meta.json
45
+ const metaPath = join(newDir, '.meta.json');
46
+ if (existsSync(metaPath)) {
47
+ const meta = JSON.parse(readFileSync(metaPath, 'utf-8'));
48
+ meta.version = newVersion;
49
+ writeFileSync(metaPath, JSON.stringify(meta, null, 2), 'utf-8');
50
+ }
51
+ }
52
+ }
53
+ async function autoSquash(projectPath, snapshots) {
54
+ // Find empty snapshots (no changed files)
55
+ const emptySnapshots = snapshots.filter(s => s.changedFiles && s.changedFiles.length === 0);
56
+ if (emptySnapshots.length === 0) {
57
+ p.log.info('No empty snapshots found. Nothing to clean up.');
58
+ return;
59
+ }
60
+ p.intro('pmpt squash --auto');
61
+ p.log.info(`Found ${emptySnapshots.length} empty snapshot(s) (no file changes):`);
62
+ for (const s of emptySnapshots) {
63
+ const git = s.git ? ` [${s.git.commit}]` : '';
64
+ p.log.message(` v${s.version} — ${s.timestamp.slice(0, 16)}${git}`);
65
+ }
66
+ const confirm = await p.confirm({
67
+ message: `Delete ${emptySnapshots.length} empty snapshot(s) and renumber?`,
68
+ initialValue: true,
69
+ });
70
+ if (p.isCancel(confirm) || !confirm) {
71
+ p.cancel('Cancelled');
72
+ process.exit(0);
73
+ }
74
+ const sp = p.spinner();
75
+ sp.start('Removing empty snapshots...');
76
+ try {
77
+ let deleted = 0;
78
+ for (const snapshot of emptySnapshots) {
79
+ if (existsSync(snapshot.snapshotDir)) {
80
+ rmSync(snapshot.snapshotDir, { recursive: true });
81
+ deleted++;
82
+ }
83
+ }
84
+ renumberSnapshots(projectPath);
85
+ sp.stop('Cleaned up');
86
+ const remaining = snapshots.length - deleted;
87
+ p.log.success(`Removed ${deleted} empty snapshot(s), renumbered to v1-v${remaining}`);
88
+ }
89
+ catch (error) {
90
+ sp.stop('Failed');
91
+ p.log.error(error.message);
92
+ process.exit(1);
93
+ }
94
+ p.outro('View history: pmpt history');
95
+ }
96
+ async function manualSquash(projectPath, snapshots, from, to) {
13
97
  const fromVersion = parseInt(from.replace(/^v/, ''), 10);
14
98
  const toVersion = parseInt(to.replace(/^v/, ''), 10);
15
99
  if (isNaN(fromVersion) || isNaN(toVersion)) {
@@ -20,11 +104,6 @@ export async function cmdSquash(from, to, path) {
20
104
  p.log.error('First version must be less than second version.');
21
105
  process.exit(1);
22
106
  }
23
- const snapshots = getAllSnapshots(projectPath);
24
- if (snapshots.length === 0) {
25
- p.log.error('No snapshots found.');
26
- process.exit(1);
27
- }
28
107
  const versionList = snapshots.map(s => `v${s.version}`).join(', ');
29
108
  // Find snapshots to squash
30
109
  const toSquash = snapshots.filter(s => s.version >= fromVersion && s.version <= toVersion);
@@ -46,7 +125,6 @@ export async function cmdSquash(from, to, path) {
46
125
  const s = p.spinner();
47
126
  s.start('Squashing versions...');
48
127
  try {
49
- const historyDir = getHistoryDir(projectPath);
50
128
  const keepSnapshot = toSquash[0]; // Keep the first one
51
129
  const deleteSnapshots = toSquash.slice(1); // Delete the rest
52
130
  // Delete the snapshots we're squashing
@@ -64,8 +142,10 @@ export async function cmdSquash(from, to, path) {
64
142
  meta.squashedAt = new Date().toISOString();
65
143
  writeFileSync(metaPath, JSON.stringify(meta, null, 2), 'utf-8');
66
144
  }
145
+ renumberSnapshots(projectPath);
67
146
  s.stop('Squashed');
68
- p.log.success(`Squashed v${fromVersion}-v${toVersion} into v${fromVersion}`);
147
+ const remaining = getAllSnapshots(projectPath);
148
+ p.log.success(`Squashed v${fromVersion}-v${toVersion}, renumbered to v1-v${remaining.length}`);
69
149
  p.log.info(`Deleted ${deleteSnapshots.length} version(s)`);
70
150
  }
71
151
  catch (error) {
package/dist/index.js CHANGED
@@ -37,11 +37,13 @@ import { cmdPublish } from './commands/publish.js';
37
37
  import { cmdUpdate } from './commands/update.js';
38
38
  import { cmdEdit } from './commands/edit.js';
39
39
  import { cmdUnpublish } from './commands/unpublish.js';
40
+ import { cmdGraduate } from './commands/graduate.js';
40
41
  import { cmdClone } from './commands/clone.js';
41
42
  import { cmdExplore } from './commands/browse.js';
42
43
  import { cmdRecover } from './commands/recover.js';
43
44
  import { cmdDiff } from './commands/diff.js';
44
45
  import { cmdInternalSeed } from './commands/internal-seed.js';
46
+ import { cmdMcpSetup } from './commands/mcp-setup.js';
45
47
  import { trackCommand } from './lib/api.js';
46
48
  import { createRequire } from 'module';
47
49
  const require = createRequire(import.meta.url);
@@ -66,6 +68,7 @@ Examples:
66
68
  $ pmpt diff v1 v2 Compare two versions
67
69
  $ pmpt diff v3 Compare v3 to working copy
68
70
  $ pmpt squash v2 v5 Merge versions v2-v5 into v2
71
+ $ pmpt squash --auto Auto-remove empty snapshots
69
72
  $ pmpt export Export as .pmpt file (single JSON)
70
73
  $ pmpt import <file.pmpt> Import from .pmpt file
71
74
  $ pmpt login Authenticate with pmptwiki
@@ -73,10 +76,14 @@ Examples:
73
76
  $ pmpt update Quick re-publish (content only)
74
77
  $ pmpt clone <slug> Clone a project from pmptwiki
75
78
  $ pmpt explore (exp) Explore projects on pmptwiki.com
79
+ $ pmpt graduate Graduate a project (Hall of Fame)
76
80
  $ pmpt recover Recover damaged pmpt.md via AI
81
+ $ pmpt mcp-setup Configure MCP for AI tools
82
+ $ pmpt feedback (fb) Share ideas or report bugs
77
83
 
78
84
  Workflow:
79
85
  init → plan → save → publish Basic publishing flow
86
+ publish → graduate Graduate to Hall of Fame
80
87
  init → plan → watch Continuous development
81
88
  login → publish → update Re-publish with updates
82
89
 
@@ -113,9 +120,11 @@ program
113
120
  .option('-f, --file <name>', 'Compare specific file only')
114
121
  .action(cmdDiff);
115
122
  program
116
- .command('squash <from> <to> [path]')
117
- .description('Squash multiple versions into one (e.g., pmpt squash v2 v5)')
118
- .action(cmdSquash);
123
+ .command('squash [from] [to]')
124
+ .description('Squash versions: pmpt squash v2 v5 or pmpt squash --auto')
125
+ .option('--auto', 'Auto-remove empty snapshots (no file changes)')
126
+ .option('-p, --path <path>', 'Project path')
127
+ .action((from, to, opts) => cmdSquash(from, to, opts));
119
128
  program
120
129
  .command('export [path]')
121
130
  .description('Export project history as a shareable .pmpt file')
@@ -174,6 +183,10 @@ program
174
183
  .command('unpublish')
175
184
  .description('Remove a published project from pmptwiki')
176
185
  .action(cmdUnpublish);
186
+ program
187
+ .command('graduate')
188
+ .description('Graduate a project — archive it with a Hall of Fame badge')
189
+ .action(cmdGraduate);
177
190
  program
178
191
  .command('clone <slug>')
179
192
  .description('Clone a project from pmptwiki platform')
@@ -187,6 +200,43 @@ program
187
200
  .command('recover [path]')
188
201
  .description('Generate a recovery prompt to regenerate pmpt.md via AI')
189
202
  .action(cmdRecover);
203
+ program
204
+ .command('mcp-setup')
205
+ .description('Configure pmpt MCP server for AI tools (Claude Code, Cursor, etc.)')
206
+ .action(cmdMcpSetup);
207
+ program
208
+ .command('feedback')
209
+ .alias('fb')
210
+ .description('Share ideas, request features, or report bugs')
211
+ .action(async () => {
212
+ const prompts = await import('@clack/prompts');
213
+ const { exec } = await import('child_process');
214
+ prompts.intro('pmpt feedback');
215
+ const type = await prompts.select({
216
+ message: 'What would you like to do?',
217
+ options: [
218
+ { value: 'idea', label: 'Suggest a feature or idea' },
219
+ { value: 'bug', label: 'Report a bug' },
220
+ { value: 'question', label: 'Ask a question' },
221
+ { value: 'browse', label: 'Browse existing discussions' },
222
+ ],
223
+ });
224
+ if (prompts.isCancel(type)) {
225
+ prompts.cancel('Cancelled');
226
+ process.exit(0);
227
+ }
228
+ const urls = {
229
+ idea: 'https://github.com/pmptwiki/pmpt-cli/discussions/categories/ideas',
230
+ bug: 'https://github.com/pmptwiki/pmpt-cli/issues/new',
231
+ question: 'https://github.com/pmptwiki/pmpt-cli/discussions/categories/q-a',
232
+ browse: 'https://github.com/pmptwiki/pmpt-cli/discussions',
233
+ };
234
+ const url = urls[type];
235
+ const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
236
+ exec(`${openCmd} "${url}"`);
237
+ prompts.log.success(`Opening ${url}`);
238
+ prompts.outro('Thanks for your feedback!');
239
+ });
190
240
  // Internal automation command (hidden from help)
191
241
  program
192
242
  .command('internal-seed', { hidden: true })
package/dist/lib/api.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * pmptwiki API client
3
3
  */
4
- const API_BASE = 'https://pmptwiki-api.sin2da.workers.dev';
4
+ const API_BASE = 'https://pmptwiki-api.raunplaymore.workers.dev';
5
5
  const R2_PUBLIC_URL = 'https://pub-ce73b2410943490d80b60ddad9243d31.r2.dev';
6
6
  export async function requestDeviceCode() {
7
7
  const res = await fetch(`${API_BASE}/auth/device`, {
@@ -74,6 +74,31 @@ export async function unpublishProject(token, slug) {
74
74
  throw new Error(err.error);
75
75
  }
76
76
  }
77
+ export async function graduateProject(token, slug, note) {
78
+ const res = await fetch(`${API_BASE}/graduate/${slug}`, {
79
+ method: 'POST',
80
+ headers: {
81
+ Authorization: `Bearer ${token}`,
82
+ 'Content-Type': 'application/json',
83
+ },
84
+ body: JSON.stringify({ note }),
85
+ });
86
+ if (!res.ok) {
87
+ const err = await res.json().catch(() => ({ error: 'Graduate failed' }));
88
+ throw new Error(err.error);
89
+ }
90
+ return res.json();
91
+ }
92
+ export async function ungraduateProject(token, slug) {
93
+ const res = await fetch(`${API_BASE}/graduate/${slug}`, {
94
+ method: 'DELETE',
95
+ headers: { Authorization: `Bearer ${token}` },
96
+ });
97
+ if (!res.ok) {
98
+ const err = await res.json().catch(() => ({ error: 'Ungraduate failed' }));
99
+ throw new Error(err.error);
100
+ }
101
+ }
77
102
  export async function fetchPmptFile(slug) {
78
103
  const res = await fetch(`${R2_PUBLIC_URL}/projects/${slug}.pmpt`);
79
104
  if (!res.ok) {
package/dist/mcp.js CHANGED
@@ -9,15 +9,18 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9
9
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
10
10
  import { z } from 'zod';
11
11
  import { resolve, join } from 'path';
12
- import { existsSync, readFileSync } from 'fs';
12
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
13
13
  import glob from 'fast-glob';
14
14
  import { createRequire } from 'module';
15
15
  import { isInitialized, loadConfig, getDocsDir } from './lib/config.js';
16
16
  import { createFullSnapshot, getAllSnapshots, getTrackedFiles, resolveFullSnapshot } from './lib/history.js';
17
17
  import { computeQuality } from './lib/quality.js';
18
- import { getPlanProgress } from './lib/plan.js';
18
+ import { getPlanProgress, savePlanProgress, savePlanDocuments, PLAN_QUESTIONS } from './lib/plan.js';
19
19
  import { isGitRepo } from './lib/git.js';
20
20
  import { diffSnapshots } from './lib/diff.js';
21
+ import { loadAuth } from './lib/auth.js';
22
+ import { publishProject, graduateProject } from './lib/api.js';
23
+ import { createPmptFile } from './lib/pmptFile.js';
21
24
  const require = createRequire(import.meta.url);
22
25
  const { version } = require('../package.json');
23
26
  // ── Server ──────────────────────────────────────────
@@ -95,7 +98,10 @@ function formatDiffs(diffs) {
95
98
  return lines.join('\n');
96
99
  }
97
100
  // ── 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 }) => {
101
+ server.tool('pmpt_save', 'Save a snapshot of .pmpt/docs/ files. Call after completing features, fixes, or milestones. IMPORTANT: Always provide a summary describing what was accomplished this gets recorded in the project development log.', {
102
+ projectPath: z.string().optional().describe('Project root path. Defaults to cwd.'),
103
+ summary: z.string().optional().describe('What was accomplished since the last save. This is recorded in pmpt.md as a development log entry. Examples: "Implemented user auth with JWT", "Fixed responsive layout on mobile", "Added search filtering by category".'),
104
+ }, async ({ projectPath, summary }) => {
99
105
  try {
100
106
  const pp = resolveProjectPath(projectPath);
101
107
  assertInitialized(pp);
@@ -103,13 +109,37 @@ server.tool('pmpt_save', 'Save a snapshot of .pmpt/docs/ files. Call after compl
103
109
  if (tracked.length === 0) {
104
110
  return { content: [{ type: 'text', text: 'No files to save. Add .md files to .pmpt/docs/ first.' }] };
105
111
  }
106
- const entry = createFullSnapshot(pp);
112
+ // Auto-update pmpt.md with summary before snapshot
113
+ if (summary) {
114
+ const docsDir = getDocsDir(pp);
115
+ const pmptMdPath = join(docsDir, 'pmpt.md');
116
+ if (existsSync(pmptMdPath)) {
117
+ let content = readFileSync(pmptMdPath, 'utf-8');
118
+ const snapshots = getAllSnapshots(pp);
119
+ const nextVersion = snapshots.length + 1;
120
+ const date = new Date().toISOString().slice(0, 10);
121
+ const entry = `\n### v${nextVersion} — ${date}\n- ${summary}\n`;
122
+ const logIndex = content.indexOf('## Snapshot Log');
123
+ if (logIndex !== -1) {
124
+ const afterHeader = content.indexOf('\n', logIndex);
125
+ const nextSection = content.indexOf('\n## ', afterHeader + 1);
126
+ const insertPos = nextSection !== -1 ? nextSection : content.length;
127
+ content = content.slice(0, insertPos) + entry + content.slice(insertPos);
128
+ }
129
+ else {
130
+ content += `\n## Snapshot Log${entry}`;
131
+ }
132
+ writeFileSync(pmptMdPath, content, 'utf-8');
133
+ }
134
+ }
135
+ const entry = createFullSnapshot(pp, summary ? { note: summary } : undefined);
107
136
  const changedCount = entry.changedFiles?.length ?? entry.files.length;
108
137
  return {
109
138
  content: [{
110
139
  type: 'text',
111
140
  text: [
112
141
  `Snapshot v${entry.version} saved (${changedCount} changed, ${entry.files.length - changedCount} unchanged).`,
142
+ summary ? `Summary: ${summary}` : '',
113
143
  '',
114
144
  `Files: ${entry.files.join(', ')}`,
115
145
  entry.changedFiles ? `Changed: ${entry.changedFiles.join(', ')}` : '',
@@ -235,6 +265,385 @@ server.tool('pmpt_quality', 'Check project quality score and publish readiness.'
235
265
  return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
236
266
  }
237
267
  });
268
+ server.tool('pmpt_plan_questions', 'Get the planning questions to ask the user. Call this FIRST when a user wants to build something, then ask each question conversationally. After collecting all answers, call pmpt_plan to generate the project.', { projectPath: z.string().optional().describe('Project root path. Defaults to cwd.') }, async ({ projectPath }) => {
269
+ try {
270
+ const pp = resolveProjectPath(projectPath);
271
+ assertInitialized(pp);
272
+ const existing = getPlanProgress(pp);
273
+ const lines = [
274
+ 'Ask the user these questions one by one in a natural, conversational way.',
275
+ 'You may skip questions the user has already answered in the conversation.',
276
+ 'Required questions are marked with *.',
277
+ '',
278
+ ];
279
+ for (const q of PLAN_QUESTIONS) {
280
+ lines.push(`${q.required ? '*' : ' '} ${q.key}: ${q.question}`);
281
+ if (q.placeholder)
282
+ lines.push(` hint: ${q.placeholder}`);
283
+ }
284
+ if (existing?.completed && existing.answers) {
285
+ lines.push('');
286
+ lines.push('NOTE: A plan already exists. Current answers:');
287
+ for (const q of PLAN_QUESTIONS) {
288
+ const val = existing.answers[q.key];
289
+ if (val)
290
+ lines.push(` ${q.key}: ${val}`);
291
+ }
292
+ lines.push('Ask if the user wants to update or start fresh.');
293
+ }
294
+ lines.push('');
295
+ lines.push('After collecting answers, call pmpt_plan with the collected answers.');
296
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
297
+ }
298
+ catch (error) {
299
+ return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
300
+ }
301
+ });
302
+ server.tool('pmpt_plan', 'Finalize the plan: submit collected answers to generate AI prompt and project docs (plan.md, pmpt.md, pmpt.ai.md). Call pmpt_plan_questions first to get the questions, ask the user conversationally, then call this with the answers.', {
303
+ projectPath: z.string().optional().describe('Project root path. Defaults to cwd.'),
304
+ projectName: z.string().describe('Project name (e.g. "my-awesome-app").'),
305
+ productIdea: z.string().describe('What to build with AI — the core product idea.'),
306
+ coreFeatures: z.string().describe('Key features, separated by commas or semicolons.'),
307
+ additionalContext: z.string().optional().describe('Any extra context AI should know.'),
308
+ techStack: z.string().optional().describe('Preferred tech stack. AI will suggest if omitted.'),
309
+ }, async ({ projectPath, projectName, productIdea, coreFeatures, additionalContext, techStack }) => {
310
+ try {
311
+ const pp = resolveProjectPath(projectPath);
312
+ assertInitialized(pp);
313
+ const answers = {
314
+ projectName,
315
+ productIdea,
316
+ coreFeatures,
317
+ additionalContext: additionalContext || '',
318
+ techStack: techStack || '',
319
+ };
320
+ // Save plan progress
321
+ savePlanProgress(pp, {
322
+ completed: true,
323
+ startedAt: new Date().toISOString(),
324
+ updatedAt: new Date().toISOString(),
325
+ answers,
326
+ });
327
+ // Generate and save plan documents (plan.md, pmpt.md, pmpt.ai.md) + initial snapshot
328
+ const result = savePlanDocuments(pp, answers);
329
+ return {
330
+ content: [{
331
+ type: 'text',
332
+ text: [
333
+ `Plan completed for "${projectName}"!`,
334
+ '',
335
+ `Generated files:`,
336
+ ` - ${result.planPath} (project plan)`,
337
+ ` - ${result.promptPath} (AI instruction — paste into AI tool)`,
338
+ '',
339
+ `Questions answered:`,
340
+ ...PLAN_QUESTIONS.map(q => ` ${q.key}: ${answers[q.key] || '(skipped)'}`),
341
+ '',
342
+ `The AI prompt in pmpt.ai.md contains your development instructions.`,
343
+ `You can now start building based on the plan. Run pmpt_save after milestones.`,
344
+ ].join('\n'),
345
+ }],
346
+ };
347
+ }
348
+ catch (error) {
349
+ return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
350
+ }
351
+ });
352
+ server.tool('pmpt_read_context', 'Read project context to understand current state. Call this at the START of a new session to resume where you left off. Returns plan answers, docs content, recent history, and quality score.', {
353
+ projectPath: z.string().optional().describe('Project root path. Defaults to cwd.'),
354
+ includeDocsContent: z.boolean().optional().describe('Include full content of docs files. Defaults to true.'),
355
+ }, async ({ projectPath, includeDocsContent }) => {
356
+ try {
357
+ const pp = resolveProjectPath(projectPath);
358
+ assertInitialized(pp);
359
+ const config = loadConfig(pp);
360
+ const planProgress = getPlanProgress(pp);
361
+ const tracked = getTrackedFiles(pp);
362
+ const snapshots = getAllSnapshots(pp);
363
+ const quality = computeQuality(buildQualityInput(pp));
364
+ const docsDir = getDocsDir(pp);
365
+ const lines = [];
366
+ // Project overview
367
+ const projectName = planProgress?.answers?.projectName || config?.lastPublishedSlug || '(unknown)';
368
+ lines.push(`# Project: ${projectName}`);
369
+ lines.push(`Quality: ${quality.score}/100 (${quality.grade}) | Snapshots: ${snapshots.length} | Files: ${tracked.length}`);
370
+ if (config?.lastPublished)
371
+ lines.push(`Last published: ${config.lastPublished.slice(0, 10)}`);
372
+ lines.push('');
373
+ // Plan answers
374
+ if (planProgress?.completed && planProgress.answers) {
375
+ lines.push('## Plan');
376
+ for (const q of PLAN_QUESTIONS) {
377
+ const val = planProgress.answers[q.key];
378
+ if (val)
379
+ lines.push(`${q.key}: ${val}`);
380
+ }
381
+ lines.push('');
382
+ }
383
+ // Docs content
384
+ if (includeDocsContent !== false) {
385
+ lines.push('## Docs');
386
+ for (const file of tracked) {
387
+ const filePath = join(docsDir, file);
388
+ if (existsSync(filePath)) {
389
+ const content = readFileSync(filePath, 'utf-8');
390
+ lines.push(`### ${file}`);
391
+ lines.push(content);
392
+ lines.push('');
393
+ }
394
+ }
395
+ }
396
+ // Recent history (last 5)
397
+ if (snapshots.length > 0) {
398
+ lines.push('## Recent History');
399
+ const recent = snapshots.slice(-5);
400
+ for (const s of recent) {
401
+ const changed = s.changedFiles?.length ?? s.files.length;
402
+ const git = s.git ? ` [${s.git.commit}]` : '';
403
+ lines.push(`v${s.version} — ${s.timestamp.slice(0, 16)} — ${changed} changed${git}`);
404
+ }
405
+ lines.push('');
406
+ }
407
+ // Quality details
408
+ const failing = quality.details.filter(d => d.score < d.maxScore);
409
+ if (failing.length > 0) {
410
+ lines.push('## Improvement Areas');
411
+ for (const d of failing) {
412
+ lines.push(`- ${d.label}: ${d.score}/${d.maxScore}${d.tip ? ` — ${d.tip}` : ''}`);
413
+ }
414
+ }
415
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
416
+ }
417
+ catch (error) {
418
+ return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
419
+ }
420
+ });
421
+ server.tool('pmpt_update_doc', 'Update pmpt.md: check off completed features, add progress notes, or append content. Use this after completing work to keep the project document up to date.', {
422
+ projectPath: z.string().optional().describe('Project root path. Defaults to cwd.'),
423
+ completedFeatures: z.array(z.string()).optional().describe('Feature names to mark as done (matches against checkbox items in pmpt.md).'),
424
+ progressNote: z.string().optional().describe('Progress note to append to the Snapshot Log section.'),
425
+ snapshotVersion: z.string().optional().describe('Version label for the snapshot log entry (e.g. "v3 - Auth Complete"). Auto-generated if omitted.'),
426
+ }, async ({ projectPath, completedFeatures, progressNote, snapshotVersion }) => {
427
+ try {
428
+ const pp = resolveProjectPath(projectPath);
429
+ assertInitialized(pp);
430
+ const docsDir = getDocsDir(pp);
431
+ const pmptMdPath = join(docsDir, 'pmpt.md');
432
+ if (!existsSync(pmptMdPath)) {
433
+ return { content: [{ type: 'text', text: 'pmpt.md not found. Run pmpt_plan first to generate project docs.' }], isError: true };
434
+ }
435
+ let content = readFileSync(pmptMdPath, 'utf-8');
436
+ const changes = [];
437
+ // Check off completed features
438
+ if (completedFeatures && completedFeatures.length > 0) {
439
+ for (const feature of completedFeatures) {
440
+ const pattern = new RegExp(`- \\[ \\] (.*${feature.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}.*)`, 'i');
441
+ const match = content.match(pattern);
442
+ if (match) {
443
+ content = content.replace(match[0], `- [x] ${match[1]}`);
444
+ changes.push(`Checked: ${match[1]}`);
445
+ }
446
+ }
447
+ }
448
+ // Add progress note to Snapshot Log
449
+ if (progressNote) {
450
+ const snapshots = getAllSnapshots(pp);
451
+ const label = snapshotVersion || `v${snapshots.length} - Progress`;
452
+ const entry = `\n### ${label}\n- ${progressNote}\n`;
453
+ const logIndex = content.indexOf('## Snapshot Log');
454
+ if (logIndex !== -1) {
455
+ // Find the end of the Snapshot Log header line
456
+ const afterHeader = content.indexOf('\n', logIndex);
457
+ // Find the next ## section or end of file
458
+ const nextSection = content.indexOf('\n## ', afterHeader + 1);
459
+ const insertPos = nextSection !== -1 ? nextSection : content.length;
460
+ content = content.slice(0, insertPos) + entry + content.slice(insertPos);
461
+ }
462
+ else {
463
+ content += `\n## Snapshot Log${entry}`;
464
+ }
465
+ changes.push(`Added log: ${label}`);
466
+ }
467
+ if (changes.length === 0) {
468
+ return { content: [{ type: 'text', text: 'No changes to make. Provide completedFeatures or progressNote.' }] };
469
+ }
470
+ writeFileSync(pmptMdPath, content, 'utf-8');
471
+ return {
472
+ content: [{
473
+ type: 'text',
474
+ text: [`Updated pmpt.md:`, ...changes.map(c => ` - ${c}`), '', 'Run pmpt_save to create a snapshot.'].join('\n'),
475
+ }],
476
+ };
477
+ }
478
+ catch (error) {
479
+ return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
480
+ }
481
+ });
482
+ server.tool('pmpt_log_decision', 'Record an architectural or technical decision in pmpt.md. Use this when making important choices (tech stack, library selection, design patterns, etc.) so the reasoning is preserved.', {
483
+ projectPath: z.string().optional().describe('Project root path. Defaults to cwd.'),
484
+ title: z.string().describe('Short decision title (e.g. "Database: SQLite over PostgreSQL").'),
485
+ reasoning: z.string().describe('Why this decision was made.'),
486
+ }, async ({ projectPath, title, reasoning }) => {
487
+ try {
488
+ const pp = resolveProjectPath(projectPath);
489
+ assertInitialized(pp);
490
+ const docsDir = getDocsDir(pp);
491
+ const pmptMdPath = join(docsDir, 'pmpt.md');
492
+ if (!existsSync(pmptMdPath)) {
493
+ return { content: [{ type: 'text', text: 'pmpt.md not found. Run pmpt_plan first.' }], isError: true };
494
+ }
495
+ let content = readFileSync(pmptMdPath, 'utf-8');
496
+ const date = new Date().toISOString().slice(0, 10);
497
+ const entry = `- **${title}** — ${reasoning} _(${date})_\n`;
498
+ const decisionsIndex = content.indexOf('## Decisions');
499
+ if (decisionsIndex !== -1) {
500
+ const afterHeader = content.indexOf('\n', decisionsIndex);
501
+ const nextSection = content.indexOf('\n## ', afterHeader + 1);
502
+ const insertPos = nextSection !== -1 ? nextSection : content.length;
503
+ content = content.slice(0, insertPos) + entry + content.slice(insertPos);
504
+ }
505
+ else {
506
+ // Insert before Snapshot Log if it exists, otherwise append
507
+ const logIndex = content.indexOf('## Snapshot Log');
508
+ if (logIndex !== -1) {
509
+ content = content.slice(0, logIndex) + `## Decisions\n${entry}\n` + content.slice(logIndex);
510
+ }
511
+ else {
512
+ content += `\n## Decisions\n${entry}`;
513
+ }
514
+ }
515
+ writeFileSync(pmptMdPath, content, 'utf-8');
516
+ return {
517
+ content: [{
518
+ type: 'text',
519
+ text: `Decision recorded: ${title}\nReasoning: ${reasoning}`,
520
+ }],
521
+ };
522
+ }
523
+ catch (error) {
524
+ return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
525
+ }
526
+ });
527
+ server.tool('pmpt_publish', 'Publish the project to pmptwiki.com. Requires prior login via `pmpt login` in CLI. Packages project docs and history into a .pmpt file and uploads it.', {
528
+ projectPath: z.string().optional().describe('Project root path. Defaults to cwd.'),
529
+ slug: z.string().describe('Project slug (3-50 chars, lowercase alphanumeric and hyphens).'),
530
+ description: z.string().optional().describe('Project description (max 500 chars).'),
531
+ tags: z.array(z.string()).optional().describe('Tags for the project (max 10).'),
532
+ category: z.string().optional().describe('Category: web-app, mobile-app, cli-tool, api-backend, ai-ml, game, library, other.'),
533
+ productUrl: z.string().optional().describe('Product URL (GitHub repo or live site).'),
534
+ productUrlType: z.enum(['git', 'url']).optional().describe('Type of product URL.'),
535
+ }, async ({ projectPath, slug, description, tags, category, productUrl, productUrlType }) => {
536
+ try {
537
+ const pp = resolveProjectPath(projectPath);
538
+ assertInitialized(pp);
539
+ // Check auth
540
+ const auth = loadAuth();
541
+ if (!auth) {
542
+ return { content: [{ type: 'text', text: 'Not logged in. Run `pmpt login` in the terminal first.' }], isError: true };
543
+ }
544
+ // Build .pmpt file content
545
+ const config = loadConfig(pp);
546
+ const planProgress = getPlanProgress(pp);
547
+ const snapshots = getAllSnapshots(pp);
548
+ const docsDir = getDocsDir(pp);
549
+ const quality = computeQuality(buildQualityInput(pp));
550
+ if (quality.score < 40) {
551
+ return {
552
+ content: [{
553
+ type: 'text',
554
+ text: `Quality score ${quality.score}/100 is below minimum (40). Improve your project before publishing.\n\n${quality.details.filter(d => d.score < d.maxScore).map(d => `- ${d.label}: ${d.tip}`).join('\n')}`,
555
+ }],
556
+ isError: true,
557
+ };
558
+ }
559
+ const projectName = planProgress?.answers?.projectName || config?.lastPublishedSlug || slug;
560
+ // Build versions
561
+ const history = snapshots.map((s, i) => {
562
+ const files = resolveFullSnapshot(snapshots, i);
563
+ return {
564
+ version: s.version,
565
+ timestamp: s.timestamp,
566
+ files,
567
+ changedFiles: s.changedFiles,
568
+ note: s.note,
569
+ git: s.git,
570
+ };
571
+ });
572
+ // Build docs
573
+ const docs = {};
574
+ const tracked = getTrackedFiles(pp);
575
+ for (const file of tracked) {
576
+ const filePath = join(docsDir, file);
577
+ if (existsSync(filePath)) {
578
+ docs[file] = readFileSync(filePath, 'utf-8');
579
+ }
580
+ }
581
+ const meta = {
582
+ projectName,
583
+ createdAt: snapshots[0]?.timestamp || new Date().toISOString(),
584
+ exportedAt: new Date().toISOString(),
585
+ description: description || '',
586
+ };
587
+ const planAnswers = planProgress?.answers
588
+ ? planProgress.answers
589
+ : undefined;
590
+ const pmptContent = createPmptFile(meta, planAnswers, docs, history);
591
+ // Publish
592
+ const result = await publishProject(auth.token, {
593
+ slug,
594
+ pmptContent,
595
+ description: description || '',
596
+ tags: tags || [],
597
+ category,
598
+ productUrl,
599
+ productUrlType,
600
+ });
601
+ return {
602
+ content: [{
603
+ type: 'text',
604
+ text: [
605
+ `Published "${projectName}" to pmptwiki!`,
606
+ '',
607
+ `URL: ${result.url}`,
608
+ `Download: ${result.downloadUrl}`,
609
+ `Slug: ${slug}`,
610
+ `Author: @${auth.username}`,
611
+ ].join('\n'),
612
+ }],
613
+ };
614
+ }
615
+ catch (error) {
616
+ return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
617
+ }
618
+ });
619
+ server.tool('pmpt_graduate', 'Graduate a project on pmptwiki — archives it with a Hall of Fame badge. The project can no longer be updated. Requires prior login via `pmpt login`.', {
620
+ slug: z.string().describe('Project slug to graduate.'),
621
+ note: z.string().optional().describe('Graduation note (e.g., "Reached 1000 users!").'),
622
+ }, async ({ slug, note }) => {
623
+ try {
624
+ const auth = loadAuth();
625
+ if (!auth) {
626
+ return { content: [{ type: 'text', text: 'Not logged in. Run `pmpt login` in the terminal first.' }], isError: true };
627
+ }
628
+ const result = await graduateProject(auth.token, slug, note);
629
+ return {
630
+ content: [{
631
+ type: 'text',
632
+ text: [
633
+ `Project "${slug}" has graduated! 🎓`,
634
+ '',
635
+ `Graduated at: ${result.graduatedAt}`,
636
+ `Hall of Fame: https://pmptwiki.com/hall-of-fame`,
637
+ '',
638
+ 'The project is now archived. No further updates are allowed.',
639
+ ].join('\n'),
640
+ }],
641
+ };
642
+ }
643
+ catch (error) {
644
+ return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
645
+ }
646
+ });
238
647
  // ── Start ───────────────────────────────────────────
239
648
  async function main() {
240
649
  const transport = new StdioServerTransport();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmpt-cli",
3
- "version": "1.12.4",
3
+ "version": "1.14.0",
4
4
  "description": "Record and share your AI-driven product development journey",
5
5
  "type": "module",
6
6
  "bin": {