@programinglive/dev-workflow-mcp-server 1.1.7 → 1.1.16

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.
@@ -225,5 +225,108 @@ export function getToolList() {
225
225
  properties: {},
226
226
  },
227
227
  },
228
+ {
229
+ name: "rerun_workflow",
230
+ description:
231
+ "Reset and restart the current task from the beginning. Useful when you want to replay the entire workflow from start to finish.",
232
+ inputSchema: {
233
+ type: "object",
234
+ properties: {},
235
+ },
236
+ },
237
+ {
238
+ name: "run_full_workflow",
239
+ description:
240
+ "Execute every workflow step in sequence using the provided arguments (mark fixed → tests → run tests → docs → ready check → commit → release → complete).",
241
+ inputSchema: {
242
+ type: "object",
243
+ properties: {
244
+ summary: {
245
+ type: "string",
246
+ description: "Brief summary recorded when marking the bug/feature as fixed",
247
+ },
248
+ testsPassed: {
249
+ type: "boolean",
250
+ description: "Whether tests passed (defaults to true)",
251
+ },
252
+ testCommand: {
253
+ type: "string",
254
+ description: "Command used to run tests",
255
+ },
256
+ testDetails: {
257
+ type: "string",
258
+ description: "Additional test output or notes",
259
+ },
260
+ documentationType: {
261
+ type: "string",
262
+ enum: ["README", "inline-comments", "API-docs", "changelog", "other"],
263
+ description: "Type of documentation created",
264
+ },
265
+ documentationSummary: {
266
+ type: "string",
267
+ description: "Summary of documentation updates",
268
+ },
269
+ commitMessage: {
270
+ type: "string",
271
+ description: "Commit message used for commit_and_push and complete_task",
272
+ },
273
+ branch: {
274
+ type: "string",
275
+ description: "Optional branch name to push to",
276
+ },
277
+ releaseCommand: {
278
+ type: "string",
279
+ description: "Release command executed during perform_release",
280
+ },
281
+ releaseNotes: {
282
+ type: "string",
283
+ description: "Optional release notes to record",
284
+ },
285
+ releaseType: {
286
+ type: "string",
287
+ description: "Optional release type override (major/minor/patch)",
288
+ },
289
+ preset: {
290
+ type: "string",
291
+ description: "Optional preset argument for perform_release",
292
+ },
293
+ },
294
+ required: [
295
+ "summary",
296
+ "testCommand",
297
+ "documentationType",
298
+ "documentationSummary",
299
+ "commitMessage",
300
+ "releaseCommand",
301
+ ],
302
+ },
303
+ },
304
+ {
305
+ name: "project_summary",
306
+ description:
307
+ "Get a project knowledge summary that aggregates completed tasks, task types, and recent activity to help understand the project and fix bugs faster over time.",
308
+ inputSchema: {
309
+ type: "object",
310
+ properties: {},
311
+ },
312
+ },
313
+ {
314
+ name: "project_summary_data",
315
+ description:
316
+ "Get the persisted project knowledge summary from a structured JSON file (project-summary.json). Falls back to in-memory if the file doesn't exist. This file can be used as a simple database for future features.",
317
+ inputSchema: {
318
+ type: "object",
319
+ properties: {},
320
+ },
321
+ },
322
+ {
323
+ name: "project_summary_db",
324
+ description:
325
+ "Get the per-user project knowledge summary from SQLite. Use DEV_WORKFLOW_USER_ID env var to isolate users.",
326
+ inputSchema: {
327
+ type: "object",
328
+ properties: {},
329
+ },
330
+ },
228
331
  ];
229
332
  }
