@programinglive/dev-workflow-mcp-server 1.1.6 β 1.1.11
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/CHANGELOG.md +51 -0
- package/README.md +92 -5
- package/db/index.js +204 -0
- package/exec.js +46 -1
- package/index.js +12 -2
- package/install.js +5 -1
- package/package.json +15 -8
- package/release-wrapper.js +1 -1
- package/tests/build.test.js +37 -0
- package/tests/exec.test.js +32 -0
- package/tests/handlers.test.js +288 -0
- package/tests/release-wrapper.test.js +4 -2
- package/tests/workflowState.test.js +220 -1
- package/tools/definitions.js +103 -0
- package/tools/handlers.js +262 -9
- package/vite.config.js +43 -0
- package/web/index.html +307 -0
- package/web/server.js +57 -0
- package/workflow-state.js +439 -15
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,57 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [1.1.11](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.1.10...v1.1.11) (2025-10-30)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### π§Ή Chores
|
|
9
|
+
|
|
10
|
+
* verify run_full_workflow with persisted project summary ([6fdb475](https://github.com/programinglive/dev-workflow-mcp-server/commit/6fdb475f65ee6ef2c061b0b1cf954a282406e2a1))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### β¨ Features
|
|
14
|
+
|
|
15
|
+
* add per-user state, SQLite project summary, and dashboard ([7b57b6e](https://github.com/programinglive/dev-workflow-mcp-server/commit/7b57b6e067ad59f2999a2badcb51b1293192c96b))
|
|
16
|
+
* per-project workflow state with unique per-user IDs; smarter workflow ([b710a52](https://github.com/programinglive/dev-workflow-mcp-server/commit/b710a524997583f58cd6da2567ba9fd14abdd3c9))
|
|
17
|
+
|
|
18
|
+
### [1.1.10](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.1.9...v1.1.10) (2025-10-29)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### β¨ Features
|
|
22
|
+
|
|
23
|
+
* add project knowledge library with persisted summary file for future database integration ([b9dc13f](https://github.com/programinglive/dev-workflow-mcp-server/commit/b9dc13f4b25dec756088a55185ccbfdb3988a644))
|
|
24
|
+
|
|
25
|
+
### [1.1.9](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.1.8...v1.1.9) (2025-10-29)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
### π§Ή Chores
|
|
29
|
+
|
|
30
|
+
* interim commit ([b1aead0](https://github.com/programinglive/dev-workflow-mcp-server/commit/b1aead05920c766a183988fb93d3f0edc3e1eafc))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
### β
Tests
|
|
34
|
+
|
|
35
|
+
* update README.md, exec.js, package-lock.json +7 more ([894fdf1](https://github.com/programinglive/dev-workflow-mcp-server/commit/894fdf1eace33ddf0081c565028d214e1c7697bf))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
### β¨ Features
|
|
39
|
+
|
|
40
|
+
* make run_full_workflow smart and resumable from current phase ([930eb3c](https://github.com/programinglive/dev-workflow-mcp-server/commit/930eb3cb84c241b14f0e194cf490313e1e99a7ce))
|
|
41
|
+
|
|
42
|
+
### [1.1.8](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.1.7...v1.1.8) (2025-10-27)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
### π Documentation
|
|
46
|
+
|
|
47
|
+
* note DEV_WORKFLOW_STATE_FILE override ([bbeb11b](https://github.com/programinglive/dev-workflow-mcp-server/commit/bbeb11bc7f8be33f7c1a51b336283a3b2a48d3ea))
|
|
48
|
+
|
|
49
|
+
### [1.1.7](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.1.6...v1.1.7) (2025-10-26)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
### β¨ Features
|
|
53
|
+
|
|
54
|
+
* prompt for documentation after tests pass ([a87e44f](https://github.com/programinglive/dev-workflow-mcp-server/commit/a87e44f2a78ea4c9220d62868ee1f9c7ad3b149b))
|
|
55
|
+
|
|
5
56
|
### [1.1.6](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.1.5...v1.1.6) (2025-10-26)
|
|
6
57
|
|
|
7
58
|
|
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ Each project gets its own isolated workflow state file.
|
|
|
24
24
|
npm install @programinglive/dev-workflow-mcp-server
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
This will automatically create a `.workflow-state.json` file in the **project where you ran `npm install`** (using npm's `INIT_CWD`), keeping workflow history separate per project. If you're installing the package itself (inside `node_modules`), the script skips creation so it never pollutes the shared package directory.
|
|
27
|
+
This will automatically create a `.state/workflow-state.json` file in the **project where you ran `npm install`** (using npm's `INIT_CWD`), keeping workflow history separate per project. If you're installing the package itself (inside `node_modules`), the script skips creation so it never pollutes the shared package directory.
|
|
28
28
|
|
|
29
29
|
### Option 2: Install from Source
|
|
30
30
|
|
|
@@ -34,6 +34,11 @@ cd dev-workflow-mcp-server
|
|
|
34
34
|
npm install
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
#### Two Usage Modes
|
|
38
|
+
|
|
39
|
+
- **Local (source)**: Point your MCP client to `index.js`. This runs directly from source and requires no build step. Recommended for MCP usage.
|
|
40
|
+
- **Production (built)**: Run `npm run build` once to generate `dist/`. This creates an optimized bundle but isnβt needed for MCP usage.
|
|
41
|
+
|
|
37
42
|
### 3. Configure in Windsurf/Claude Desktop
|
|
38
43
|
|
|
39
44
|
Point your MCP client to the server entry point. Replace `<PROJECT_ROOT>` with the absolute path to this repository on your machine.
|
|
@@ -128,27 +133,109 @@ Point your MCP client to the server entry point. Replace `<PROJECT_ROOT>` with t
|
|
|
128
133
|
|
|
129
134
|
After adding the configuration, restart the application to load the MCP server.
|
|
130
135
|
|
|
136
|
+
## ποΈ Build System
|
|
137
|
+
|
|
138
|
+
This project includes a Vite-based build system for creating optimized distributions.
|
|
139
|
+
|
|
140
|
+
### Scripts
|
|
141
|
+
|
|
142
|
+
- `npm run build` - Bundle the source into `dist/index.mjs` for distribution
|
|
143
|
+
- `npm run dev` - Run in development mode with file watching
|
|
144
|
+
- `npm run local` - Alias for running from source (same as `npm start`)
|
|
145
|
+
|
|
146
|
+
### Build Output
|
|
147
|
+
|
|
148
|
+
Running `npm run build` generates:
|
|
149
|
+
- `dist/index.mjs` - Optimized ES module bundle
|
|
150
|
+
- Source maps and other build artifacts
|
|
151
|
+
|
|
152
|
+
The build bundles all source files while externalizing Node.js built-in modules and dependencies, resulting in a single file distribution.
|
|
153
|
+
|
|
154
|
+
### Usage
|
|
155
|
+
|
|
156
|
+
For MCP server usage, point your client at `index.js` (source) to avoid stdio transport compatibility issues. The built `dist/index.mjs` is primarily for:
|
|
157
|
+
- npm package distribution
|
|
158
|
+
- Performance optimization
|
|
159
|
+
- Embedding in other projects
|
|
160
|
+
|
|
131
161
|
## π Project-Specific Workflow State
|
|
132
162
|
|
|
133
|
-
When you install this package in a project, a `.workflow-state.json` file is automatically created in your project root. This file:
|
|
163
|
+
When you install this package in a project, a `.state/workflow-state.json` file is automatically created in your project root. This file:
|
|
134
164
|
|
|
135
165
|
- **Stores workflow history** specific to that project
|
|
136
166
|
- **Tracks task progress** independently per project
|
|
137
167
|
- **Should be gitignored** (already in `.gitignore` by default)
|
|
138
168
|
- **Persists across sessions** so your workflow state is preserved
|
|
169
|
+
- **Stays centralized** even if you run the server from nested build outputs like `dist/`. The MCP server walks back to the project root (looking for `.git` or `package.json`) before reading or writing workflow state, so you never need duplicate copies under build directories.
|
|
139
170
|
|
|
140
|
-
Each project maintains its own isolated workflow history, so you can work on multiple projects without mixing their histories.
|
|
171
|
+
Each project maintains its own isolated workflow history, so you can work on multiple projects without mixing their histories. Within that `.state` directory, the MCP server automatically creates a unique per-user subdirectory (e.g., `.state/users/user-abc123/`). The generated identifier persists locally so multiple developers sharing the same repository never clobber each otherβs workflow files. If you prefer a specific name, set `DEV_WORKFLOW_USER_ID` before launching the server and that value will be used instead of the auto-generated ID.
|
|
141
172
|
|
|
142
173
|
### Adding to .gitignore
|
|
143
174
|
|
|
144
175
|
If you're using this package, add this to your project's `.gitignore`:
|
|
145
176
|
|
|
146
177
|
```
|
|
147
|
-
.
|
|
178
|
+
.state/
|
|
148
179
|
```
|
|
149
180
|
|
|
150
181
|
This keeps workflow state local to each developer's machine.
|
|
151
182
|
|
|
183
|
+
> **Need to override the location?** Set `DEV_WORKFLOW_STATE_FILE=/absolute/path/to/your/project/.state/workflow-state.json` before launching the server (or inside your MCP client config). The server will honor that path, letting you keep the package installed centrally while maintaining per-project workflow history.
|
|
184
|
+
|
|
185
|
+
## π οΈ Available Tools
|
|
186
|
+
|
|
187
|
+
- `start_task` - Begin a new coding task
|
|
188
|
+
- `mark_bug_fixed` - Mark the feature/bug as fixed (requires tests next)
|
|
189
|
+
- `create_tests` - Mark that tests have been created
|
|
190
|
+
- `skip_tests` - Skip tests with justification
|
|
191
|
+
- `run_tests` - Record test results (must pass to proceed)
|
|
192
|
+
- `create_documentation` - Mark documentation as created
|
|
193
|
+
- `check_ready_to_commit` - Verify all steps are complete
|
|
194
|
+
- `commit_and_push` - Commit and push changes
|
|
195
|
+
- `perform_release` - Record release details
|
|
196
|
+
- `complete_task` - Mark task as complete and reset
|
|
197
|
+
- `force_complete_task` - Force completion with reason
|
|
198
|
+
- `drop_task` - Abandon current task
|
|
199
|
+
- `get_workflow_status` - Show current status
|
|
200
|
+
- `view_history` - View completed tasks
|
|
201
|
+
- `continue_workflow` - Get next-step guidance
|
|
202
|
+
- `rerun_workflow` - Reset and restart the current task from the beginning
|
|
203
|
+
- `run_full_workflow` - Execute every workflow step in sequence with a single command (requires supplying the details for each phase)
|
|
204
|
+
|
|
205
|
+
### `run_full_workflow`
|
|
206
|
+
|
|
207
|
+
Use this when you already have all the information needed for each workflow phase and want to execute them in one go.
|
|
208
|
+
|
|
209
|
+
```json
|
|
210
|
+
{
|
|
211
|
+
"summary": "Add payment webhooks",
|
|
212
|
+
"testCommand": "npm test",
|
|
213
|
+
"documentationType": "README",
|
|
214
|
+
"documentationSummary": "Document webhook configuration",
|
|
215
|
+
"commitMessage": "feat: add payment webhooks",
|
|
216
|
+
"releaseCommand": "npm run release:minor",
|
|
217
|
+
"releaseNotes": "Release webhook support",
|
|
218
|
+
"branch": "feature/payments",
|
|
219
|
+
"testsPassed": true,
|
|
220
|
+
"testDetails": "node --test; 42 tests",
|
|
221
|
+
"releaseType": "minor",
|
|
222
|
+
"preset": "minor"
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The tool will:
|
|
227
|
+
|
|
228
|
+
1. `mark_bug_fixed` using `summary`
|
|
229
|
+
2. `create_tests`
|
|
230
|
+
3. `run_tests` with `testsPassed`, `testCommand`, and optional `testDetails`
|
|
231
|
+
4. `create_documentation` with `documentationType` and `documentationSummary`
|
|
232
|
+
5. `check_ready_to_commit`
|
|
233
|
+
6. `commit_and_push` with `commitMessage` and optional `branch`
|
|
234
|
+
7. `perform_release` with `releaseCommand`, plus optional `releaseNotes`, `releaseType`, and `preset`
|
|
235
|
+
8. `complete_task` reusing `commitMessage`
|
|
236
|
+
|
|
237
|
+
All arguments except the optional flags are required and must be non-empty strings.
|
|
238
|
+
|
|
152
239
|
## π« Releasing Without the Workflow Steps
|
|
153
240
|
|
|
154
241
|
The package ships with a release guard (`release-wrapper.js`) that backs the `npm run release:*` scripts. The guard refuses to run unless:
|
|
@@ -372,7 +459,7 @@ You can modify the workflow in `index.js`:
|
|
|
372
459
|
|
|
373
460
|
## π State Management
|
|
374
461
|
|
|
375
|
-
The server maintains state in `.workflow-state.json`:
|
|
462
|
+
The server maintains state in `.state/workflow-state.json`:
|
|
376
463
|
- Current phase
|
|
377
464
|
- Task description
|
|
378
465
|
- Completion status of each step
|
package/db/index.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
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 DB_FILENAME = "dev-workflow.db";
|
|
9
|
+
const FREQUENCY_FORMATS = {
|
|
10
|
+
daily: "%Y-%m-%d",
|
|
11
|
+
monthly: "%Y-%m",
|
|
12
|
+
yearly: "%Y",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function getDbPath() {
|
|
16
|
+
const initCwd = process.env.INIT_CWD || process.cwd();
|
|
17
|
+
let projectRoot = initCwd;
|
|
18
|
+
while (projectRoot !== path.dirname(projectRoot)) {
|
|
19
|
+
if (existsSync(path.join(projectRoot, "package.json"))) break;
|
|
20
|
+
projectRoot = path.dirname(projectRoot);
|
|
21
|
+
}
|
|
22
|
+
return path.join(projectRoot, ".state", DB_FILENAME);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let db;
|
|
26
|
+
export function getDb() {
|
|
27
|
+
if (!db) {
|
|
28
|
+
const dbPath = getDbPath();
|
|
29
|
+
db = new Database(dbPath);
|
|
30
|
+
db.pragma("journal_mode = WAL");
|
|
31
|
+
migrate();
|
|
32
|
+
}
|
|
33
|
+
return db;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function migrate() {
|
|
37
|
+
const createHistory = `
|
|
38
|
+
CREATE TABLE IF NOT EXISTS workflow_history (
|
|
39
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
40
|
+
user_id TEXT NOT NULL,
|
|
41
|
+
task_description TEXT NOT NULL,
|
|
42
|
+
task_type TEXT NOT NULL,
|
|
43
|
+
commit_message TEXT,
|
|
44
|
+
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
45
|
+
forced INTEGER DEFAULT 0,
|
|
46
|
+
force_reason TEXT,
|
|
47
|
+
dropped INTEGER DEFAULT 0,
|
|
48
|
+
drop_reason TEXT,
|
|
49
|
+
UNIQUE(user_id, timestamp, task_description)
|
|
50
|
+
)
|
|
51
|
+
`;
|
|
52
|
+
const createSummary = `
|
|
53
|
+
CREATE TABLE IF NOT EXISTS project_summary (
|
|
54
|
+
user_id TEXT PRIMARY KEY,
|
|
55
|
+
total_tasks INTEGER DEFAULT 0,
|
|
56
|
+
task_types TEXT DEFAULT '{}',
|
|
57
|
+
last_active DATETIME,
|
|
58
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
59
|
+
recent_tasks TEXT DEFAULT '[]'
|
|
60
|
+
)
|
|
61
|
+
`;
|
|
62
|
+
const idxHistory = `CREATE INDEX IF NOT EXISTS idx_workflow_history_user ON workflow_history(user_id)`;
|
|
63
|
+
const idxSummary = `CREATE INDEX IF NOT EXISTS idx_project_summary_user ON project_summary(user_id)`;
|
|
64
|
+
|
|
65
|
+
db.exec(createHistory);
|
|
66
|
+
db.exec(createSummary);
|
|
67
|
+
db.exec(idxHistory);
|
|
68
|
+
db.exec(idxSummary);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function insertHistoryEntry(userId, entry) {
|
|
72
|
+
const stmt = getDb().prepare(`
|
|
73
|
+
INSERT OR REPLACE INTO workflow_history
|
|
74
|
+
(user_id, task_description, task_type, commit_message, timestamp, forced, force_reason, dropped, drop_reason)
|
|
75
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
76
|
+
`);
|
|
77
|
+
stmt.run(
|
|
78
|
+
userId,
|
|
79
|
+
entry.taskDescription,
|
|
80
|
+
entry.taskType,
|
|
81
|
+
entry.commitMessage,
|
|
82
|
+
entry.timestamp || new Date().toISOString(),
|
|
83
|
+
entry.forced ? 1 : 0,
|
|
84
|
+
entry.forceReason || null,
|
|
85
|
+
entry.dropped ? 1 : 0,
|
|
86
|
+
entry.dropReason || null
|
|
87
|
+
);
|
|
88
|
+
updateSummaryForUser(userId);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function updateSummaryForUser(userId) {
|
|
92
|
+
const getHistory = getDb().prepare(`
|
|
93
|
+
SELECT task_type, timestamp, task_description
|
|
94
|
+
FROM workflow_history
|
|
95
|
+
WHERE user_id = ?
|
|
96
|
+
ORDER BY timestamp DESC
|
|
97
|
+
LIMIT 20
|
|
98
|
+
`);
|
|
99
|
+
const rows = getHistory.all(userId);
|
|
100
|
+
|
|
101
|
+
const taskTypes = {};
|
|
102
|
+
for (const row of rows) {
|
|
103
|
+
taskTypes[row.task_type] = (taskTypes[row.task_type] || 0) + 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const recentTasks = rows.slice(0, 5).map(r => ({
|
|
107
|
+
description: r.task_description,
|
|
108
|
+
type: r.task_type,
|
|
109
|
+
timestamp: r.timestamp,
|
|
110
|
+
}));
|
|
111
|
+
|
|
112
|
+
const lastActive = rows[0]?.timestamp || null;
|
|
113
|
+
|
|
114
|
+
const upsert = getDb().prepare(`
|
|
115
|
+
INSERT OR REPLACE INTO project_summary
|
|
116
|
+
(user_id, total_tasks, task_types, last_active, recent_tasks, updated_at)
|
|
117
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
118
|
+
`);
|
|
119
|
+
upsert.run(
|
|
120
|
+
userId,
|
|
121
|
+
rows.length,
|
|
122
|
+
JSON.stringify(taskTypes),
|
|
123
|
+
lastActive,
|
|
124
|
+
JSON.stringify(recentTasks),
|
|
125
|
+
new Date().toISOString()
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function getSummaryForUser(userId) {
|
|
130
|
+
const stmt = getDb().prepare(`
|
|
131
|
+
SELECT * FROM project_summary WHERE user_id = ?
|
|
132
|
+
`);
|
|
133
|
+
const row = stmt.get(userId);
|
|
134
|
+
if (!row) return null;
|
|
135
|
+
return {
|
|
136
|
+
totalTasks: row.total_tasks,
|
|
137
|
+
taskTypes: JSON.parse(row.task_types),
|
|
138
|
+
lastActive: row.last_active,
|
|
139
|
+
recentTasks: JSON.parse(row.recent_tasks),
|
|
140
|
+
updatedAt: row.updated_at,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function buildHistoryFilters({ startDate, endDate }) {
|
|
145
|
+
const clauses = [];
|
|
146
|
+
const params = [];
|
|
147
|
+
if (startDate) {
|
|
148
|
+
clauses.push("DATE(timestamp) >= DATE(?)");
|
|
149
|
+
params.push(startDate);
|
|
150
|
+
}
|
|
151
|
+
if (endDate) {
|
|
152
|
+
clauses.push("DATE(timestamp) <= DATE(?)");
|
|
153
|
+
params.push(endDate);
|
|
154
|
+
}
|
|
155
|
+
return { clauses, params };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function getHistoryForUser(userId, options = {}) {
|
|
159
|
+
const { page = 1, pageSize = 20, startDate, endDate } = options;
|
|
160
|
+
const sanitizedPage = Math.max(1, Number.isFinite(Number(page)) ? Math.floor(Number(page)) : 1);
|
|
161
|
+
const sanitizedPageSize = Math.min(100, Math.max(1, Number.isFinite(Number(pageSize)) ? Math.floor(Number(pageSize)) : 20));
|
|
162
|
+
const offset = (sanitizedPage - 1) * sanitizedPageSize;
|
|
163
|
+
|
|
164
|
+
const { clauses, params } = buildHistoryFilters({ startDate, endDate });
|
|
165
|
+
const where = ["user_id = ?", ...clauses].join(" AND ");
|
|
166
|
+
const dbInstance = getDb();
|
|
167
|
+
|
|
168
|
+
const countStmt = dbInstance.prepare(`SELECT COUNT(*) as total FROM workflow_history WHERE ${where}`);
|
|
169
|
+
const total = countStmt.get(userId, ...params).total;
|
|
170
|
+
|
|
171
|
+
const rowsStmt = dbInstance.prepare(`
|
|
172
|
+
SELECT *
|
|
173
|
+
FROM workflow_history
|
|
174
|
+
WHERE ${where}
|
|
175
|
+
ORDER BY timestamp DESC
|
|
176
|
+
LIMIT ?
|
|
177
|
+
OFFSET ?
|
|
178
|
+
`);
|
|
179
|
+
const entries = rowsStmt.all(userId, ...params, sanitizedPageSize, offset);
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
entries,
|
|
183
|
+
total,
|
|
184
|
+
page: sanitizedPage,
|
|
185
|
+
pageSize: sanitizedPageSize,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function getHistorySummary(userId, options = {}) {
|
|
190
|
+
const { startDate, endDate, frequency = "daily" } = options;
|
|
191
|
+
const format = FREQUENCY_FORMATS[frequency] || FREQUENCY_FORMATS.daily;
|
|
192
|
+
const { clauses, params } = buildHistoryFilters({ startDate, endDate });
|
|
193
|
+
const where = ["user_id = ?", ...clauses].join(" AND ");
|
|
194
|
+
const stmt = getDb().prepare(`
|
|
195
|
+
SELECT strftime('${format}', timestamp) AS period, COUNT(*) AS count
|
|
196
|
+
FROM workflow_history
|
|
197
|
+
WHERE ${where}
|
|
198
|
+
GROUP BY period
|
|
199
|
+
ORDER BY period DESC
|
|
200
|
+
`);
|
|
201
|
+
return stmt
|
|
202
|
+
.all(userId, ...params)
|
|
203
|
+
.map((row) => ({ period: row.period, count: row.count }));
|
|
204
|
+
}
|
package/exec.js
CHANGED
|
@@ -1,4 +1,49 @@
|
|
|
1
1
|
import { exec as execCallback } from "child_process";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
2
5
|
import { promisify } from "util";
|
|
3
6
|
|
|
4
|
-
|
|
7
|
+
const execPromise = promisify(execCallback);
|
|
8
|
+
|
|
9
|
+
function findRepoRoot(startDir) {
|
|
10
|
+
if (!startDir) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let current = path.resolve(startDir);
|
|
15
|
+
const { root } = path.parse(current);
|
|
16
|
+
|
|
17
|
+
while (true) {
|
|
18
|
+
if (existsSync(path.join(current, ".git"))) {
|
|
19
|
+
return current;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (current === root) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const parent = path.dirname(current);
|
|
27
|
+
if (parent === current) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
current = parent;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getDefaultCwd() {
|
|
36
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
37
|
+
const candidateDirs = [process.cwd(), process.env.INIT_CWD, moduleDir];
|
|
38
|
+
return candidateDirs.reduce((resolved, candidate) => {
|
|
39
|
+
if (resolved) {
|
|
40
|
+
return resolved;
|
|
41
|
+
}
|
|
42
|
+
return findRepoRoot(candidate) || null;
|
|
43
|
+
}, null) || process.cwd();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function exec(command, options = {}) {
|
|
47
|
+
const execOptions = options.cwd ? options : { ...options, cwd: getDefaultCwd() };
|
|
48
|
+
return execPromise(command, execOptions);
|
|
49
|
+
}
|
package/index.js
CHANGED
|
@@ -32,8 +32,7 @@ export { createCommitMessageParts, determineReleaseTypeFromCommit } from "./comm
|
|
|
32
32
|
export { containsTestFilesInStatus, workingTreeSummary } from "./git-helpers.js";
|
|
33
33
|
export { WorkflowState } from "./workflow-state.js";
|
|
34
34
|
|
|
35
|
-
export
|
|
36
|
-
await workflowState.load();
|
|
35
|
+
export let workflowState = null;
|
|
37
36
|
|
|
38
37
|
const __filename = fileURLToPath(import.meta.url);
|
|
39
38
|
|
|
@@ -184,6 +183,17 @@ Use 'check_ready_to_commit' to verify workflow completion.
|
|
|
184
183
|
|
|
185
184
|
// Start server
|
|
186
185
|
export async function main() {
|
|
186
|
+
try {
|
|
187
|
+
// Initialize workflow state
|
|
188
|
+
workflowState = new WorkflowState();
|
|
189
|
+
await workflowState.load();
|
|
190
|
+
await workflowState.ensurePrimaryFile();
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.error("Warning: Failed to initialize workflow state:", error.message);
|
|
193
|
+
// Continue anyway - workflow state is optional for MCP to function
|
|
194
|
+
workflowState = new WorkflowState();
|
|
195
|
+
}
|
|
196
|
+
|
|
187
197
|
const transport = new StdioServerTransport();
|
|
188
198
|
await server.connect(transport);
|
|
189
199
|
console.error("Dev Workflow MCP Server running on stdio");
|
package/install.js
CHANGED
|
@@ -25,7 +25,11 @@ async function install() {
|
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
const
|
|
28
|
+
const stateDir = path.join(projectRoot, '.state');
|
|
29
|
+
const stateFile = path.join(stateDir, 'workflow-state.json');
|
|
30
|
+
|
|
31
|
+
// Create .state directory if it doesn't exist
|
|
32
|
+
await fs.mkdir(stateDir, { recursive: true });
|
|
29
33
|
|
|
30
34
|
// Check if state file already exists
|
|
31
35
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@programinglive/dev-workflow-mcp-server",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.11",
|
|
4
4
|
"description": "MCP server to enforce development workflow discipline",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -8,16 +8,19 @@
|
|
|
8
8
|
"dev-workflow-mcp": "index.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
+
"build": "vite build",
|
|
12
|
+
"dev": "node --watch index.js",
|
|
13
|
+
"start": "node index.js",
|
|
14
|
+
"local": "node index.js",
|
|
11
15
|
"test": "node --test",
|
|
12
16
|
"check": "node --check index.js",
|
|
13
|
-
"start": "node index.js",
|
|
14
|
-
"dev": "node --watch index.js",
|
|
15
17
|
"prepare": "husky",
|
|
16
18
|
"postinstall": "node install.js",
|
|
17
19
|
"release": "standard-version",
|
|
18
20
|
"release:major": "node release-wrapper.js major",
|
|
19
21
|
"release:minor": "node release-wrapper.js minor",
|
|
20
|
-
"release:patch": "node release-wrapper.js patch"
|
|
22
|
+
"release:patch": "node release-wrapper.js patch",
|
|
23
|
+
"web": "node web/server.js"
|
|
21
24
|
},
|
|
22
25
|
"keywords": [
|
|
23
26
|
"mcp",
|
|
@@ -28,7 +31,13 @@
|
|
|
28
31
|
"author": "",
|
|
29
32
|
"license": "MIT",
|
|
30
33
|
"dependencies": {
|
|
31
|
-
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
34
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
35
|
+
"better-sqlite3": "^11.8.0",
|
|
36
|
+
"esbuild": "^0.25.0",
|
|
37
|
+
"get-port": "^7.1.0",
|
|
38
|
+
"husky": "^9.1.7",
|
|
39
|
+
"standard-version": "^9.5.0",
|
|
40
|
+
"vite": "^7.1.12"
|
|
32
41
|
},
|
|
33
42
|
"publishConfig": {
|
|
34
43
|
"access": "public"
|
|
@@ -36,9 +45,7 @@
|
|
|
36
45
|
"devDependencies": {
|
|
37
46
|
"@commitlint/cli": "^20.1.0",
|
|
38
47
|
"@commitlint/config-conventional": "^20.0.0",
|
|
39
|
-
"@programinglive/commiter": "^1.
|
|
40
|
-
"husky": "^9.1.7",
|
|
41
|
-
"standard-version": "^9.5.0"
|
|
48
|
+
"@programinglive/commiter": "^1.1.0"
|
|
42
49
|
},
|
|
43
50
|
"standard-version": {
|
|
44
51
|
"releaseCommitMessageFormat": "chore(release): {{currentTag}} π",
|
package/release-wrapper.js
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { readFile, access } from 'fs/promises';
|
|
4
|
+
import { exec as execCallback } from 'child_process';
|
|
5
|
+
import { promisify } from 'util';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
|
|
8
|
+
const exec = promisify(execCallback);
|
|
9
|
+
|
|
10
|
+
test('build generates dist/index.mjs file', async () => {
|
|
11
|
+
const { stderr } = await exec('npm run build');
|
|
12
|
+
// Allow the known vite warning about node:process
|
|
13
|
+
const knownWarning = '[plugin vite:resolve] Module "node:process" has been externalized for browser compatibility';
|
|
14
|
+
const actualStderr = stderr.replace(/\x1B\[[0-9;]*m/g, ''); // Remove ANSI codes
|
|
15
|
+
assert(
|
|
16
|
+
actualStderr === '' || actualStderr.includes(knownWarning),
|
|
17
|
+
`Build should not have unexpected errors: ${actualStderr}`
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
// Verify the file exists
|
|
21
|
+
const distPath = path.join(process.cwd(), 'dist', 'index.mjs');
|
|
22
|
+
await access(distPath);
|
|
23
|
+
|
|
24
|
+
// Verify it contains expected content
|
|
25
|
+
const content = await readFile(distPath, 'utf-8');
|
|
26
|
+
assert(content.includes('main'), 'Bundle should contain main function');
|
|
27
|
+
assert(content.length > 1000, 'Bundle should be substantial');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('build script exists in package.json', async () => {
|
|
31
|
+
const packagePath = path.join(process.cwd(), 'package.json');
|
|
32
|
+
const content = await readFile(packagePath, 'utf-8');
|
|
33
|
+
const pkg = JSON.parse(content);
|
|
34
|
+
|
|
35
|
+
assert.strictEqual(typeof pkg.scripts.build, 'string');
|
|
36
|
+
assert(pkg.scripts.build.includes('vite build'));
|
|
37
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { exec } from '../exec.js';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { mkdtemp, rm } from 'fs/promises';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
|
|
8
|
+
test('exec runs commands successfully', async () => {
|
|
9
|
+
const { stdout } = await exec('echo "hello world"');
|
|
10
|
+
assert.strictEqual(stdout.trim(), 'hello world');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('exec respects explicit cwd option', async () => {
|
|
14
|
+
const tempDir = await mkdtemp(path.join(tmpdir(), 'mcp-test-'));
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const { stdout } = await exec('pwd', { cwd: tempDir });
|
|
18
|
+
// Just verify it runs in the specified directory (path may vary by platform)
|
|
19
|
+
assert(stdout.trim().includes('mcp-test-'));
|
|
20
|
+
} finally {
|
|
21
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('exec handles command errors', async () => {
|
|
26
|
+
try {
|
|
27
|
+
await exec('command-that-does-not-exist');
|
|
28
|
+
assert.fail('Should have thrown an error');
|
|
29
|
+
} catch (error) {
|
|
30
|
+
assert(error.code !== undefined || error.message.includes('command-not-found'));
|
|
31
|
+
}
|
|
32
|
+
});
|