devglide 0.1.1

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.
Files changed (252) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +338 -0
  3. package/bin/claude-md-template.js +94 -0
  4. package/bin/devglide.js +387 -0
  5. package/package.json +85 -0
  6. package/pnpm-workspace.yaml +3 -0
  7. package/src/apps/coder/.turbo/turbo-lint.log +5 -0
  8. package/src/apps/coder/package.json +16 -0
  9. package/src/apps/coder/public/favicon.svg +7 -0
  10. package/src/apps/coder/public/page.css +275 -0
  11. package/src/apps/coder/public/page.js +528 -0
  12. package/src/apps/coder/server.js +3 -0
  13. package/src/apps/documentation/public/page.css +597 -0
  14. package/src/apps/documentation/public/page.js +609 -0
  15. package/src/apps/kanban/.turbo/turbo-lint.log +97 -0
  16. package/src/apps/kanban/.turbo/turbo-typecheck.log +5 -0
  17. package/src/apps/kanban/package.json +32 -0
  18. package/src/apps/kanban/public/favicon.svg +7 -0
  19. package/src/apps/kanban/public/page.css +1010 -0
  20. package/src/apps/kanban/public/page.js +1730 -0
  21. package/src/apps/kanban/public/vendor/marked.min.js +6 -0
  22. package/src/apps/kanban/public/vendor/sortable.min.js +2 -0
  23. package/src/apps/kanban/src/db.ts +319 -0
  24. package/src/apps/kanban/src/index.ts +14 -0
  25. package/src/apps/kanban/src/mcp-helpers.test.ts +88 -0
  26. package/src/apps/kanban/src/mcp-helpers.ts +60 -0
  27. package/src/apps/kanban/src/mcp.ts +59 -0
  28. package/src/apps/kanban/src/routes/attachments.ts +161 -0
  29. package/src/apps/kanban/src/routes/features.ts +233 -0
  30. package/src/apps/kanban/src/routes/issues.ts +373 -0
  31. package/src/apps/kanban/src/tools/feature-tools.ts +164 -0
  32. package/src/apps/kanban/src/tools/item-tools.ts +307 -0
  33. package/src/apps/kanban/src/tools/versioned-entry-tools.ts +72 -0
  34. package/src/apps/kanban/tsconfig.check.json +9 -0
  35. package/src/apps/kanban/tsconfig.json +9 -0
  36. package/src/apps/keymap/.turbo/turbo-lint.log +5 -0
  37. package/src/apps/keymap/package.json +16 -0
  38. package/src/apps/keymap/public/page.css +275 -0
  39. package/src/apps/keymap/public/page.js +294 -0
  40. package/src/apps/keymap/server.js +25 -0
  41. package/src/apps/log/.turbo/turbo-build.log +5 -0
  42. package/src/apps/log/.turbo/turbo-lint.log +45 -0
  43. package/src/apps/log/.turbo/turbo-typecheck.log +5 -0
  44. package/src/apps/log/node_modules/.bin/tsc +21 -0
  45. package/src/apps/log/node_modules/.bin/tsserver +21 -0
  46. package/src/apps/log/node_modules/.bin/tsx +21 -0
  47. package/src/apps/log/package.json +36 -0
  48. package/src/apps/log/public/console-sniffer.js +221 -0
  49. package/src/apps/log/public/favicon.svg +7 -0
  50. package/src/apps/log/public/page.css +322 -0
  51. package/src/apps/log/public/page.js +463 -0
  52. package/src/apps/log/src/index.ts +9 -0
  53. package/src/apps/log/src/mcp.ts +122 -0
  54. package/src/apps/log/src/routes/log.ts +333 -0
  55. package/src/apps/log/src/routes/status.ts +25 -0
  56. package/src/apps/log/src/server-sniffer.ts +118 -0
  57. package/src/apps/log/src/services/file-patterns.ts +39 -0
  58. package/src/apps/log/src/services/file-tailer.ts +228 -0
  59. package/src/apps/log/src/services/line-parser.ts +94 -0
  60. package/src/apps/log/src/services/log-writer.ts +39 -0
  61. package/src/apps/log/tsconfig.json +8 -0
  62. package/src/apps/prompts/.turbo/turbo-build.log +5 -0
  63. package/src/apps/prompts/.turbo/turbo-lint.log +24 -0
  64. package/src/apps/prompts/.turbo/turbo-typecheck.log +5 -0
  65. package/src/apps/prompts/mcp.ts +175 -0
  66. package/src/apps/prompts/node_modules/.bin/tsc +21 -0
  67. package/src/apps/prompts/node_modules/.bin/tsserver +21 -0
  68. package/src/apps/prompts/node_modules/.bin/tsx +21 -0
  69. package/src/apps/prompts/package.json +25 -0
  70. package/src/apps/prompts/public/page.css +315 -0
  71. package/src/apps/prompts/public/page.js +541 -0
  72. package/src/apps/prompts/services/prompt-store.ts +212 -0
  73. package/src/apps/prompts/src/index.ts +9 -0
  74. package/src/apps/prompts/tsconfig.json +8 -0
  75. package/src/apps/prompts/types.ts +27 -0
  76. package/src/apps/shell/.turbo/turbo-build.log +5 -0
  77. package/src/apps/shell/.turbo/turbo-lint.log +34 -0
  78. package/src/apps/shell/.turbo/turbo-typecheck.log +5 -0
  79. package/src/apps/shell/package.json +35 -0
  80. package/src/apps/shell/public/favicon.svg +7 -0
  81. package/src/apps/shell/public/page.css +407 -0
  82. package/src/apps/shell/public/page.js +1577 -0
  83. package/src/apps/shell/src/index.ts +150 -0
  84. package/src/apps/shell/src/mcp.ts +398 -0
  85. package/src/apps/shell/src/shell-types.ts +41 -0
  86. package/src/apps/shell/tsconfig.json +8 -0
  87. package/src/apps/test/.turbo/turbo-build.log +5 -0
  88. package/src/apps/test/.turbo/turbo-lint.log +27 -0
  89. package/src/apps/test/.turbo/turbo-typecheck.log +5 -0
  90. package/src/apps/test/node_modules/.bin/tsc +21 -0
  91. package/src/apps/test/node_modules/.bin/tsserver +21 -0
  92. package/src/apps/test/node_modules/.bin/tsx +21 -0
  93. package/src/apps/test/node_modules/.bin/uuid +21 -0
  94. package/src/apps/test/package.json +35 -0
  95. package/src/apps/test/public/favicon.svg +7 -0
  96. package/src/apps/test/public/page.css +499 -0
  97. package/src/apps/test/public/page.js +417 -0
  98. package/src/apps/test/public/scenario-runner.js +450 -0
  99. package/src/apps/test/src/index.ts +9 -0
  100. package/src/apps/test/src/mcp.ts +192 -0
  101. package/src/apps/test/src/routes/trigger.ts +285 -0
  102. package/src/apps/test/src/services/scenario-broadcaster.ts +60 -0
  103. package/src/apps/test/src/services/scenario-manager.ts +361 -0
  104. package/src/apps/test/src/services/scenario-store.ts +145 -0
  105. package/src/apps/test/tsconfig.json +8 -0
  106. package/src/apps/vocabulary/.turbo/turbo-build.log +5 -0
  107. package/src/apps/vocabulary/.turbo/turbo-lint.log +25 -0
  108. package/src/apps/vocabulary/.turbo/turbo-typecheck.log +5 -0
  109. package/src/apps/vocabulary/mcp.ts +173 -0
  110. package/src/apps/vocabulary/node_modules/.bin/tsc +21 -0
  111. package/src/apps/vocabulary/node_modules/.bin/tsserver +21 -0
  112. package/src/apps/vocabulary/node_modules/.bin/tsx +21 -0
  113. package/src/apps/vocabulary/package.json +25 -0
  114. package/src/apps/vocabulary/public/page.css +247 -0
  115. package/src/apps/vocabulary/public/page.js +444 -0
  116. package/src/apps/vocabulary/services/vocabulary-store.ts +179 -0
  117. package/src/apps/vocabulary/src/index.ts +10 -0
  118. package/src/apps/vocabulary/tsconfig.json +8 -0
  119. package/src/apps/vocabulary/types.ts +22 -0
  120. package/src/apps/voice/.turbo/turbo-build.log +5 -0
  121. package/src/apps/voice/.turbo/turbo-lint.log +43 -0
  122. package/src/apps/voice/.turbo/turbo-typecheck.log +5 -0
  123. package/src/apps/voice/node_modules/.bin/openai +21 -0
  124. package/src/apps/voice/node_modules/.bin/tsc +21 -0
  125. package/src/apps/voice/node_modules/.bin/tsserver +21 -0
  126. package/src/apps/voice/node_modules/.bin/tsx +21 -0
  127. package/src/apps/voice/package.json +35 -0
  128. package/src/apps/voice/public/favicon.svg +7 -0
  129. package/src/apps/voice/public/page.css +388 -0
  130. package/src/apps/voice/public/page.js +718 -0
  131. package/src/apps/voice/src/index.ts +10 -0
  132. package/src/apps/voice/src/mcp.ts +70 -0
  133. package/src/apps/voice/src/providers/index.ts +85 -0
  134. package/src/apps/voice/src/providers/openai-compatible.ts +94 -0
  135. package/src/apps/voice/src/providers/types.ts +27 -0
  136. package/src/apps/voice/src/routes/config.ts +118 -0
  137. package/src/apps/voice/src/routes/transcribe.ts +90 -0
  138. package/src/apps/voice/src/services/config-store.ts +129 -0
  139. package/src/apps/voice/src/services/stats.ts +108 -0
  140. package/src/apps/voice/src/transcribe.ts +11 -0
  141. package/src/apps/voice/src/utils/mime.ts +16 -0
  142. package/src/apps/voice/tsconfig.json +8 -0
  143. package/src/apps/workflow/.turbo/turbo-build.log +5 -0
  144. package/src/apps/workflow/.turbo/turbo-lint.log +96 -0
  145. package/src/apps/workflow/.turbo/turbo-typecheck.log +5 -0
  146. package/src/apps/workflow/engine/executors/decision-executor.ts +87 -0
  147. package/src/apps/workflow/engine/executors/file-executor.ts +90 -0
  148. package/src/apps/workflow/engine/executors/git-executor.ts +137 -0
  149. package/src/apps/workflow/engine/executors/http-executor.ts +65 -0
  150. package/src/apps/workflow/engine/executors/index.ts +28 -0
  151. package/src/apps/workflow/engine/executors/kanban-executor.ts +154 -0
  152. package/src/apps/workflow/engine/executors/llm-executor.ts +46 -0
  153. package/src/apps/workflow/engine/executors/log-executor.ts +62 -0
  154. package/src/apps/workflow/engine/executors/loop-executor.ts +14 -0
  155. package/src/apps/workflow/engine/executors/shell-executor.ts +107 -0
  156. package/src/apps/workflow/engine/executors/sub-workflow-executor.ts +61 -0
  157. package/src/apps/workflow/engine/executors/test-executor.ts +73 -0
  158. package/src/apps/workflow/engine/executors/trigger-executor.ts +39 -0
  159. package/src/apps/workflow/engine/expression-evaluator.ts +117 -0
  160. package/src/apps/workflow/engine/graph-runner.ts +438 -0
  161. package/src/apps/workflow/engine/node-executor.ts +104 -0
  162. package/src/apps/workflow/engine/node-registry.ts +15 -0
  163. package/src/apps/workflow/engine/variable-resolver.ts +109 -0
  164. package/src/apps/workflow/mcp.ts +223 -0
  165. package/src/apps/workflow/node_modules/.bin/tsc +21 -0
  166. package/src/apps/workflow/node_modules/.bin/tsserver +21 -0
  167. package/src/apps/workflow/node_modules/.bin/tsx +21 -0
  168. package/src/apps/workflow/package.json +25 -0
  169. package/src/apps/workflow/public/editor/canvas.js +366 -0
  170. package/src/apps/workflow/public/editor/drag-manager.js +326 -0
  171. package/src/apps/workflow/public/editor/edge-renderer.js +235 -0
  172. package/src/apps/workflow/public/editor/history-manager.js +147 -0
  173. package/src/apps/workflow/public/editor/layout-engine.js +159 -0
  174. package/src/apps/workflow/public/editor/node-renderer.js +199 -0
  175. package/src/apps/workflow/public/editor/selection-manager.js +193 -0
  176. package/src/apps/workflow/public/favicon.svg +7 -0
  177. package/src/apps/workflow/public/models/node-types.js +300 -0
  178. package/src/apps/workflow/public/models/workflow-model.js +257 -0
  179. package/src/apps/workflow/public/page.css +406 -0
  180. package/src/apps/workflow/public/page.js +658 -0
  181. package/src/apps/workflow/public/panels/inspector.js +360 -0
  182. package/src/apps/workflow/public/panels/palette.js +106 -0
  183. package/src/apps/workflow/public/panels/run-view.js +275 -0
  184. package/src/apps/workflow/public/panels/toolbar.js +232 -0
  185. package/src/apps/workflow/public/panels/workflow-list.js +237 -0
  186. package/src/apps/workflow/public/state/store.js +47 -0
  187. package/src/apps/workflow/services/custom-node-loader.ts +48 -0
  188. package/src/apps/workflow/services/legacy-converter.ts +72 -0
  189. package/src/apps/workflow/services/run-manager.ts +190 -0
  190. package/src/apps/workflow/services/workflow-store.ts +424 -0
  191. package/src/apps/workflow/services/workflow-validator.test.ts +103 -0
  192. package/src/apps/workflow/services/workflow-validator.ts +98 -0
  193. package/src/apps/workflow/src/index.ts +10 -0
  194. package/src/apps/workflow/templates/ci-pipeline.json +18 -0
  195. package/src/apps/workflow/templates/code-review.json +22 -0
  196. package/src/apps/workflow/templates/kanban-testing.json +24 -0
  197. package/src/apps/workflow/tsconfig.json +8 -0
  198. package/src/apps/workflow/types.ts +268 -0
  199. package/src/packages/auth-middleware.ts +14 -0
  200. package/src/packages/design-tokens/.turbo/turbo-build.log +10 -0
  201. package/src/packages/design-tokens/STYLEGUIDE.md +414 -0
  202. package/src/packages/design-tokens/build.js +413 -0
  203. package/src/packages/design-tokens/demo/index.html +1367 -0
  204. package/src/packages/design-tokens/demo/proposition-a.html +717 -0
  205. package/src/packages/design-tokens/demo/proposition-b.html +1239 -0
  206. package/src/packages/design-tokens/demo/proposition-c.html +1049 -0
  207. package/src/packages/design-tokens/dist/tailwind-preset.js +115 -0
  208. package/src/packages/design-tokens/dist/tokens.css +345 -0
  209. package/src/packages/design-tokens/dist/tokens.d.ts +229 -0
  210. package/src/packages/design-tokens/dist/tokens.js +386 -0
  211. package/src/packages/design-tokens/package.json +25 -0
  212. package/src/packages/design-tokens/tokens.json +228 -0
  213. package/src/packages/devtools-middleware.ts +22 -0
  214. package/src/packages/eslint-config/index.js +63 -0
  215. package/src/packages/eslint-config/node_modules/.bin/eslint +21 -0
  216. package/src/packages/eslint-config/package.json +18 -0
  217. package/src/packages/json-file-store.ts +232 -0
  218. package/src/packages/mcp-utils/.turbo/turbo-build.log +5 -0
  219. package/src/packages/mcp-utils/dist/index.d.ts +33 -0
  220. package/src/packages/mcp-utils/dist/index.d.ts.map +1 -0
  221. package/src/packages/mcp-utils/dist/index.js +126 -0
  222. package/src/packages/mcp-utils/dist/index.js.map +1 -0
  223. package/src/packages/mcp-utils/node_modules/.bin/tsc +21 -0
  224. package/src/packages/mcp-utils/node_modules/.bin/tsserver +21 -0
  225. package/src/packages/mcp-utils/package.json +32 -0
  226. package/src/packages/mcp-utils/src/index.ts +171 -0
  227. package/src/packages/mcp-utils/tsconfig.json +9 -0
  228. package/src/packages/paths.ts +18 -0
  229. package/src/packages/project-context/index.js +55 -0
  230. package/src/packages/project-context/package.json +13 -0
  231. package/src/packages/project-store.ts +127 -0
  232. package/src/packages/server-sniffer.ts +132 -0
  233. package/src/packages/shared-assets/favicon.svg +7 -0
  234. package/src/packages/shared-assets/keymap-registry.js +512 -0
  235. package/src/packages/shared-assets/logo.svg +6 -0
  236. package/src/packages/shared-assets/package.json +11 -0
  237. package/src/packages/shared-assets/ui-utils.js +48 -0
  238. package/src/packages/shared-assets/voice-widget.d.ts +37 -0
  239. package/src/packages/shared-assets/voice-widget.js +695 -0
  240. package/src/packages/shared-types/.turbo/turbo-build.log +5 -0
  241. package/src/packages/shared-types/dist/index.d.ts +39 -0
  242. package/src/packages/shared-types/dist/index.d.ts.map +1 -0
  243. package/src/packages/shared-types/node_modules/.bin/tsc +21 -0
  244. package/src/packages/shared-types/node_modules/.bin/tsserver +21 -0
  245. package/src/packages/shared-types/package.json +25 -0
  246. package/src/packages/shared-types/src/index.ts +41 -0
  247. package/src/packages/shared-types/tsconfig.json +11 -0
  248. package/src/packages/tsconfig/base.json +15 -0
  249. package/src/packages/tsconfig/next.json +14 -0
  250. package/src/packages/tsconfig/node.json +11 -0
  251. package/src/packages/tsconfig/package.json +10 -0
  252. package/turbo.json +25 -0