package/tools/handlers.js CHANGED
@@ -1,3 +1,12 @@
1
+ import fs from "fs/promises";
2
+ import { existsSync } from "fs";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const PROJECT_SUMMARY_FILENAME = "project-summary.json";
9
+
1
10
  const textResponse = (text) => ({
2
11
  content: [
3
12
  {
@@ -48,6 +57,30 @@ function summarizeHistory(history, limit) {
48
57
  return `📜 Workflow History (last ${recent.length} tasks)\n\n${historyText}`;
49
58
  }
50
59
 
60
+ function generateProjectSummary(history) {
61
+ if (!history || history.length === 0) {
62
+ return "No workflow history available.";
63
+ }
64
+
65
+ const taskTypes = {};
66
+ const recentTasks = history.slice(-20);
67
+
68
+ for (const entry of recentTasks) {
69
+ const type = entry.taskType || "other";
70
+ taskTypes[type] = (taskTypes[type] || 0) + 1;
71
+ }
72
+
73
+ const typeSummary = Object.entries(taskTypes)
74
+ .map(([type, count]) => `${type}: ${count}`)
75
+ .join(", ");
76
+
77
+ const totalTasks = history.length;
78
+ const lastTask = history[history.length - 1];
79
+ const lastActive = lastTask ? new Date(lastTask.timestamp).toLocaleDateString() : "Never";
80
+
81
+ return `🧠 Project Knowledge Summary\n\n📊 Stats\n- Total tasks completed: ${totalTasks}\n- Recent task mix: ${typeSummary}\n- Last active: ${lastActive}\n\n📝 Recent tasks (last 5)\n${history.slice(-5).reverse().map((e, i) => `${i + 1}. ${e.taskDescription} (${e.taskType})`).join("\n")}`;
82
+ }
83
+
51
84
  function getContinueGuidance(status) {
52
85
  switch (status.currentPhase) {
53
86
  case "coding": {
@@ -162,6 +195,29 @@ async function handleMarkBugFixed(args, workflowState) {
162
195
  return textResponse("⚠️ Please start a task first using 'start_task'!");
163
196
  }
164
197
 
198
+ // Idempotent behavior: if already marked fixed, do not reset downstream flags.
199
+ if (workflowState.state.bugFixed) {
200
+ const summary = typeof args.summary === "string" ? args.summary : "";
201
+ if (summary && summary.trim() !== "") {
202
+ workflowState.state.fixSummary = summary;
203
+ }
204
+
205
+ // Preserve existing progress and set an appropriate phase.
206
+ if (workflowState.state.testsPassed && workflowState.state.documentationCreated) {
207
+ workflowState.state.currentPhase = "ready";
208
+ workflowState.state.readyToCommit = true;
209
+ } else if (workflowState.state.testsCreated) {
210
+ workflowState.state.currentPhase = "testing";
211
+ }
212
+ workflowState.state.readyCheckCompleted = false;
213
+ workflowState.state.commitAndPushCompleted = false;
214
+ await workflowState.save();
215
+
216
+ return textResponse(
217
+ "ℹ️ Already marked as fixed; kept existing tests/documentation. Continue with the next step."
218
+ );
219
+ }
220
+
165
221
  workflowState.state.bugFixed = true;
166
222
  workflowState.state.testsCreated = false;
167
223
  workflowState.state.currentPhase = "testing";
@@ -358,14 +414,12 @@ async function handleReadyCheck(workflowState) {
358
414
 
359
415
  async function handleCommitAndPush(args, context) {
360
416
  const { workflowState, exec, git, utils } = context;
361
- if (!workflowState.state.readyToCommit) {
362
- return textResponse("⚠️ Not ready yet! Run 'check_ready_to_commit' and complete all steps first.");
363
- }
364
-
365
- if (!workflowState.state.readyCheckCompleted) {
366
- return textResponse(
367
- "⚠️ Please run 'check_ready_to_commit' and resolve any outstanding items before committing!"
368
- );
417
+ if (!workflowState.state.readyToCommit || !workflowState.state.readyCheckCompleted) {
418
+ // Be smart: auto-run the ready check and proceed if it passes.
419
+ const readyResponse = await handleReadyCheck(workflowState);
420
+ if (workflowState.state.currentPhase !== "commit") {
421
+ return readyResponse;
422
+ }
369
423
  }
370
424
 
371
425
  const providedCommitMessage =
@@ -696,6 +750,195 @@ function handleViewHistory(args, workflowState) {
696
750
  return textResponse(message);
697
751
  }
698
752
 
753
+ function handleProjectSummary(workflowState) {
754
+ const summary = generateProjectSummary(workflowState.state.history);
755
+ return textResponse(summary);
756
+ }
757
+
758
+ async function handleProjectSummaryData(workflowState) {
759
+ const summaryPath = path.join(path.dirname(workflowState.stateFile), PROJECT_SUMMARY_FILENAME);
760
+ try {
761
+ const data = await fs.readFile(summaryPath, "utf-8");
762
+ const parsed = JSON.parse(data);
763
+ const formatted = `🧠 Project Knowledge Summary (from persisted data)\n\n📊 Stats\n- Total tasks completed: ${parsed.totalTasks}\n- Recent task mix: ${Object.entries(parsed.taskTypes).map(([t, c]) => `${t}: ${c}`).join(", ")}\n- Last active: ${parsed.lastActive ? new Date(parsed.lastActive).toLocaleDateString() : "Never"}\n- Updated: ${new Date(parsed.updatedAt).toLocaleString()}\n\n📝 Recent tasks (last 5)\n${parsed.recentTasks.map((e, i) => `${i + 1}. ${e.description} (${e.type})`).join("\n")}`;
764
+ return textResponse(formatted);
765
+ } catch (error) {
766
+ // Fallback to in-memory generation if file doesn't exist
767
+ return handleProjectSummary(workflowState);
768
+ }
769
+ }
770
+
771
+ async function handleProjectSummaryDb(workflowState) {
772
+ const { getSummaryForUser } = await import("../db/index.js");
773
+ const userId = process.env.DEV_WORKFLOW_USER_ID || "default";
774
+ const summary = getSummaryForUser(userId);
775
+ if (!summary) {
776
+ return textResponse("📜 No project summary in database yet.");
777
+ }
778
+ const formatted = `🧠 Project Knowledge Summary (from SQLite)\n\n📊 Stats\n- Total tasks completed: ${summary.totalTasks}\n- Recent task mix: ${Object.entries(summary.taskTypes).map(([t, c]) => `${t}: ${c}`).join(", ")}\n- Last active: ${summary.lastActive ? new Date(summary.lastActive).toLocaleDateString() : "Never"}\n- Updated: ${new Date(summary.updatedAt).toLocaleString()}\n\n📝 Recent tasks (last 5)\n${summary.recentTasks.map((e, i) => `${i + 1}. ${e.description} (${e.type})`).join("\n")}`;
779
+ return textResponse(formatted);
780
+ }
781
+
782
+ async function handleRerunWorkflow(workflowState) {
783
+ const currentDescription = workflowState.state.taskDescription;
784
+ const currentType = workflowState.state.taskType;
785
+
786
+ if (!currentDescription) {
787
+ return textResponse("⚠️ No active task to rerun. Use 'start_task' to begin a new workflow.");
788
+ }
789
+
790
+ workflowState.reset();
791
+ workflowState.state.currentPhase = "coding";
792
+ workflowState.state.taskDescription = currentDescription;
793
+ workflowState.state.taskType = currentType;
794
+ await workflowState.save();
795
+
796
+ return textResponse(
797
+ `🔄 Rerunning workflow from the start!\n\nTask: ${currentDescription}\nType: ${currentType}\n\nWorkflow Steps:\n1. ✓ Start task (current)\n2. ⏳ Fix/implement the feature\n3. ⏳ Create tests\n4. ⏳ Run tests (must pass!)\n5. ⏳ Create documentation\n6. ⏳ Run 'check_ready_to_commit'\n7. ⏳ Run 'commit_and_push' (commits and pushes)\n8. ⏳ Run 'perform_release' (handles versioning and tags)\n9. ⏳ Complete task\n\n🎯 Be conscious about what you're coding!`
798
+ );
799
+ }
800
+
801
+ function isErrorResponse(response) {
802
+ const text = response?.content?.[0]?.text || "";
803
+ return /^\s*[⚠️❌]/u.test(text);
804
+ }
805
+
806
+ function summarizeResponse(label, response) {
807
+ const text = response?.content?.[0]?.text || "";
808
+ const firstLine = text.split("\n")[0] || "";
809
+ return `${label}: ${firstLine}`;
810
+ }
811
+
812
+ async function handleRunFullWorkflow(args, { workflowState, exec, git, utils }) {
813
+ const description = workflowState.state.taskDescription;
814
+ if (!description) {
815
+ return textResponse("⚠️ Please start a task first using 'start_task' before running the full workflow.");
816
+ }
817
+
818
+ await workflowState.ensurePrimaryFile();
819
+
820
+ const requiredStrings = [
821
+ { key: "summary", message: "Provide a non-empty 'summary' for mark_bug_fixed." },
822
+ { key: "testCommand", message: "Provide a non-empty 'testCommand' for run_tests." },
823
+ { key: "documentationType", message: "Provide 'documentationType' for create_documentation." },
824
+ { key: "documentationSummary", message: "Provide a non-empty 'documentationSummary'." },
825
+ { key: "commitMessage", message: "Provide a non-empty 'commitMessage'." },
826
+ { key: "releaseCommand", message: "Provide a non-empty 'releaseCommand'." },
827
+ ];
828
+
829
+ for (const { key, message } of requiredStrings) {
830
+ const value = args[key];
831
+ if (typeof value !== "string" || value.trim() === "") {
832
+ return textResponse(`⚠️ ${message}`);
833
+ }
834
+ }
835
+
836
+ const steps = [];
837
+ const testsPassed = typeof args.testsPassed === "boolean" ? args.testsPassed : true;
838
+ const testDetails = typeof args.testDetails === "string" ? args.testDetails : "";
839
+ const releaseArgs = {
840
+ command: args.releaseCommand,
841
+ };
842
+ if (typeof args.releaseNotes === "string" && args.releaseNotes.trim() !== "") {
843
+ releaseArgs.notes = args.releaseNotes;
844
+ }
845
+ if (typeof args.releaseType === "string" && args.releaseType.trim() !== "") {
846
+ releaseArgs.releaseType = args.releaseType;
847
+ }
848
+ if (typeof args.preset === "string" && args.preset.trim() !== "") {
849
+ releaseArgs.preset = args.preset;
850
+ }
851
+
852
+ // Smart flow: detect current phase and run only remaining steps
853
+ async function executeStep(name, handlerFn, handlerArgs = []) {
854
+ if (isErrorResponse(handlerFn)) {
855
+ return handlerFn;
856
+ }
857
+ const response = await handlerFn(...handlerArgs);
858
+ if (isErrorResponse(response)) {
859
+ return response;
860
+ }
861
+ steps.push(summarizeResponse(name, response));
862
+ await workflowState.load();
863
+ return response;
864
+ }
865
+
866
+ while (true) {
867
+ const state = workflowState.state;
868
+
869
+ if (!state.bugFixed) {
870
+ const resp = await executeStep("mark_bug_fixed", handleMarkBugFixed, [{ summary: args.summary }, workflowState]);
871
+ if (isErrorResponse(resp)) return resp;
872
+ continue;
873
+ }
874
+
875
+ if (!state.testsCreated) {
876
+ const resp = await executeStep("create_tests", handleCreateTests, [workflowState]);
877
+ if (isErrorResponse(resp)) return resp;
878
+ continue;
879
+ }
880
+
881
+ if (!state.testsPassed && !state.testsSkipped) {
882
+ const resp = await executeStep("run_tests", handleRunTests, [
883
+ { passed: testsPassed, testCommand: args.testCommand, details: testDetails },
884
+ workflowState,
885
+ ]);
886
+ if (isErrorResponse(resp)) return resp;
887
+ continue;
888
+ }
889
+
890
+ if (!state.documentationCreated) {
891
+ const resp = await executeStep("create_documentation", handleCreateDocumentation, [
892
+ { documentationType: args.documentationType, summary: args.documentationSummary },
893
+ workflowState,
894
+ ]);
895
+ if (isErrorResponse(resp)) return resp;
896
+ continue;
897
+ }
898
+
899
+ if (!state.readyCheckCompleted) {
900
+ const resp = await executeStep("check_ready_to_commit", handleReadyCheck, [workflowState]);
901
+ if (isErrorResponse(resp)) return resp;
902
+ continue;
903
+ }
904
+
905
+ if (!state.commitAndPushCompleted) {
906
+ const resp = await executeStep("commit_and_push", handleCommitAndPush, [
907
+ {
908
+ commitMessage: args.commitMessage,
909
+ branch: typeof args.branch === "string" ? args.branch : "",
910
+ },
911
+ { workflowState, exec, git, utils },
912
+ ]);
913
+ if (isErrorResponse(resp)) return resp;
914
+ continue;
915
+ }
916
+
917
+ if (!state.released) {
918
+ const resp = await executeStep("perform_release", handlePerformRelease, [
919
+ releaseArgs,
920
+ { workflowState, exec, git, utils },
921
+ ]);
922
+ if (isErrorResponse(resp)) return resp;
923
+ continue;
924
+ }
925
+
926
+ const completeResponse = await handleCompleteTask({ commitMessage: args.commitMessage }, workflowState);
927
+ if (isErrorResponse(completeResponse)) {
928
+ return completeResponse;
929
+ }
930
+ steps.push(summarizeResponse("complete_task", completeResponse));
931
+ break;
932
+ }
933
+
934
+ const summary = steps.map((line, index) => `${index + 1}. ${line}`).join("\n");
935
+ const celebration = workflowState.state.history?.length
936
+ ? "🎉 Cheers! We solved another issue and captured it in the knowledge base."
937
+ : "";
938
+ const message = [`✅ Full workflow completed successfully!`, celebration, summary].filter(Boolean).join("\n\n");
939
+ return textResponse(message);
940
+ }
941
+
699
942
  export async function handleToolCall({
700
943
  request,
701
944
  normalizeRequestArgs,
@@ -741,8 +984,18 @@ export async function handleToolCall({
741
984
  return handleGetWorkflowStatus(workflowState);
742
985
  case "view_history":
743
986
  return handleViewHistory(args, workflowState);
987
+ case "project_summary":
988
+ return handleProjectSummary(workflowState);
989
+ case "project_summary_data":
990
+ return handleProjectSummaryData(workflowState);
991
+ case "project_summary_db":
992
+ return handleProjectSummaryDb(workflowState);
744
993
  case "continue_workflow":
745
994
  return handleContinueWorkflow(workflowState, { workflowState, exec, git, utils });
995
+ case "rerun_workflow":
996
+ return handleRerunWorkflow(workflowState);
997
+ case "run_full_workflow":
998
+ return handleRunFullWorkflow(args, { workflowState, exec, git, utils });
746
999
  default:
747
1000
  throw new Error(`Unknown tool: ${name}`);
748
1001
  }
package/vite.config.js ADDED
@@ -0,0 +1,43 @@
1
+ import { defineConfig } from 'vite';
2
+ import { fileURLToPath } from 'url';
3
+ import path from 'path';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+
8
+ export default defineConfig({
9
+ build: {
10
+ target: 'node18',
11
+ outDir: 'dist',
12
+ minify: false,
13
+ rollupOptions: {
14
+ input: path.resolve(__dirname, 'index.js'),
15
+ output: {
16
+ entryFileNames: 'index.mjs',
17
+ format: 'es',
18
+ },
19
+ external: [
20
+ '@modelcontextprotocol/sdk',
21
+ // Node.js built-in modules
22
+ 'node:fs',
23
+ 'node:path',
24
+ 'node:url',
25
+ 'node:util',
26
+ 'node:os',
27
+ 'node:process',
28
+ 'node:child_process',
29
+ 'fs',
30
+ 'fs/promises',
31
+ 'path',
32
+ 'url',
33
+ 'util',
34
+ 'os',
35
+ 'process',
36
+ 'child_process',
37
+ ],
38
+ },
39
+ },
40
+ define: {
41
+ __filename: JSON.stringify(path.resolve(__dirname, 'index.js')),
42
+ },
43
+ });