pmpt-cli 1.12.3 → 1.13.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
+ }
package/dist/index.js CHANGED
@@ -37,19 +37,22 @@ 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);
48
50
  const { version } = require('../package.json');
49
51
  const program = new Command();
50
52
  // Track every command invocation (fire-and-forget)
51
- program.hook('preAction', (thisCommand) => {
52
- trackCommand(thisCommand.name());
53
+ program.hook('preAction', (thisCommand, actionCommand) => {
54
+ const commandName = actionCommand?.name() || thisCommand.name();
55
+ trackCommand(commandName);
53
56
  });
54
57
  program
55
58
  .name('pmpt')
@@ -72,10 +75,14 @@ Examples:
72
75
  $ pmpt update Quick re-publish (content only)
73
76
  $ pmpt clone <slug> Clone a project from pmptwiki
74
77
  $ pmpt explore (exp) Explore projects on pmptwiki.com
78
+ $ pmpt graduate Graduate a project (Hall of Fame)
75
79
  $ pmpt recover Recover damaged pmpt.md via AI
80
+ $ pmpt mcp-setup Configure MCP for AI tools
81
+ $ pmpt feedback (fb) Share ideas or report bugs
76
82
 
77
83
  Workflow:
78
84
  init → plan → save → publish Basic publishing flow
85
+ publish → graduate Graduate to Hall of Fame
79
86
  init → plan → watch Continuous development
80
87
  login → publish → update Re-publish with updates
81
88
 
@@ -173,6 +180,10 @@ program
173
180
  .command('unpublish')
174
181
  .description('Remove a published project from pmptwiki')
175
182
  .action(cmdUnpublish);
183
+ program
184
+ .command('graduate')
185
+ .description('Graduate a project — archive it with a Hall of Fame badge')
186
+ .action(cmdGraduate);
176
187
  program
177
188
  .command('clone <slug>')
178
189
  .description('Clone a project from pmptwiki platform')
@@ -186,6 +197,43 @@ program
186
197
  .command('recover [path]')
187
198
  .description('Generate a recovery prompt to regenerate pmpt.md via AI')
188
199
  .action(cmdRecover);
200
+ program
201
+ .command('mcp-setup')
202
+ .description('Configure pmpt MCP server for AI tools (Claude Code, Cursor, etc.)')
203
+ .action(cmdMcpSetup);
204
+ program
205
+ .command('feedback')
206
+ .alias('fb')
207
+ .description('Share ideas, request features, or report bugs')
208
+ .action(async () => {
209
+ const prompts = await import('@clack/prompts');
210
+ const { exec } = await import('child_process');
211
+ prompts.intro('pmpt feedback');
212
+ const type = await prompts.select({
213
+ message: 'What would you like to do?',
214
+ options: [
215
+ { value: 'idea', label: 'Suggest a feature or idea' },
216
+ { value: 'bug', label: 'Report a bug' },
217
+ { value: 'question', label: 'Ask a question' },
218
+ { value: 'browse', label: 'Browse existing discussions' },
219
+ ],
220
+ });
221
+ if (prompts.isCancel(type)) {
222
+ prompts.cancel('Cancelled');
223
+ process.exit(0);
224
+ }
225
+ const urls = {
226
+ idea: 'https://github.com/pmptwiki/pmpt-cli/discussions/categories/ideas',
227
+ bug: 'https://github.com/pmptwiki/pmpt-cli/issues/new',
228
+ question: 'https://github.com/pmptwiki/pmpt-cli/discussions/categories/q-a',
229
+ browse: 'https://github.com/pmptwiki/pmpt-cli/discussions',
230
+ };
231
+ const url = urls[type];
232
+ const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
233
+ exec(`${openCmd} "${url}"`);
234
+ prompts.log.success(`Opening ${url}`);
235
+ prompts.outro('Thanks for your feedback!');
236
+ });
189
237
  // Internal automation command (hidden from help)
190
238
  program
191
239
  .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 ──────────────────────────────────────────
@@ -235,6 +238,385 @@ server.tool('pmpt_quality', 'Check project quality score and publish readiness.'
235
238
  return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
236
239
  }
237
240
  });
