pmpt-cli 1.14.6 → 1.14.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  import * as p from '@clack/prompts';
2
- import { resolve } from 'path';
3
- import { existsSync, statSync } from 'fs';
2
+ import { resolve, join } from 'path';
3
+ import { existsSync, statSync, readFileSync, writeFileSync } from 'fs';
4
4
  import { isInitialized, getDocsDir } from '../lib/config.js';
5
- import { createFullSnapshot, getTrackedFiles } from '../lib/history.js';
5
+ import { createFullSnapshot, getTrackedFiles, getAllSnapshots } from '../lib/history.js';
6
6
  export async function cmdSave(fileOrPath) {
7
7
  const projectPath = fileOrPath && existsSync(fileOrPath) && statSync(fileOrPath).isDirectory()
8
8
  ? resolve(fileOrPath)
@@ -21,10 +21,46 @@ export async function cmdSave(fileOrPath) {
21
21
  p.outro('');
22
22
  return;
23
23
  }
24
+ // Ask for summary
25
+ const summary = await p.text({
26
+ message: 'What did you accomplish? (this is shown on your project page)',
27
+ placeholder: 'e.g. Added user auth with JWT, built login/signup pages',
28
+ });
29
+ if (p.isCancel(summary)) {
30
+ p.cancel('Save cancelled.');
31
+ process.exit(0);
32
+ }
33
+ const note = summary.trim() || undefined;
34
+ // Write summary to pmpt.md Snapshot Log before snapshot
35
+ if (note) {
36
+ const pmptMdPath = join(docsDir, 'pmpt.md');
37
+ if (existsSync(pmptMdPath)) {
38
+ let content = readFileSync(pmptMdPath, 'utf-8');
39
+ const snapshots = getAllSnapshots(projectPath);
40
+ const nextVersion = snapshots.length + 1;
41
+ const date = new Date().toISOString().slice(0, 10);
42
+ const noteLines = note.split(/(?:\.\s+|\n)/).filter(s => s.trim()).map(s => {
43
+ const trimmed = s.trim().replace(/\.?$/, '');
44
+ return `- ${trimmed}`;
45
+ });
46
+ const entry = `\n### v${nextVersion} — ${date}\n${noteLines.join('\n')}\n`;
47
+ const logIndex = content.indexOf('## Snapshot Log');
48
+ if (logIndex !== -1) {
49
+ const afterHeader = content.indexOf('\n', logIndex);
50
+ const nextSection = content.indexOf('\n## ', afterHeader + 1);
51
+ const insertPos = nextSection !== -1 ? nextSection : content.length;
52
+ content = content.slice(0, insertPos) + entry + content.slice(insertPos);
53
+ }
54
+ else {
55
+ content += `\n## Snapshot Log${entry}`;
56
+ }
57
+ writeFileSync(pmptMdPath, content, 'utf-8');
58
+ }
59
+ }
24
60
  const s = p.spinner();
25
61
  s.start(`Creating snapshot of ${files.length} file(s)...`);
26
62
  try {
27
- const entry = createFullSnapshot(projectPath);
63
+ const entry = createFullSnapshot(projectPath, { note });
28
64
  s.stop('Snapshot saved');
29
65
  let msg = `v${entry.version} saved`;
30
66
  if (entry.git) {
@@ -38,11 +74,8 @@ export async function cmdSave(fileOrPath) {
38
74
  msg += ` (${changedCount} changed, ${unchangedCount} skipped)`;
39
75
  }
40
76
  p.log.success(msg);
41
- // Warn if pmpt.md was not updated since last save
42
- if (entry.version > 1 && entry.changedFiles && !entry.changedFiles.includes('pmpt.md')) {
43
- p.log.message('');
44
- p.log.warn('pmpt.md has not been updated since the last save.');
45
- p.log.message(' Tip: Mark completed features and update the Snapshot Log before saving.');
77
+ if (note) {
78
+ p.log.info(`Summary: ${note}`);
46
79
  }
47
80
  p.log.message('');
48
81
  p.log.info('Files included:');
@@ -14,7 +14,7 @@ export function cmdWatch(path) {
14
14
  p.log.info('Auto-saving snapshots on MD file changes.');
15
15
  p.log.info('Press Ctrl+C to stop.');
16
16
  p.log.message('');
17
- const watcher = startWatching(projectPath, (version, files, git) => {
17
+ const watcher = startWatching(projectPath, (version, files, git, note) => {
18
18
  let msg = `v${version} saved (${files.length} file(s))`;
19
19
  if (git) {
20
20
  msg += ` · ${git.commit}`;
@@ -22,6 +22,9 @@ export function cmdWatch(path) {
22
22
  msg += ' (uncommitted)';
23
23
  }
24
24
  p.log.success(msg);
25
+ if (note) {
26
+ p.log.info(` ${note}`);
27
+ }
25
28
  });
26
29
  process.on('SIGINT', () => {
27
30
  p.log.message('');
@@ -2,7 +2,7 @@ import chokidar from 'chokidar';
2
2
  import { loadConfig, getDocsDir } from './config.js';
3
3
  import { createFullSnapshot } from './history.js';
4
4
  import { readFileSync } from 'fs';
5
- import { join } from 'path';
5
+ import { join, relative } from 'path';
6
6
  export function startWatching(projectPath, onSnapshot) {
7
7
  const config = loadConfig(projectPath);
8
8
  if (!config) {
@@ -19,11 +19,18 @@ export function startWatching(projectPath, onSnapshot) {
19
19
  },
20
20
  });
21
21
  const fileContents = new Map();
22
+ const pendingChanges = new Set();
22
23
  let debounceTimer = null;
23
24
  const saveSnapshot = () => {
24
- const entry = createFullSnapshot(projectPath);
25
+ // Build auto-note from changed file names
26
+ const changedNames = [...pendingChanges].map(p => relative(docsDir, p));
27
+ const note = changedNames.length > 0
28
+ ? `Updated ${changedNames.join(', ')}`
29
+ : undefined;
30
+ pendingChanges.clear();
31
+ const entry = createFullSnapshot(projectPath, { note });
25
32
  if (onSnapshot) {
26
- onSnapshot(entry.version, entry.files, entry.git);
33
+ onSnapshot(entry.version, entry.files, entry.git, note);
27
34
  }
28
35
  };
29
36
  // Debounced snapshot save (1 second)
@@ -37,6 +44,7 @@ export function startWatching(projectPath, onSnapshot) {
37
44
  try {
38
45
  const content = readFileSync(path, 'utf-8');
39
46
  fileContents.set(path, content);
47
+ pendingChanges.add(path);
40
48
  debouncedSave();
41
49
  }
42
50
  catch {
@@ -50,6 +58,7 @@ export function startWatching(projectPath, onSnapshot) {
50
58
  // Only snapshot if content actually changed
51
59
  if (oldContent !== newContent) {
52
60
  fileContents.set(path, newContent);
61
+ pendingChanges.add(path);
53
62
  debouncedSave();
54
63
  }
55
64
  }
package/dist/mcp.js CHANGED
@@ -15,7 +15,7 @@ import { createRequire } from 'module';
15
15
  import { isInitialized, loadConfig, saveConfig, getDocsDir, getHistoryDir } 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, savePlanProgress, savePlanDocuments, PLAN_QUESTIONS } from './lib/plan.js';
18
+ import { getPlanProgress, savePlanProgress, savePlanDocuments, PLAN_QUESTIONS, generatePlanDocument, generateAIPrompt } from './lib/plan.js';
19
19
  import { isGitRepo } from './lib/git.js';
20
20
  import { diffSnapshots } from './lib/diff.js';
21
21
  import { loadAuth } from './lib/auth.js';
@@ -98,9 +98,9 @@ function formatDiffs(diffs) {
98
98
  return lines.join('\n');
99
99
  }
100
100
  // ── Tools ───────────────────────────────────────────
101
- server.tool('pmpt_save', 'Save a snapshot of .pmpt/docs/ files. Call after completing features, fixes, or milestones. CRITICAL: Always provide a summary parameter — it becomes the version description shown on pmptwiki.com. Without a summary, the version appears empty on the project page. Write a concise description of what was accomplished (e.g. "Added user authentication with JWT").', {
101
+ server.tool('pmpt_save', 'Save a snapshot of .pmpt/docs/ files. Call after completing features, fixes, or milestones. CRITICAL: Always provide a detailed summary parameter — it becomes the version description shown publicly on pmptwiki.com. Without a summary, the version appears empty on the project page. Write a DETAILED summary (3-5 sentences) that explains: (1) WHAT was built or changed, (2) WHY it matters, (3) key technical decisions made. Think of it as a mini dev blog entry that helps others learn from your journey.', {
102
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".'),
103
+ summary: z.string().optional().describe('Detailed description of what was accomplished since the last save. Write 3-5 sentences that tell the story: what you built, why, and how. This is shown publicly on the project page. BAD example: "Added auth" — too vague. GOOD example: "Implemented user authentication using JWT with refresh token rotation. Chose JWT over session-based auth for stateless API compatibility. Added login/signup pages with form validation and error handling. Protected routes now redirect unauthenticated users to login."'),
104
104
  }, async ({ projectPath, summary }) => {
105
105
  try {
106
106
  const pp = resolveProjectPath(projectPath);
@@ -118,7 +118,11 @@ server.tool('pmpt_save', 'Save a snapshot of .pmpt/docs/ files. Call after compl
118
118
  const snapshots = getAllSnapshots(pp);
119
119
  const nextVersion = snapshots.length + 1;
120
120
  const date = new Date().toISOString().slice(0, 10);
121
- const entry = `\n### v${nextVersion} ${date}\n- ${summary}\n`;
121
+ const summaryLines = summary.split(/(?:\.\s+|\n)/).filter(s => s.trim()).map(s => {
122
+ const trimmed = s.trim().replace(/\.?$/, '');
123
+ return `- ${trimmed}`;
124
+ });
125
+ const entry = `\n### v${nextVersion} — ${date}\n${summaryLines.join('\n')}\n`;
122
126
  const logIndex = content.indexOf('## Snapshot Log');
123
127
  if (logIndex !== -1) {
124
128
  const afterHeader = content.indexOf('\n', logIndex);
@@ -476,11 +480,11 @@ server.tool('pmpt_read_context', 'Read project context to understand current sta
476
480
  return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
477
481
  }
478
482
  });
479
- server.tool('pmpt_update_doc', 'Update pmpt.md: check off completed features, add progress notes, or backfill missing version summaries. Use after completing work OR before publishing to fill in empty Snapshot Log entries. To backfill: set snapshotVersion="v2 — Description" and progressNote="What was done in this version".', {
483
+ server.tool('pmpt_update_doc', 'Update pmpt.md: check off completed features, add progress notes, or backfill missing version summaries. Use after completing work OR before publishing to fill in empty Snapshot Log entries. To backfill: use pmpt_diff to understand what changed, then set snapshotVersion="v2 — Short Title" and progressNote with DETAILED multi-sentence description. Write as if explaining to another developer what happened in this version and why.', {
480
484
  projectPath: z.string().optional().describe('Project root path. Defaults to cwd.'),
481
485
  completedFeatures: z.array(z.string()).optional().describe('Feature names to mark as done (matches against checkbox items in pmpt.md).'),
482
- progressNote: z.string().optional().describe('Progress note to append to the Snapshot Log section.'),
483
- snapshotVersion: z.string().optional().describe('Version label for the snapshot log entry (e.g. "v3 - Auth Complete"). Auto-generated if omitted.'),
486
+ progressNote: z.string().optional().describe('Detailed progress note (3-5 sentences) to append to the Snapshot Log. Explain what was done, why, and any key decisions. Each sentence becomes a bullet point. BAD: "Fixed bugs". GOOD: "Fixed authentication redirect loop caused by token expiry race condition. Added token refresh middleware that silently renews expired tokens. Users no longer get logged out unexpectedly during long sessions."'),
487
+ snapshotVersion: z.string().optional().describe('Version label for the snapshot log entry (e.g. "v3 Auth Complete"). Auto-generated if omitted.'),
484
488
  }, async ({ projectPath, completedFeatures, progressNote, snapshotVersion }) => {
485
489
  try {
486
490
  const pp = resolveProjectPath(projectPath);
@@ -507,7 +511,11 @@ server.tool('pmpt_update_doc', 'Update pmpt.md: check off completed features, ad
507
511
  if (progressNote) {
508
512
  const snapshots = getAllSnapshots(pp);
509
513
  const label = snapshotVersion || `v${snapshots.length} - Progress`;
510
- const entry = `\n### ${label}\n- ${progressNote}\n`;
514
+ const noteLines = progressNote.split(/(?:\.\s+|\n)/).filter(s => s.trim()).map(s => {
515
+ const trimmed = s.trim().replace(/\.?$/, '');
516
+ return `- ${trimmed}`;
517
+ });
518
+ const entry = `\n### ${label}\n${noteLines.join('\n')}\n`;
511
519
  const logIndex = content.indexOf('## Snapshot Log');
512
520
  if (logIndex !== -1) {
513
521
  // Find the end of the Snapshot Log header line
@@ -582,7 +590,7 @@ server.tool('pmpt_log_decision', 'Record an architectural or technical decision
582
590
  return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
583
591
  }
584
592
  });
585
- server.tool('pmpt_publish', 'Publish the project to pmptwiki.com. Non-interactive — just provide slug and optional metadata. MANDATORY pre-publish checklist: (1) Run pmpt_history — if ANY version lacks a note/summary, you MUST fix it before publishing. (2) For each empty version, run pmpt_diff for that version to understand changes, then use pmpt_update_doc with progressNote and snapshotVersion (e.g. snapshotVersion="v2 Tech stack migration to Cloudflare") to add Snapshot Log entries to pmpt.md. (3) After backfilling all versions, run pmpt_save with a summary. (4) Run pmpt_quality to verify readiness. DO NOT publish with empty versions — they display poorly on the project page. Note: user must have run `pmpt login` once before.', {
593
+ server.tool('pmpt_publish', 'Publish the project to pmptwiki.com. Non-interactive — just provide slug and optional metadata. MANDATORY pre-publish checklist: (1) Run pmpt_history — if ANY version lacks a note/summary, you MUST fix it before publishing. (2) For each empty version, run pmpt_diff for that version to understand what changed, then use pmpt_update_doc with a DETAILED progressNote (3-5 sentences explaining what, why, and key decisions) and snapshotVersion. Write like a dev blog others will read this to learn from your journey. (3) After backfilling all versions, run pmpt_save with a detailed summary. (4) Run pmpt_quality to verify readiness. DO NOT publish with empty or vague single-line versions — they display poorly on the project page. Note: user must have run `pmpt login` once before.', {
586
594
  projectPath: z.string().optional().describe('Project root path. Defaults to cwd.'),
587
595
  slug: z.string().describe('Project slug (3-50 chars, lowercase alphanumeric and hyphens).'),
588
596
  description: z.string().optional().describe('Project description (max 500 chars).'),
@@ -681,6 +689,134 @@ server.tool('pmpt_publish', 'Publish the project to pmptwiki.com. Non-interactiv
681
689
  return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
682
690
  }
683
691
  });
692
+ server.tool('pmpt_edit_plan', 'Edit existing plan fields (productIdea, coreFeatures, techStack, additionalContext, projectName). Use this to update plan content — e.g., translate productIdea to English, add new features, change tech stack. Only provided fields are updated; others stay unchanged. Regenerates plan.md and pmpt.ai.md automatically. pmpt.md progress/log sections are preserved — only the plan-derived sections (Product Idea, Features, Tech Stack, Additional Context) are updated.', {
693
+ projectPath: z.string().optional().describe('Project root path. Defaults to cwd.'),
694
+ projectName: z.string().optional().describe('New project name.'),
695
+ productIdea: z.string().optional().describe('New product idea description.'),
696
+ coreFeatures: z.string().optional().describe('New core features (comma or semicolon separated).'),
697
+ techStack: z.string().optional().describe('New tech stack preference.'),
698
+ additionalContext: z.string().optional().describe('New additional context.'),
699
+ }, async ({ projectPath, projectName, productIdea, coreFeatures, techStack, additionalContext }) => {
700
+ try {
701
+ const pp = resolveProjectPath(projectPath);
702
+ assertInitialized(pp);
703
+ const progress = getPlanProgress(pp);
704
+ if (!progress?.completed || !progress.answers) {
705
+ return { content: [{ type: 'text', text: 'No plan found. Run pmpt_plan first to create a plan.' }], isError: true };
706
+ }
707
+ // Track what changed
708
+ const changes = [];
709
+ const answers = { ...progress.answers };
710
+ if (projectName !== undefined) {
711
+ answers.projectName = projectName;
712
+ changes.push(`projectName → "${projectName}"`);
713
+ }
714
+ if (productIdea !== undefined) {
715
+ answers.productIdea = productIdea;
716
+ changes.push(`productIdea updated`);
717
+ }
718
+ if (coreFeatures !== undefined) {
719
+ answers.coreFeatures = coreFeatures;
720
+ changes.push(`coreFeatures updated`);
721
+ }
722
+ if (techStack !== undefined) {
723
+ answers.techStack = techStack;
724
+ changes.push(`techStack → "${techStack}"`);
725
+ }
726
+ if (additionalContext !== undefined) {
727
+ answers.additionalContext = additionalContext;
728
+ changes.push(`additionalContext updated`);
729
+ }
730
+ if (changes.length === 0) {
731
+ return { content: [{ type: 'text', text: 'No fields provided. Pass at least one field to update.' }] };
732
+ }
733
+ // Update plan-progress.json
734
+ savePlanProgress(pp, { ...progress, answers });
735
+ // Regenerate plan.md and pmpt.ai.md
736
+ const docsDir = getDocsDir(pp);
737
+ const planPath = join(docsDir, 'plan.md');
738
+ writeFileSync(planPath, generatePlanDocument(answers), 'utf-8');
739
+ const aiMdPath = join(docsDir, 'pmpt.ai.md');
740
+ writeFileSync(aiMdPath, generateAIPrompt(answers), 'utf-8');
741
+ // Update pmpt.md plan-derived sections (preserve progress, log, decisions)
742
+ const pmptMdPath = join(docsDir, 'pmpt.md');
743
+ if (existsSync(pmptMdPath)) {
744
+ let content = readFileSync(pmptMdPath, 'utf-8');
745
+ // Update title (first # heading)
746
+ content = content.replace(/^# .+$/m, `# ${answers.projectName}`);
747
+ // Update Product Idea section
748
+ const ideaRegex = /## Product Idea\n[\s\S]*?(?=\n## )/;
749
+ if (ideaRegex.test(content)) {
750
+ content = content.replace(ideaRegex, `## Product Idea\n${answers.productIdea}\n`);
751
+ }
752
+ // Update Features section (only unchecked items get replaced; checked items are preserved)
753
+ const featuresRegex = /## Features\n[\s\S]*?(?=\n## )/;
754
+ if (featuresRegex.test(content)) {
755
+ const existingMatch = content.match(featuresRegex);
756
+ if (existingMatch) {
757
+ // Extract already-checked features
758
+ const checked = existingMatch[0]
759
+ .split('\n')
760
+ .filter(line => line.startsWith('- [x]'))
761
+ .join('\n');
762
+ const newFeatures = answers.coreFeatures
763
+ .split(/[,;\n]/)
764
+ .map((f) => f.trim())
765
+ .filter((f) => f)
766
+ .map((f) => `- [ ] ${f}`)
767
+ .join('\n');
768
+ const featuresSection = checked
769
+ ? `## Features\n${checked}\n${newFeatures}\n`
770
+ : `## Features\n${newFeatures}\n`;
771
+ content = content.replace(featuresRegex, featuresSection);
772
+ }
773
+ }
774
+ // Update Tech Stack section
775
+ if (answers.techStack) {
776
+ const techRegex = /## Tech Stack\n[\s\S]*?(?=\n## )/;
777
+ if (techRegex.test(content)) {
778
+ content = content.replace(techRegex, `## Tech Stack\n${answers.techStack}\n`);
779
+ }
780
+ else {
781
+ // Insert before Progress section
782
+ const progressIdx = content.indexOf('\n## Progress');
783
+ if (progressIdx !== -1) {
784
+ content = content.slice(0, progressIdx) + `\n## Tech Stack\n${answers.techStack}\n` + content.slice(progressIdx);
785
+ }
786
+ }
787
+ }
788
+ // Update Additional Context section
789
+ if (answers.additionalContext) {
790
+ const ctxRegex = /## Additional Context\n[\s\S]*?(?=\n## )/;
791
+ if (ctxRegex.test(content)) {
792
+ content = content.replace(ctxRegex, `## Additional Context\n${answers.additionalContext}\n`);
793
+ }
794
+ else {
795
+ const ideaEnd = content.indexOf('\n## ', content.indexOf('## Product Idea') + 1);
796
+ if (ideaEnd !== -1) {
797
+ content = content.slice(0, ideaEnd) + `\n## Additional Context\n${answers.additionalContext}\n` + content.slice(ideaEnd);
798
+ }
799
+ }
800
+ }
801
+ writeFileSync(pmptMdPath, content, 'utf-8');
802
+ }
803
+ return {
804
+ content: [{
805
+ type: 'text',
806
+ text: [
807
+ `Plan updated:`,
808
+ ...changes.map(c => ` - ${c}`),
809
+ '',
810
+ 'Regenerated: plan.md, pmpt.ai.md, pmpt.md (progress preserved)',
811
+ 'Run pmpt_save to snapshot this change.',
812
+ ].join('\n'),
813
+ }],
814
+ };
815
+ }
816
+ catch (error) {
817
+ return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
818
+ }
819
+ });
684
820
  server.tool('pmpt_graduate', 'Graduate a project on pmptwiki — archives it with a Hall of Fame badge. The project can no longer be updated. Non-interactive. User must have run `pmpt login` once before.', {
685
821
  slug: z.string().describe('Project slug to graduate.'),
686
822
  note: z.string().optional().describe('Graduation note (e.g., "Reached 1000 users!").'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmpt-cli",
3
- "version": "1.14.6",
3
+ "version": "1.14.8",
4
4
  "description": "Record and share your AI-driven product development journey",
5
5
  "type": "module",
6
6
  "bin": {