@@ -0,0 +1,373 @@
1
+ import { Router, Request, Response } from "express";
2
+ import { getDb, generateId, nowIso, appendVersionedEntry, getVersionedEntries } from "../db.js";
3
+ import path from "path";
4
+ import fs from "fs";
5
+ import { fileURLToPath } from "url";
6
+
7
+ declare module "express" {
8
+ interface Request {
9
+ projectId?: string;
10
+ }
11
+ }
12
+
13
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
+ const uploadsDir = path.join(__dirname, "..", "..", "uploads");
15
+
16
+ export const issuesRouter: Router = Router();
17
+
18
+ function mapIssue(row: any) {
19
+ if (!row) return row;
20
+ const { projectId, ...rest } = row;
21
+ return { ...rest, featureId: projectId };
22
+ }
23
+
24
+ // GET /api/issues
25
+ issuesRouter.get("/", (req: Request, res: Response) => {
26
+ try {
27
+ const { featureId, columnId, priority, type } = req.query;
28
+ const db = getDb(req.projectId);
29
+
30
+ const conditions: string[] = [];
31
+ const params: any[] = [];
32
+
33
+ if (featureId) {
34
+ conditions.push(`i."projectId" = ?`);
35
+ params.push(featureId);
36
+ }
37
+ if (columnId) {
38
+ conditions.push(`i."columnId" = ?`);
39
+ params.push(columnId);
40
+ }
41
+ if (priority) {
42
+ conditions.push(`i."priority" = ?`);
43
+ params.push(priority);
44
+ }
45
+ if (type) {
46
+ conditions.push(`i."type" = ?`);
47
+ params.push(type);
48
+ }
49
+
50
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
51
+
52
+ const rows = db
53
+ .prepare(
54
+ `SELECT i.*, c.name AS columnName, c."order" AS columnOrder, c.color AS columnColor
55
+ FROM "Issue" i
56
+ LEFT JOIN "Column" c ON i."columnId" = c."id"
57
+ ${where}
58
+ ORDER BY i."order" ASC`
59
+ )
60
+ .all(...params);
61
+
62
+ res.json(rows.map(mapIssue));
63
+ } catch (err: unknown) {
64
+ res.status(500).json({ error: (err instanceof Error ? err.message : String(err)) });
65
+ }
66
+ });
67
+
68
+ const VALID_PRIORITIES = ["CRITICAL", "HIGH", "MEDIUM", "LOW", "NONE"];
69
+ const VALID_TYPES = ["BUG", "TASK", "FEATURE", "IMPROVEMENT", "EPIC"];
70
+
71
+ // POST /api/issues
72
+ issuesRouter.post("/", (req: Request, res: Response) => {
73
+ try {
74
+ const { title, description, priority, type, labels, dueDate, featureId, columnId } = req.body;
75
+
76
+ if (!title || !featureId || !columnId) {
77
+ res.status(400).json({ error: "title, featureId, and columnId are required" });
78
+ return;
79
+ }
80
+
81
+ if (typeof title !== "string" || title.length > 500) {
82
+ res.status(400).json({ error: "title must be a string of at most 500 characters" });
83
+ return;
84
+ }
85
+
86
+ if (priority && !VALID_PRIORITIES.includes(priority)) {
87
+ res.status(400).json({ error: `priority must be one of: ${VALID_PRIORITIES.join(", ")}` });
88
+ return;
89
+ }
90
+
91
+ if (type && !VALID_TYPES.includes(type)) {
92
+ res.status(400).json({ error: `type must be one of: ${VALID_TYPES.join(", ")}` });
93
+ return;
94
+ }
95
+
96
+ const db = getDb(req.projectId);
97
+ const now = nowIso();
98
+ const id = generateId();
99
+
100
+ // Calculate order: max order in target column + 1
101
+ const maxOrder = db
102
+ .prepare(`SELECT MAX("order") AS maxOrd FROM "Issue" WHERE "columnId" = ?`)
103
+ .get(columnId) as any;
104
+ const order = (maxOrder?.maxOrd ?? -1) + 1;
105
+
106
+ db.prepare(
107
+ `INSERT INTO "Issue" ("id", "title", "description", "type", "priority", "order", "labels", "dueDate", "projectId", "columnId", "updatedAt")
108
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
109
+ ).run(
110
+ id,
111
+ title,
112
+ description ?? null,
113
+ type ?? "TASK",
114
+ priority ?? "MEDIUM",
115
+ order,
116
+ labels ?? "[]",
117
+ dueDate ?? null,
118
+ featureId,
119
+ columnId,
120
+ now
121
+ );
122
+
123
+ const row = db.prepare(`SELECT * FROM "Issue" WHERE "id" = ?`).get(id);
124
+ res.status(201).json(mapIssue(row));
125
+ } catch (err: unknown) {
126
+ res.status(500).json({ error: (err instanceof Error ? err.message : String(err)) });
127
+ }
128
+ });
129
+
130
+ // POST /api/issues/reorder (defined before /:id to avoid route conflict)
131
+ issuesRouter.post("/reorder", (req: Request, res: Response) => {
132
+ try {
133
+ const { issueId, newColumnId, newOrder } = req.body;
134
+
135
+ if (!issueId || !newColumnId || newOrder === undefined) {
136
+ res.status(400).json({ error: "issueId, newColumnId, and newOrder are required" });
137
+ return;
138
+ }
139
+
140
+ const db = getDb(req.projectId);
141
+ const now = nowIso();
142
+
143
+ const txn = db.transaction(() => {
144
+ // Verify issue exists BEFORE shifting
145
+ const existing = db.prepare(`SELECT * FROM "Issue" WHERE "id" = ?`).get(issueId);
146
+ if (!existing) return null;
147
+
148
+ // Shift existing issues in target column
149
+ db.prepare(
150
+ `UPDATE "Issue" SET "order" = "order" + 1, "updatedAt" = ? WHERE "columnId" = ? AND "order" >= ?`
151
+ ).run(now, newColumnId, newOrder);
152
+
153
+ // Update the moved issue
154
+ db.prepare(
155
+ `UPDATE "Issue" SET "columnId" = ?, "order" = ?, "updatedAt" = ? WHERE "id" = ?`
156
+ ).run(newColumnId, newOrder, now, issueId);
157
+
158
+ return existing;
159
+ });
160
+
161
+ const result = txn();
162
+ if (!result) {
163
+ res.status(404).json({ error: "Issue not found" });
164
+ return;
165
+ }
166
+
167
+ const row = db.prepare(`SELECT * FROM "Issue" WHERE "id" = ?`).get(issueId);
168
+
169
+ res.json(mapIssue(row));
170
+ } catch (err: unknown) {
171
+ res.status(500).json({ error: (err instanceof Error ? err.message : String(err)) });
172
+ }
173
+ });
174
+
175
+ // GET /api/issues/:id
176
+ issuesRouter.get("/:id", (req: Request, res: Response) => {
177
+ try {
178
+ const id = req.params.id as string;
179
+ const db = getDb(req.projectId);
180
+
181
+ const row = db
182
+ .prepare(
183
+ `SELECT i.*,
184
+ c.name AS columnName, c."order" AS columnOrder, c.color AS columnColor,
185
+ p.name AS featureName, p.description AS featureDescription, p.color AS featureColor
186
+ FROM "Issue" i
187
+ LEFT JOIN "Column" c ON i."columnId" = c."id"
188
+ LEFT JOIN "Project" p ON i."projectId" = p."id"
189
+ WHERE i."id" = ?`
190
+ )
191
+ .get(id);
192
+
193
+ if (!row) {
194
+ res.status(404).json({ error: "Issue not found" });
195
+ return;
196
+ }
197
+
198
+ const attachments = db
199
+ .prepare(`SELECT * FROM "Attachment" WHERE "issueId" = ?`)
200
+ .all(id);
201
+
202
+ const mapped = mapIssue(row);
203
+ mapped.attachments = attachments;
204
+ mapped.workLog = getVersionedEntries(db, id, "work_log");
205
+ mapped.reviewHistory = getVersionedEntries(db, id, "review");
206
+
207
+ res.json(mapped);
208
+ } catch (err: unknown) {
209
+ res.status(500).json({ error: (err instanceof Error ? err.message : String(err)) });
210
+ }
211
+ });
212
+
213
+ // PATCH /api/issues/:id
214
+ issuesRouter.patch("/:id", (req: Request, res: Response) => {
215
+ try {
216
+ const id = req.params.id as string;
217
+ const db = getDb(req.projectId);
218
+
219
+ // Check issue exists
220
+ const existing = db.prepare(`SELECT * FROM "Issue" WHERE "id" = ?`).get(id);
221
+ if (!existing) {
222
+ res.status(404).json({ error: "Issue not found" });
223
+ return;
224
+ }
225
+
226
+ const allowedFields: Record<string, string> = {
227
+ title: '"title"',
228
+ description: '"description"',
229
+ priority: '"priority"',
230
+ labels: '"labels"',
231
+ dueDate: '"dueDate"',
232
+ columnId: '"columnId"',
233
+ order: '"order"',
234
+ type: '"type"',
235
+ };
236
+
237
+ // Redirect reviewFeedback to versioned entry
238
+ if (req.body.reviewFeedback && typeof req.body.reviewFeedback === "string" && req.body.reviewFeedback.trim()) {
239
+ appendVersionedEntry(db, id, "review", req.body.reviewFeedback.trim());
240
+ }
241
+
242
+ const setClauses: string[] = [];
243
+ const params: any[] = [];
244
+
245
+ for (const [key, col] of Object.entries(allowedFields)) {
246
+ if (req.body[key] !== undefined) {
247
+ setClauses.push(`${col} = ?`);
248
+ params.push(req.body[key]);
249
+ }
250
+ }
251
+
252
+ if (setClauses.length === 0) {
253
+ res.status(400).json({ error: "No valid fields to update" });
254
+ return;
255
+ }
256
+
257
+ // Always set updatedAt
258
+ const now = nowIso();
259
+ setClauses.push(`"updatedAt" = ?`);
260
+ params.push(now);
261
+
262
+ params.push(id);
263
+
264
+ db.prepare(`UPDATE "Issue" SET ${setClauses.join(", ")} WHERE "id" = ?`).run(...params);
265
+
266
+ const row = db.prepare(`SELECT * FROM "Issue" WHERE "id" = ?`).get(id);
267
+ res.json(mapIssue(row));
268
+ } catch (err: unknown) {
269
+ res.status(500).json({ error: (err instanceof Error ? err.message : String(err)) });
270
+ }
271
+ });
272
+
273
+ // GET /api/issues/:id/work-log
274
+ issuesRouter.get("/:id/work-log", (req: Request, res: Response) => {
275
+ try {
276
+ const id = req.params.id as string;
277
+ const db = getDb(req.projectId);
278
+ const entries = getVersionedEntries(db, id, "work_log");
279
+ res.json(entries);
280
+ } catch (err: unknown) {
281
+ res.status(500).json({ error: (err instanceof Error ? err.message : String(err)) });
282
+ }
283
+ });
284
+
285
+ // POST /api/issues/:id/work-log
286
+ issuesRouter.post("/:id/work-log", (req: Request, res: Response) => {
287
+ try {
288
+ const id = req.params.id as string;
289
+ const { content } = req.body;
290
+ if (!content || typeof content !== "string" || !content.trim()) {
291
+ res.status(400).json({ error: "content is required" });
292
+ return;
293
+ }
294
+ const db = getDb(req.projectId);
295
+ const existing = db.prepare(`SELECT "id" FROM "Issue" WHERE "id" = ?`).get(id);
296
+ if (!existing) { res.status(404).json({ error: "Issue not found" }); return; }
297
+ const entry = appendVersionedEntry(db, id, "work_log", content.trim());
298
+ db.prepare(`UPDATE "Issue" SET "updatedAt" = ? WHERE "id" = ?`).run(nowIso(), id);
299
+ res.status(201).json(entry);
300
+ } catch (err: unknown) {
301
+ res.status(500).json({ error: (err instanceof Error ? err.message : String(err)) });
302
+ }
303
+ });
304
+
305
+ // GET /api/issues/:id/review
306
+ issuesRouter.get("/:id/review", (req: Request, res: Response) => {
307
+ try {
308
+ const id = req.params.id as string;
309
+ const db = getDb(req.projectId);
310
+ const entries = getVersionedEntries(db, id, "review");
311
+ res.json(entries);
312
+ } catch (err: unknown) {
313
+ res.status(500).json({ error: (err instanceof Error ? err.message : String(err)) });
314
+ }
315
+ });
316
+
317
+ // POST /api/issues/:id/review
318
+ issuesRouter.post("/:id/review", (req: Request, res: Response) => {
319
+ try {
320
+ const id = req.params.id as string;
321
+ const { content } = req.body;
322
+ if (!content || typeof content !== "string" || !content.trim()) {
323
+ res.status(400).json({ error: "content is required" });
324
+ return;
325
+ }
326
+ const db = getDb(req.projectId);
327
+ const existing = db.prepare(`SELECT "id" FROM "Issue" WHERE "id" = ?`).get(id);
328
+ if (!existing) { res.status(404).json({ error: "Issue not found" }); return; }
329
+ const entry = appendVersionedEntry(db, id, "review", content.trim());
330
+ db.prepare(`UPDATE "Issue" SET "updatedAt" = ? WHERE "id" = ?`).run(nowIso(), id);
331
+ res.status(201).json(entry);
332
+ } catch (err: unknown) {
333
+ res.status(500).json({ error: (err instanceof Error ? err.message : String(err)) });
334
+ }
335
+ });
336
+
337
+ // DELETE /api/issues/:id
338
+ issuesRouter.delete("/:id", (req: Request, res: Response) => {
339
+ try {
340
+ const id = req.params.id as string;
341
+ const db = getDb(req.projectId);
342
+
343
+ // Check issue exists
344
+ const existing = db.prepare(`SELECT * FROM "Issue" WHERE "id" = ?`).get(id);
345
+ if (!existing) {
346
+ res.status(404).json({ error: "Issue not found" });
347
+ return;
348
+ }
349
+
350
+ // Fetch attachments so we can delete files from disk
351
+ const attachments = db
352
+ .prepare(`SELECT * FROM "Attachment" WHERE "issueId" = ?`)
353
+ .all(id) as any[];
354
+
355
+ // Delete attachment files from disk
356
+ for (const att of attachments) {
357
+ const ext = path.extname(att.filename);
358
+ const filePath = path.join(uploadsDir, `${att.id}${ext}`);
359
+ try {
360
+ fs.unlinkSync(filePath);
361
+ } catch {
362
+ // Ignore errors (file may not exist)
363
+ }
364
+ }
365
+
366
+ // Delete the issue (CASCADE handles attachment DB records)
367
+ db.prepare(`DELETE FROM "Issue" WHERE "id" = ?`).run(id);
368
+
369
+ res.json({ success: true });
370
+ } catch (err: unknown) {
371
+ res.status(500).json({ error: (err instanceof Error ? err.message : String(err)) });
372
+ }
373
+ });
@@ -0,0 +1,164 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { getDb, generateId, nowIso, type ColumnRow, type IssueRow } from "../db.js";
4
+ import { jsonResult, errorResult } from "../../../../packages/mcp-utils/src/index.js";
5
+ import { DEFAULT_COLUMNS, mapColumnRow, mapIssueRow } from "../mcp-helpers.js";
6
+
7
+ export function registerFeatureTools(server: McpServer, projectId?: string | null): void {
8
+
9
+ // ── kanban_list_features ──────────────────────────────────────────────────
10
+
11
+ server.tool(
12
+ "kanban_list_features",
13
+ "List all features in the current project. Features represent product initiatives or modules, each with its own kanban board of tasks and bugs.",
14
+ {
15
+ limit: z.coerce.number().int().min(1).max(100).optional().describe("Max items to return (default 25, max 100)"),
16
+ offset: z.coerce.number().int().min(0).optional().describe("Number of items to skip (default 0)"),
17
+ },
18
+ async ({ limit, offset }) => {
19
+ const db = getDb(projectId);
20
+ const take = limit ?? 25;
21
+ const skip = offset ?? 0;
22
+
23
+ const totalRow = db.prepare(`SELECT COUNT(*) AS cnt FROM "Project"`).get() as any;
24
+ const total = totalRow.cnt;
25
+
26
+ const features = db
27
+ .prepare(
28
+ `SELECT p.*,
29
+ (SELECT COUNT(*) FROM "Issue" i WHERE i."projectId" = p."id") AS issueCount
30
+ FROM "Project" p
31
+ ORDER BY p."name" COLLATE NOCASE ASC
32
+ LIMIT ? OFFSET ?`
33
+ )
34
+ .all(take, skip);
35
+
36
+ return jsonResult({
37
+ data: features,
38
+ pagination: { total, limit: take, offset: skip, hasMore: skip + take < total },
39
+ });
40
+ }
41
+ );
42
+
43
+ // ── kanban_create_feature ─────────────────────────────────────────────────
44
+
45
+ server.tool(
46
+ "kanban_create_feature",
47
+ "Create a new feature with default kanban columns. A feature represents a product initiative or module.",
48
+ {
49
+ name: z.string().describe("Feature name"),
50
+ description: z.string().optional().describe("Feature description"),
51
+ color: z.string().optional().describe('Hex color e.g. #6366f1'),
52
+ },
53
+ async ({ name, description, color }) => {
54
+ const db = getDb(projectId);
55
+ const now = nowIso();
56
+ const featureId = generateId();
57
+
58
+ const txn = db.transaction(() => {
59
+ db.prepare(
60
+ `INSERT INTO "Project" ("id", "name", "description", "color", "updatedAt")
61
+ VALUES (?, ?, ?, ?, ?)`
62
+ ).run(featureId, name, description ?? null, color ?? "#6366f1", now);
63
+
64
+ for (const col of DEFAULT_COLUMNS) {
65
+ db.prepare(
66
+ `INSERT INTO "Column" ("id", "name", "order", "color", "projectId", "updatedAt")
67
+ VALUES (?, ?, ?, ?, ?, ?)`
68
+ ).run(generateId(), col.name, col.order, col.color, featureId, now);
69
+ }
70
+ });
71
+
72
+ txn();
73
+
74
+ const feature = db.prepare(`SELECT * FROM "Project" WHERE "id" = ?`).get(featureId) as Record<string, unknown>;
75
+ const columns = db
76
+ .prepare(`SELECT * FROM "Column" WHERE "projectId" = ? ORDER BY "order" ASC`)
77
+ .all(featureId)
78
+ .map((r) => mapColumnRow(r as ColumnRow));
79
+
80
+ return jsonResult({ ...feature, columns });
81
+ }
82
+ );
83
+
84
+ // ── kanban_get_feature ────────────────────────────────────────────────────
85
+
86
+ server.tool(
87
+ "kanban_get_feature",
88
+ "Get full feature details including columns, tasks, and bugs",
89
+ { id: z.string().describe("Feature ID") },
90
+ async ({ id }) => {
91
+ const db = getDb(projectId);
92
+
93
+ const feature = db.prepare(`SELECT * FROM "Project" WHERE "id" = ?`).get(id) as Record<string, unknown> | undefined;
94
+ if (!feature) return errorResult("Feature not found");
95
+
96
+ const columns = db
97
+ .prepare(`SELECT * FROM "Column" WHERE "projectId" = ? ORDER BY "order" ASC`)
98
+ .all(id) as ColumnRow[];
99
+
100
+ const columnsWithIssues = columns.map((col) => {
101
+ const issues = db
102
+ .prepare(`SELECT * FROM "Issue" WHERE "columnId" = ? ORDER BY "order" ASC`)
103
+ .all(col.id)
104
+ .map((r) => mapIssueRow(r as IssueRow));
105
+ return { ...mapColumnRow(col), issues };
106
+ });
107
+
108
+ return jsonResult({ ...feature, columns: columnsWithIssues });
109
+ }
110
+ );
111
+
112
+ // ── kanban_delete_feature ─────────────────────────────────────────────────
113
+
114
+ server.tool(
115
+ "kanban_delete_feature",
116
+ "Delete a feature and all its tasks and bugs",
117
+ { id: z.string().describe("Feature ID") },
118
+ async ({ id }) => {
119
+ const db = getDb(projectId);
120
+ const existing = db.prepare(`SELECT "id" FROM "Project" WHERE "id" = ?`).get(id);
121
+ if (!existing) return errorResult("Feature not found");
122
+ db.prepare(`DELETE FROM "Project" WHERE "id" = ?`).run(id);
123
+ return jsonResult({ message: `Feature ${id} deleted.` });
124
+ }
125
+ );
126
+
127
+ // ── kanban_update_feature ─────────────────────────────────────────────────
128
+
129
+ server.tool(
130
+ "kanban_update_feature",
131
+ "Update an existing feature's name, description, or color",
132
+ {
133
+ id: z.string().describe("Feature ID"),
134
+ name: z.string().optional().describe("New feature name"),
135
+ description: z.string().optional().describe("New feature description"),
136
+ color: z.string().optional().describe('New hex color e.g. #6366f1'),
137
+ },
138
+ async ({ id, name, description, color }) => {
139
+ const db = getDb(projectId);
140
+
141
+ const existing = db.prepare(`SELECT * FROM "Project" WHERE "id" = ?`).get(id);
142
+ if (!existing) return errorResult("Feature not found");
143
+
144
+ const setClauses: string[] = [];
145
+ const params: any[] = [];
146
+
147
+ if (name !== undefined) { setClauses.push(`"name" = ?`); params.push(name); }
148
+ if (description !== undefined) { setClauses.push(`"description" = ?`); params.push(description); }
149
+ if (color !== undefined) { setClauses.push(`"color" = ?`); params.push(color); }
150
+
151
+ if (setClauses.length === 0) return errorResult("No valid fields to update");
152
+
153
+ const now = nowIso();
154
+ setClauses.push(`"updatedAt" = ?`);
155
+ params.push(now);
156
+ params.push(id);
157
+
158
+ db.prepare(`UPDATE "Project" SET ${setClauses.join(", ")} WHERE "id" = ?`).run(...params);
159
+
160
+ const row = db.prepare(`SELECT * FROM "Project" WHERE "id" = ?`).get(id);
161
+ return jsonResult(row);
162
+ }
163
+ );
164
+ }