241
+ 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 }) => {
242
+ try {
243
+ const pp = resolveProjectPath(projectPath);
244
+ assertInitialized(pp);
245
+ const existing = getPlanProgress(pp);
246
+ const lines = [
247
+ 'Ask the user these questions one by one in a natural, conversational way.',
248
+ 'You may skip questions the user has already answered in the conversation.',
249
+ 'Required questions are marked with *.',
250
+ '',
251
+ ];
252
+ for (const q of PLAN_QUESTIONS) {
253
+ lines.push(`${q.required ? '*' : ' '} ${q.key}: ${q.question}`);
254
+ if (q.placeholder)
255
+ lines.push(` hint: ${q.placeholder}`);
256
+ }
257
+ if (existing?.completed && existing.answers) {
258
+ lines.push('');
259
+ lines.push('NOTE: A plan already exists. Current answers:');
260
+ for (const q of PLAN_QUESTIONS) {
261
+ const val = existing.answers[q.key];
262
+ if (val)
263
+ lines.push(` ${q.key}: ${val}`);
264
+ }
265
+ lines.push('Ask if the user wants to update or start fresh.');
266
+ }
267
+ lines.push('');
268
+ lines.push('After collecting answers, call pmpt_plan with the collected answers.');
269
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
270
+ }
271
+ catch (error) {
272
+ return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
273
+ }
274
+ });
275
+ 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.', {
276
+ projectPath: z.string().optional().describe('Project root path. Defaults to cwd.'),
277
+ projectName: z.string().describe('Project name (e.g. "my-awesome-app").'),
278
+ productIdea: z.string().describe('What to build with AI — the core product idea.'),
279
+ coreFeatures: z.string().describe('Key features, separated by commas or semicolons.'),
280
+ additionalContext: z.string().optional().describe('Any extra context AI should know.'),
281
+ techStack: z.string().optional().describe('Preferred tech stack. AI will suggest if omitted.'),
282
+ }, async ({ projectPath, projectName, productIdea, coreFeatures, additionalContext, techStack }) => {
283
+ try {
284
+ const pp = resolveProjectPath(projectPath);
285
+ assertInitialized(pp);
286
+ const answers = {
287
+ projectName,
288
+ productIdea,
289
+ coreFeatures,
290
+ additionalContext: additionalContext || '',
291
+ techStack: techStack || '',
292
+ };
293
+ // Save plan progress
294
+ savePlanProgress(pp, {
295
+ completed: true,
296
+ startedAt: new Date().toISOString(),
297
+ updatedAt: new Date().toISOString(),
298
+ answers,
299
+ });
300
+ // Generate and save plan documents (plan.md, pmpt.md, pmpt.ai.md) + initial snapshot
301
+ const result = savePlanDocuments(pp, answers);
302
+ return {
303
+ content: [{
304
+ type: 'text',
305
+ text: [
306
+ `Plan completed for "${projectName}"!`,
307
+ '',
308
+ `Generated files:`,
309
+ ` - ${result.planPath} (project plan)`,
310
+ ` - ${result.promptPath} (AI instruction — paste into AI tool)`,
311
+ '',
312
+ `Questions answered:`,
313
+ ...PLAN_QUESTIONS.map(q => ` ${q.key}: ${answers[q.key] || '(skipped)'}`),
314
+ '',
315
+ `The AI prompt in pmpt.ai.md contains your development instructions.`,
316
+ `You can now start building based on the plan. Run pmpt_save after milestones.`,
317
+ ].join('\n'),
318
+ }],
319
+ };
320
+ }
321
+ catch (error) {
322
+ return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
323
+ }
324
+ });
325
+ 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.', {
326
+ projectPath: z.string().optional().describe('Project root path. Defaults to cwd.'),
327
+ includeDocsContent: z.boolean().optional().describe('Include full content of docs files. Defaults to true.'),
328
+ }, async ({ projectPath, includeDocsContent }) => {
329
+ try {
330
+ const pp = resolveProjectPath(projectPath);
331
+ assertInitialized(pp);
332
+ const config = loadConfig(pp);
333
+ const planProgress = getPlanProgress(pp);
334
+ const tracked = getTrackedFiles(pp);
335
+ const snapshots = getAllSnapshots(pp);
336
+ const quality = computeQuality(buildQualityInput(pp));
337
+ const docsDir = getDocsDir(pp);
338
+ const lines = [];
339
+ // Project overview
340
+ const projectName = planProgress?.answers?.projectName || config?.lastPublishedSlug || '(unknown)';
341
+ lines.push(`# Project: ${projectName}`);
342
+ lines.push(`Quality: ${quality.score}/100 (${quality.grade}) | Snapshots: ${snapshots.length} | Files: ${tracked.length}`);
343
+ if (config?.lastPublished)
344
+ lines.push(`Last published: ${config.lastPublished.slice(0, 10)}`);
345
+ lines.push('');
346
+ // Plan answers
347
+ if (planProgress?.completed && planProgress.answers) {
348
+ lines.push('## Plan');
349
+ for (const q of PLAN_QUESTIONS) {
350
+ const val = planProgress.answers[q.key];
351
+ if (val)
352
+ lines.push(`${q.key}: ${val}`);
353
+ }
354
+ lines.push('');
355
+ }
356
+ // Docs content
357
+ if (includeDocsContent !== false) {
358
+ lines.push('## Docs');
359
+ for (const file of tracked) {
360
+ const filePath = join(docsDir, file);
361
+ if (existsSync(filePath)) {
362
+ const content = readFileSync(filePath, 'utf-8');
363
+ lines.push(`### ${file}`);
364
+ lines.push(content);
365
+ lines.push('');
366
+ }
367
+ }
368
+ }
369
+ // Recent history (last 5)
370
+ if (snapshots.length > 0) {
371
+ lines.push('## Recent History');
372
+ const recent = snapshots.slice(-5);
373
+ for (const s of recent) {
374
+ const changed = s.changedFiles?.length ?? s.files.length;
375
+ const git = s.git ? ` [${s.git.commit}]` : '';
376
+ lines.push(`v${s.version} — ${s.timestamp.slice(0, 16)} — ${changed} changed${git}`);
377
+ }
378
+ lines.push('');
379
+ }
380
+ // Quality details
381
+ const failing = quality.details.filter(d => d.score < d.maxScore);
382
+ if (failing.length > 0) {
383
+ lines.push('## Improvement Areas');
384
+ for (const d of failing) {
385
+ lines.push(`- ${d.label}: ${d.score}/${d.maxScore}${d.tip ? ` — ${d.tip}` : ''}`);
386
+ }
387
+ }
388
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
389
+ }
390
+ catch (error) {
391
+ return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
392
+ }
393
+ });
394
+ 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.', {
395
+ projectPath: z.string().optional().describe('Project root path. Defaults to cwd.'),
396
+ completedFeatures: z.array(z.string()).optional().describe('Feature names to mark as done (matches against checkbox items in pmpt.md).'),
397
+ progressNote: z.string().optional().describe('Progress note to append to the Snapshot Log section.'),
398
+ snapshotVersion: z.string().optional().describe('Version label for the snapshot log entry (e.g. "v3 - Auth Complete"). Auto-generated if omitted.'),
399
+ }, async ({ projectPath, completedFeatures, progressNote, snapshotVersion }) => {
400
+ try {
401
+ const pp = resolveProjectPath(projectPath);
402
+ assertInitialized(pp);
403
+ const docsDir = getDocsDir(pp);
404
+ const pmptMdPath = join(docsDir, 'pmpt.md');
405
+ if (!existsSync(pmptMdPath)) {
406
+ return { content: [{ type: 'text', text: 'pmpt.md not found. Run pmpt_plan first to generate project docs.' }], isError: true };
407
+ }
408
+ let content = readFileSync(pmptMdPath, 'utf-8');
409
+ const changes = [];
410
+ // Check off completed features
411
+ if (completedFeatures && completedFeatures.length > 0) {
412
+ for (const feature of completedFeatures) {
413
+ const pattern = new RegExp(`- \\[ \\] (.*${feature.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}.*)`, 'i');
414
+ const match = content.match(pattern);
415
+ if (match) {
416
+ content = content.replace(match[0], `- [x] ${match[1]}`);
417
+ changes.push(`Checked: ${match[1]}`);
418
+ }
419
+ }
420
+ }
421
+ // Add progress note to Snapshot Log
422
+ if (progressNote) {
423
+ const snapshots = getAllSnapshots(pp);
424
+ const label = snapshotVersion || `v${snapshots.length} - Progress`;
425
+ const entry = `\n### ${label}\n- ${progressNote}\n`;
426
+ const logIndex = content.indexOf('## Snapshot Log');
427
+ if (logIndex !== -1) {
428
+ // Find the end of the Snapshot Log header line
429
+ const afterHeader = content.indexOf('\n', logIndex);
430
+ // Find the next ## section or end of file
431
+ const nextSection = content.indexOf('\n## ', afterHeader + 1);
432
+ const insertPos = nextSection !== -1 ? nextSection : content.length;
433
+ content = content.slice(0, insertPos) + entry + content.slice(insertPos);
434
+ }
435
+ else {
436
+ content += `\n## Snapshot Log${entry}`;
437
+ }
438
+ changes.push(`Added log: ${label}`);
439
+ }
440
+ if (changes.length === 0) {
441
+ return { content: [{ type: 'text', text: 'No changes to make. Provide completedFeatures or progressNote.' }] };
442
+ }
443
+ writeFileSync(pmptMdPath, content, 'utf-8');
444
+ return {
445
+ content: [{
446
+ type: 'text',
447
+ text: [`Updated pmpt.md:`, ...changes.map(c => ` - ${c}`), '', 'Run pmpt_save to create a snapshot.'].join('\n'),
448
+ }],
449
+ };
450
+ }
451
+ catch (error) {
452
+ return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
453
+ }
454
+ });
455
+ 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.', {
456
+ projectPath: z.string().optional().describe('Project root path. Defaults to cwd.'),
457
+ title: z.string().describe('Short decision title (e.g. "Database: SQLite over PostgreSQL").'),
458
+ reasoning: z.string().describe('Why this decision was made.'),
459
+ }, async ({ projectPath, title, reasoning }) => {
460
+ try {
461
+ const pp = resolveProjectPath(projectPath);
462
+ assertInitialized(pp);
463
+ const docsDir = getDocsDir(pp);
464
+ const pmptMdPath = join(docsDir, 'pmpt.md');
465
+ if (!existsSync(pmptMdPath)) {
466
+ return { content: [{ type: 'text', text: 'pmpt.md not found. Run pmpt_plan first.' }], isError: true };
467
+ }
468
+ let content = readFileSync(pmptMdPath, 'utf-8');
469
+ const date = new Date().toISOString().slice(0, 10);
470
+ const entry = `- **${title}** — ${reasoning} _(${date})_\n`;
471
+ const decisionsIndex = content.indexOf('## Decisions');
472
+ if (decisionsIndex !== -1) {
473
+ const afterHeader = content.indexOf('\n', decisionsIndex);
474
+ const nextSection = content.indexOf('\n## ', afterHeader + 1);
475
+ const insertPos = nextSection !== -1 ? nextSection : content.length;
476
+ content = content.slice(0, insertPos) + entry + content.slice(insertPos);
477
+ }
478
+ else {
479
+ // Insert before Snapshot Log if it exists, otherwise append
480
+ const logIndex = content.indexOf('## Snapshot Log');
481
+ if (logIndex !== -1) {
482
+ content = content.slice(0, logIndex) + `## Decisions\n${entry}\n` + content.slice(logIndex);
483
+ }
484
+ else {
485
+ content += `\n## Decisions\n${entry}`;
486
+ }
487
+ }
488
+ writeFileSync(pmptMdPath, content, 'utf-8');
489
+ return {
490
+ content: [{
491
+ type: 'text',
492
+ text: `Decision recorded: ${title}\nReasoning: ${reasoning}`,
493
+ }],
494
+ };
495
+ }
496
+ catch (error) {
497
+ return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
498
+ }
499
+ });
500
+ 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.', {
501
+ projectPath: z.string().optional().describe('Project root path. Defaults to cwd.'),
502
+ slug: z.string().describe('Project slug (3-50 chars, lowercase alphanumeric and hyphens).'),
503
+ description: z.string().optional().describe('Project description (max 500 chars).'),
504
+ tags: z.array(z.string()).optional().describe('Tags for the project (max 10).'),
505
+ category: z.string().optional().describe('Category: web-app, mobile-app, cli-tool, api-backend, ai-ml, game, library, other.'),
506
+ productUrl: z.string().optional().describe('Product URL (GitHub repo or live site).'),
507
+ productUrlType: z.enum(['git', 'url']).optional().describe('Type of product URL.'),
508
+ }, async ({ projectPath, slug, description, tags, category, productUrl, productUrlType }) => {
509
+ try {
510
+ const pp = resolveProjectPath(projectPath);
511
+ assertInitialized(pp);
512
+ // Check auth
513
+ const auth = loadAuth();
514
+ if (!auth) {
515
+ return { content: [{ type: 'text', text: 'Not logged in. Run `pmpt login` in the terminal first.' }], isError: true };
516
+ }
517
+ // Build .pmpt file content
518
+ const config = loadConfig(pp);
519
+ const planProgress = getPlanProgress(pp);
520
+ const snapshots = getAllSnapshots(pp);
521
+ const docsDir = getDocsDir(pp);
522
+ const quality = computeQuality(buildQualityInput(pp));
523
+ if (quality.score < 40) {
524
+ return {
525
+ content: [{
526
+ type: 'text',
527
+ 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')}`,
528
+ }],
529
+ isError: true,
530
+ };
531
+ }
532
+ const projectName = planProgress?.answers?.projectName || config?.lastPublishedSlug || slug;
533
+ // Build versions
534
+ const history = snapshots.map((s, i) => {
535
+ const files = resolveFullSnapshot(snapshots, i);
536
+ return {
537
+ version: s.version,
538
+ timestamp: s.timestamp,
539
+ files,
540
+ changedFiles: s.changedFiles,
541
+ note: s.note,
542
+ git: s.git,
543
+ };
544
+ });
545
+ // Build docs
546
+ const docs = {};
547
+ const tracked = getTrackedFiles(pp);
548
+ for (const file of tracked) {
549
+ const filePath = join(docsDir, file);
550
+ if (existsSync(filePath)) {
551
+ docs[file] = readFileSync(filePath, 'utf-8');
552
+ }
553
+ }
554
+ const meta = {
555
+ projectName,
556
+ createdAt: snapshots[0]?.timestamp || new Date().toISOString(),
557
+ exportedAt: new Date().toISOString(),
558
+ description: description || '',
559
+ };
560
+ const planAnswers = planProgress?.answers
561
+ ? planProgress.answers
562
+ : undefined;
563
+ const pmptContent = createPmptFile(meta, planAnswers, docs, history);
564
+ // Publish
565
+ const result = await publishProject(auth.token, {
566
+ slug,
567
+ pmptContent,
568
+ description: description || '',
569
+ tags: tags || [],
570
+ category,
571
+ productUrl,
572
+ productUrlType,
573
+ });
574
+ return {
575
+ content: [{
576
+ type: 'text',
577
+ text: [
578
+ `Published "${projectName}" to pmptwiki!`,
579
+ '',
580
+ `URL: ${result.url}`,
581
+ `Download: ${result.downloadUrl}`,
582
+ `Slug: ${slug}`,
583
+ `Author: @${auth.username}`,
584
+ ].join('\n'),
585
+ }],
586
+ };
587
+ }
588
+ catch (error) {
589
+ return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
590
+ }
591
+ });
592
+ 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`.', {
593
+ slug: z.string().describe('Project slug to graduate.'),
594
+ note: z.string().optional().describe('Graduation note (e.g., "Reached 1000 users!").'),
595
+ }, async ({ slug, note }) => {
596
+ try {
597
+ const auth = loadAuth();
598
+ if (!auth) {
599
+ return { content: [{ type: 'text', text: 'Not logged in. Run `pmpt login` in the terminal first.' }], isError: true };
600
+ }
601
+ const result = await graduateProject(auth.token, slug, note);
602
+ return {
603
+ content: [{
604
+ type: 'text',
605
+ text: [
606
+ `Project "${slug}" has graduated! 🎓`,
607
+ '',
608
+ `Graduated at: ${result.graduatedAt}`,
609
+ `Hall of Fame: https://pmptwiki.com/hall-of-fame`,
610
+ '',
611
+ 'The project is now archived. No further updates are allowed.',
612
+ ].join('\n'),
613
+ }],
614
+ };
615
+ }
616
+ catch (error) {
617
+ return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
618
+ }
619
+ });
238
620
  // ── Start ───────────────────────────────────────────
239
621
  async function main() {
240
622
  const transport = new StdioServerTransport();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmpt-cli",
3
- "version": "1.12.3",
3
+ "version": "1.13.0",
4
4
  "description": "Record and share your AI-driven product development journey",
5
5
  "type": "module",
6
6
  "bin": {