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,88 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { normalizeEscapes, truncateDescription, DEFAULT_COLUMNS, mapColumnRow, mapIssueRow } from './mcp-helpers.js';
3
+ import type { ColumnRow, IssueRow } from './db.js';
4
+
5
+ describe('normalizeEscapes', () => {
6
+ it('converts literal \\n to newline', () => {
7
+ expect(normalizeEscapes('line1\\nline2')).toBe('line1\nline2');
8
+ });
9
+
10
+ it('converts literal \\t to tab', () => {
11
+ expect(normalizeEscapes('col1\\tcol2')).toBe('col1\tcol2');
12
+ });
13
+
14
+ it('handles multiple escapes', () => {
15
+ expect(normalizeEscapes('a\\nb\\nc\\td')).toBe('a\nb\nc\td');
16
+ });
17
+
18
+ it('returns unchanged string when no escapes', () => {
19
+ expect(normalizeEscapes('no escapes here')).toBe('no escapes here');
20
+ });
21
+ });
22
+
23
+ describe('truncateDescription', () => {
24
+ it('returns null for null/undefined input', () => {
25
+ expect(truncateDescription(null)).toBeNull();
26
+ expect(truncateDescription(undefined)).toBeNull();
27
+ });
28
+
29
+ it('returns short descriptions unchanged', () => {
30
+ expect(truncateDescription('short desc')).toBe('short desc');
31
+ });
32
+
33
+ it('truncates descriptions over 200 chars', () => {
34
+ const long = 'a'.repeat(250);
35
+ const result = truncateDescription(long)!;
36
+ expect(result.length).toBeLessThan(250);
37
+ expect(result).toContain('…(truncated)');
38
+ });
39
+
40
+ it('returns exactly 200 chars unchanged', () => {
41
+ const exact = 'x'.repeat(200);
42
+ expect(truncateDescription(exact)).toBe(exact);
43
+ });
44
+ });
45
+
46
+ describe('DEFAULT_COLUMNS', () => {
47
+ it('has 6 columns in correct order', () => {
48
+ expect(DEFAULT_COLUMNS).toHaveLength(6);
49
+ expect(DEFAULT_COLUMNS.map((c) => c.name)).toEqual([
50
+ 'Backlog', 'Todo', 'In Progress', 'In Review', 'Testing', 'Done',
51
+ ]);
52
+ });
53
+
54
+ it('has sequential order values', () => {
55
+ expect(DEFAULT_COLUMNS.map((c) => c.order)).toEqual([0, 1, 2, 3, 4, 5]);
56
+ });
57
+ });
58
+
59
+ describe('mapColumnRow', () => {
60
+ it('remaps projectId to featureId', () => {
61
+ const row: ColumnRow = {
62
+ id: 'c1', name: 'Todo', order: 1, color: '#blue',
63
+ projectId: 'proj1', createdAt: '2025-01-01', updatedAt: '2025-01-01',
64
+ };
65
+ const mapped = mapColumnRow(row);
66
+ expect(mapped.featureId).toBe('proj1');
67
+ expect('projectId' in mapped).toBe(false);
68
+ });
69
+
70
+ it('returns undefined for undefined input', () => {
71
+ expect(mapColumnRow(undefined)).toBeUndefined();
72
+ });
73
+ });
74
+
75
+ describe('mapIssueRow', () => {
76
+ it('remaps projectId to featureId', () => {
77
+ const row: IssueRow = {
78
+ id: 'i1', title: 'Bug', description: null, type: 'BUG',
79
+ priority: 'HIGH', order: 0, labels: '[]', dueDate: null,
80
+ reviewFeedback: null, projectId: 'proj1', columnId: 'c1',
81
+ createdAt: '2025-01-01', updatedAt: '2025-01-01',
82
+ };
83
+ const mapped = mapIssueRow(row);
84
+ expect(mapped.featureId).toBe('proj1');
85
+ expect(mapped.title).toBe('Bug');
86
+ expect('projectId' in mapped).toBe(false);
87
+ });
88
+ });
@@ -0,0 +1,60 @@
1
+ import type Database from "better-sqlite3";
2
+ import type { ColumnRow, IssueRow } from "./db.js";
3
+
4
+ /** Convert literal escape sequences (\n, \t) to real characters. */
5
+ export function normalizeEscapes(text: string): string {
6
+ return text.replace(/\\n/g, '\n').replace(/\\t/g, '\t');
7
+ }
8
+
9
+ // ── Constants ────────────────────────────────────────────────────────────────
10
+
11
+ export const DEFAULT_COLUMNS = [
12
+ { name: "Backlog", color: "#64748b", order: 0 },
13
+ { name: "Todo", color: "#3b82f6", order: 1 },
14
+ { name: "In Progress", color: "#f59e0b", order: 2 },
15
+ { name: "In Review", color: "#8b5cf6", order: 3 },
16
+ { name: "Testing", color: "#14b8a6", order: 4 },
17
+ { name: "Done", color: "#22c55e", order: 5 },
18
+ ];
19
+
20
+ // ── Row mappers ──────────────────────────────────────────────────────────────
21
+ // Remap internal "projectId" column to external "featureId" for MCP consumers.
22
+
23
+ type MappedColumn = Omit<ColumnRow, 'projectId'> & { featureId: string };
24
+ type MappedIssue = Omit<IssueRow, 'projectId'> & { featureId: string };
25
+
26
+ export function mapColumnRow(row: ColumnRow): MappedColumn;
27
+ export function mapColumnRow(row: ColumnRow | undefined): MappedColumn | undefined;
28
+ export function mapColumnRow(row: ColumnRow | undefined): MappedColumn | undefined {
29
+ if (!row) return row;
30
+ const { projectId, ...rest } = row;
31
+ return { ...rest, featureId: projectId };
32
+ }
33
+
34
+ export function mapIssueRow(row: IssueRow): MappedIssue;
35
+ export function mapIssueRow(row: IssueRow | undefined): MappedIssue | undefined;
36
+ export function mapIssueRow(row: IssueRow | undefined): MappedIssue | undefined {
37
+ if (!row) return row;
38
+ const { projectId, ...rest } = row;
39
+ return { ...rest, featureId: projectId };
40
+ }
41
+
42
+ // ── Helpers ──────────────────────────────────────────────────────────────────
43
+
44
+ export function resolveColumnId(
45
+ db: Database.Database,
46
+ featureId: string,
47
+ columnName: string
48
+ ): string | null {
49
+ const col = db
50
+ .prepare('SELECT id FROM "Column" WHERE projectId = ? AND name = ?')
51
+ .get(featureId, columnName) as { id: string } | undefined;
52
+ return col?.id ?? null;
53
+ }
54
+
55
+ const DESC_TRUNCATE_LEN = 200;
56
+ export function truncateDescription(desc: string | null | undefined): string | null {
57
+ if (!desc) return desc ?? null;
58
+ if (desc.length <= DESC_TRUNCATE_LEN) return desc;
59
+ return desc.slice(0, DESC_TRUNCATE_LEN) + "…(truncated)";
60
+ }
@@ -0,0 +1,59 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { createDevglideMcpServer } from "../../../packages/mcp-utils/src/index.js";
3
+ import { registerFeatureTools } from "./tools/feature-tools.js";
4
+ import { registerItemTools } from "./tools/item-tools.js";
5
+ import { registerVersionedEntryTools } from "./tools/versioned-entry-tools.js";
6
+
7
+ // ── Factory ──────────────────────────────────────────────────────────────────
8
+
9
+ export function createKanbanMcpServer(
10
+ projectId?: string | null
11
+ ): McpServer {
12
+ const server = createDevglideMcpServer(
13
+ "devglide-kanban",
14
+ "0.1.0",
15
+ "Kanban board management for features, tasks, and bugs",
16
+ {
17
+ instructions: [
18
+ "## Kanban — Workflow Conventions",
19
+ "",
20
+ "### Picking up work",
21
+ "- When looking for tasks to work on, search the **Todo** column by default (columnName: 'Todo') unless the user specifies a different column or a specific task.",
22
+ "- Each feature has its own kanban board with columns: Backlog → Todo → In Progress → In Review → Testing → Done.",
23
+ "",
24
+ "### Updating task status",
25
+ "- Move items to **In Progress** when starting work.",
26
+ "- Move items to **In Review** or **Testing** when work is complete.",
27
+ "- **Never** move items to **Done** — only the user can mark items as done.",
28
+ "",
29
+ "### Creating items",
30
+ "- New items can only be created in **Backlog** or **Todo** columns.",
31
+ "- Default priority is MEDIUM if not specified.",
32
+ "- Use labels to categorize work (e.g. 'ui', 'api', 'search', 'research').",
33
+ "",
34
+ "### Review feedback",
35
+ "- Items in **In Review** may have review history with versioned notes on what needs to change.",
36
+ "- Use `hasReviewFeedback: true` on kanban_list_items to find items that need revisions.",
37
+ "- Use `kanban_append_review` to add new review feedback (append-only, versioned).",
38
+ "- Use `kanban_get_review_history` to read the full review history.",
39
+ "",
40
+ "### Work log",
41
+ "- Use `kanban_append_work_log` to record what was done while working on a task.",
42
+ "- Use `kanban_get_work_log` to read the full work log history.",
43
+ "- Work log entries are append-only with automatic versioning.",
44
+ "",
45
+ "### Quick reference — commonly confused parameters",
46
+ "- `kanban_append_work_log(id, content)` — `content` is the log text (not `entry` or `text`).",
47
+ "- `kanban_append_review(id, content)` — `content` is the review feedback text.",
48
+ "- `kanban_create_item(title, featureId, ...)` — `title` and `featureId` are required. Use `columnName` (e.g. 'Todo') or `columnId` to place it.",
49
+ "- `kanban_move_item(id, ...)` — use `columnName` (e.g. 'In Progress') or `columnId` to set destination.",
50
+ ],
51
+ }
52
+ );
53
+
54
+ registerFeatureTools(server, projectId);
55
+ registerItemTools(server, projectId);
56
+ registerVersionedEntryTools(server, projectId);
57
+
58
+ return server;
59
+ }
@@ -0,0 +1,161 @@
1
+ import { Router, Request, Response } from "express";
2
+ import { getDb, generateId } from "../db.js";
3
+ import multer from "multer";
4
+ import path from "path";
5
+ import fs from "fs";
6
+ import fsp from "fs/promises";
7
+ import { fileURLToPath } from "url";
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ const uploadsDir = path.join(__dirname, "..", "..", "uploads");
11
+
12
+ /**
13
+ * Sanitize a filename to prevent Content-Disposition header injection.
14
+ * Strips everything except alphanumeric, dots, hyphens, underscores, and spaces.
15
+ * Falls back to "download" if the result is empty after sanitization.
16
+ */
17
+ function sanitizeFilename(filename: string): string {
18
+ // Strip path separators to prevent directory traversal
19
+ const basename = filename.replace(/^.*[/\\]/, "");
20
+ // Keep only safe characters: alphanumeric, dot, hyphen, underscore, space
21
+ const sanitized = basename.replace(/[^a-zA-Z0-9.\-_ ]/g, "");
22
+ // Prevent empty filenames or dot-only filenames
23
+ const trimmed = sanitized.replace(/^\.+/, "").trim();
24
+ return trimmed || "download";
25
+ }
26
+
27
+ export const attachmentsRouter: Router = Router();
28
+
29
+ const ALLOWED_MIME_TYPES = [
30
+ "image/jpeg",
31
+ "image/png",
32
+ "image/gif",
33
+ "image/webp",
34
+ "image/svg+xml",
35
+ ];
36
+
37
+ const upload = multer({
38
+ storage: multer.memoryStorage(),
39
+ limits: { fileSize: 5 * 1024 * 1024 },
40
+ });
41
+
42
+ // POST /api/attachments
43
+ attachmentsRouter.post("/", upload.single("file"), async (req: Request, res: Response) => {
44
+ try {
45
+ const file = req.file;
46
+ const issueId = req.body.issueId;
47
+
48
+ if (!file) {
49
+ res.status(400).json({ error: "No file uploaded" });
50
+ return;
51
+ }
52
+
53
+ if (!issueId) {
54
+ res.status(400).json({ error: "issueId is required" });
55
+ return;
56
+ }
57
+
58
+ if (!ALLOWED_MIME_TYPES.includes(file.mimetype)) {
59
+ res.status(400).json({
60
+ error: `Invalid file type: ${file.mimetype}. Allowed: ${ALLOWED_MIME_TYPES.join(", ")}`,
61
+ });
62
+ return;
63
+ }
64
+
65
+ const db = getDb(req.projectId);
66
+
67
+ // Verify issue exists
68
+ const issue = db.prepare(`SELECT "id" FROM "Issue" WHERE "id" = ?`).get(issueId);
69
+ if (!issue) {
70
+ res.status(404).json({ error: "Issue not found" });
71
+ return;
72
+ }
73
+
74
+ const id = generateId();
75
+ const safeFilename = sanitizeFilename(file.originalname);
76
+ const ext = path.extname(safeFilename);
77
+
78
+ // Ensure uploads directory exists
79
+ await fsp.mkdir(uploadsDir, { recursive: true });
80
+
81
+ // Save file to disk
82
+ const filePath = path.join(uploadsDir, `${id}${ext}`);
83
+ await fsp.writeFile(filePath, file.buffer);
84
+
85
+ // Create DB record (store sanitized filename)
86
+ db.prepare(
87
+ `INSERT INTO "Attachment" ("id", "filename", "mimeType", "size", "issueId")
88
+ VALUES (?, ?, ?, ?, ?)`
89
+ ).run(id, safeFilename, file.mimetype, file.size, issueId);
90
+
91
+ const row = db.prepare(`SELECT * FROM "Attachment" WHERE "id" = ?`).get(id);
92
+ res.status(201).json(row);
93
+ } catch (err: unknown) {
94
+ res.status(500).json({ error: (err instanceof Error ? err.message : String(err)) });
95
+ }
96
+ });
97
+
98
+ // GET /api/attachments/:id
99
+ attachmentsRouter.get("/:id", (req: Request, res: Response) => {
100
+ try {
101
+ const { id } = req.params;
102
+ const db = getDb(req.projectId);
103
+
104
+ const row = db.prepare(`SELECT * FROM "Attachment" WHERE "id" = ?`).get(id) as any;
105
+ if (!row) {
106
+ res.status(404).json({ error: "Attachment not found" });
107
+ return;
108
+ }
109
+
110
+ const ext = path.extname(row.filename);
111
+ const filePath = path.join(uploadsDir, `${id}${ext}`);
112
+
113
+ if (!fs.existsSync(filePath)) {
114
+ res.status(404).json({ error: "Attachment file not found on disk" });
115
+ return;
116
+ }
117
+
118
+ // Sanitize filename on output as defense-in-depth (guards against
119
+ // pre-existing records stored before upload-time sanitization was added)
120
+ const safeDownloadName = sanitizeFilename(row.filename);
121
+ res.setHeader("Content-Type", row.mimeType);
122
+ res.setHeader(
123
+ "Content-Disposition",
124
+ `inline; filename="${safeDownloadName}"`
125
+ );
126
+ res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
127
+ res.sendFile(filePath);
128
+ } catch (err: unknown) {
129
+ res.status(500).json({ error: (err instanceof Error ? err.message : String(err)) });
130
+ }
131
+ });
132
+
133
+ // DELETE /api/attachments/:id
134
+ attachmentsRouter.delete("/:id", (req: Request, res: Response) => {
135
+ try {
136
+ const { id } = req.params;
137
+ const db = getDb(req.projectId);
138
+
139
+ const row = db.prepare(`SELECT * FROM "Attachment" WHERE "id" = ?`).get(id) as any;
140
+ if (!row) {
141
+ res.status(404).json({ error: "Attachment not found" });
142
+ return;
143
+ }
144
+
145
+ // Delete file from disk (ignore errors)
146
+ const ext = path.extname(row.filename);
147
+ const filePath = path.join(uploadsDir, `${id}${ext}`);
148
+ try {
149
+ fs.unlinkSync(filePath);
150
+ } catch {
151
+ // Ignore errors — file may already be deleted
152
+ }
153
+
154
+ // Delete DB record
155
+ db.prepare(`DELETE FROM "Attachment" WHERE "id" = ?`).run(id);
156
+
157
+ res.json({ success: true });
158
+ } catch (err: unknown) {
159
+ res.status(500).json({ error: (err instanceof Error ? err.message : String(err)) });
160
+ }
161
+ });
@@ -0,0 +1,233 @@
1
+ import { Router, Request, Response } from "express";
2
+ import { getDb, generateId, nowIso } from "../db.js";
3
+ import path from "path";
4
+ import fs from "fs";
5
+ import { fileURLToPath } from "url";
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const uploadsDir = path.join(__dirname, "..", "..", "uploads");
9
+
10
+ export const featuresRouter: Router = Router();
11
+
12
+ const DEFAULT_COLUMNS = [
13
+ { name: "Backlog", color: "#64748b", order: 0 },
14
+ { name: "Todo", color: "#3b82f6", order: 1 },
15
+ { name: "In Progress", color: "#f59e0b", order: 2 },
16
+ { name: "In Review", color: "#8b5cf6", order: 3 },
17
+ { name: "Testing", color: "#14b8a6", order: 4 },
18
+ { name: "Done", color: "#22c55e", order: 5 },
19
+ ];
20
+
21
+ function mapColumn(row: any) {
22
+ if (!row) return row;
23
+ const { projectId, ...rest } = row;
24
+ return { ...rest, featureId: projectId };
25
+ }
26
+
27
+ function mapIssue(row: any) {
28
+ if (!row) return row;
29
+ const { projectId, ...rest } = row;
30
+ return { ...rest, featureId: projectId };
31
+ }
32
+
33
+ // GET /api/features
34
+ featuresRouter.get("/", (req: Request, res: Response) => {
35
+ try {
36
+ const db = getDb(req.projectId);
37
+
38
+ const rows = db
39
+ .prepare(
40
+ `SELECT p.*,
41
+ (SELECT COUNT(*) FROM "Issue" i
42
+ JOIN "Column" c ON c."id" = i."columnId"
43
+ WHERE i."projectId" = p."id" AND c."name" != 'Done') AS issueCount
44
+ FROM "Project" p
45
+ ORDER BY p."name" COLLATE NOCASE ASC`
46
+ )
47
+ .all() as any[];
48
+
49
+ const features = rows.map((row) => {
50
+ const { issueCount, ...rest } = row;
51
+ return { ...rest, _count: { issues: issueCount } };
52
+ });
53
+
54
+ res.json(features);
55
+ } catch (err: unknown) {
56
+ res.status(500).json({ error: (err instanceof Error ? err.message : String(err)) });
57
+ }
58
+ });
59
+
60
+ // POST /api/features
61
+ featuresRouter.post("/", (req: Request, res: Response) => {
62
+ try {
63
+ const { name, description, color } = req.body;
64
+
65
+ if (!name || typeof name !== "string" || !name.trim()) {
66
+ res.status(400).json({ error: "name is required" });
67
+ return;
68
+ }
69
+
70
+ if (name.length > 500) {
71
+ res.status(400).json({ error: "name must be at most 500 characters" });
72
+ return;
73
+ }
74
+
75
+ const db = getDb(req.projectId);
76
+ const now = nowIso();
77
+ const featureId = generateId();
78
+
79
+ const txn = db.transaction(() => {
80
+ db.prepare(
81
+ `INSERT INTO "Project" ("id", "name", "description", "color", "updatedAt")
82
+ VALUES (?, ?, ?, ?, ?)`
83
+ ).run(featureId, name, description ?? null, color ?? "#6366f1", now);
84
+
85
+ for (const col of DEFAULT_COLUMNS) {
86
+ const colId = generateId();
87
+ db.prepare(
88
+ `INSERT INTO "Column" ("id", "name", "order", "color", "projectId", "updatedAt")
89
+ VALUES (?, ?, ?, ?, ?, ?)`
90
+ ).run(colId, col.name, col.order, col.color, featureId, now);
91
+ }
92
+ });
93
+
94
+ txn();
95
+
96
+ const feature = db.prepare(`SELECT * FROM "Project" WHERE "id" = ?`).get(featureId) as Record<string, unknown>;
97
+ const columns = db
98
+ .prepare(`SELECT * FROM "Column" WHERE "projectId" = ? ORDER BY "order" ASC`)
99
+ .all(featureId);
100
+
101
+ res.status(201).json({ ...feature, columns: columns.map(mapColumn) });
102
+ } catch (err: unknown) {
103
+ res.status(500).json({ error: (err instanceof Error ? err.message : String(err)) });
104
+ }
105
+ });
106
+
107
+ // GET /api/features/:id
108
+ featuresRouter.get("/:id", (req: Request, res: Response) => {
109
+ try {
110
+ const { id } = req.params;
111
+ const db = getDb(req.projectId);
112
+
113
+ const feature = db.prepare(`SELECT * FROM "Project" WHERE "id" = ?`).get(id) as any;
114
+
115
+ if (!feature) {
116
+ res.status(404).json({ error: "Feature not found" });
117
+ return;
118
+ }
119
+
120
+ const columns = db
121
+ .prepare(`SELECT * FROM "Column" WHERE "projectId" = ? ORDER BY "order" ASC`)
122
+ .all(id) as any[];
123
+
124
+ const issues = db
125
+ .prepare(
126
+ `SELECT i.*,
127
+ (SELECT COUNT(*) FROM "VersionedEntry" ve WHERE ve."issueId" = i."id" AND ve."type" = 'review') AS reviewCount,
128
+ (SELECT COUNT(*) FROM "VersionedEntry" ve WHERE ve."issueId" = i."id" AND ve."type" = 'work_log') AS workLogCount
129
+ FROM "Issue" i WHERE i."projectId" = ? ORDER BY i."order" ASC`
130
+ )
131
+ .all(id) as any[];
132
+
133
+ // Group issues by columnId
134
+ const issuesByColumn = new Map<string, any[]>();
135
+ for (const issue of issues) {
136
+ const list = issuesByColumn.get(issue.columnId) ?? [];
137
+ list.push(mapIssue(issue));
138
+ issuesByColumn.set(issue.columnId, list);
139
+ }
140
+
141
+ const mappedColumns = columns.map((col) => ({
142
+ ...mapColumn(col),
143
+ issues: issuesByColumn.get(col.id) ?? [],
144
+ }));
145
+
146
+ res.json({ ...feature, columns: mappedColumns });
147
+ } catch (err: unknown) {
148
+ res.status(500).json({ error: (err instanceof Error ? err.message : String(err)) });
149
+ }
150
+ });
151
+
152
+ // PATCH /api/features/:id
153
+ featuresRouter.patch("/:id", (req: Request, res: Response) => {
154
+ try {
155
+ const { id } = req.params;
156
+ const db = getDb(req.projectId);
157
+
158
+ const existing = db.prepare(`SELECT * FROM "Project" WHERE "id" = ?`).get(id);
159
+ if (!existing) {
160
+ res.status(404).json({ error: "Feature not found" });
161
+ return;
162
+ }
163
+
164
+ const allowedFields: Record<string, string> = {
165
+ name: '"name"',
166
+ description: '"description"',
167
+ color: '"color"',
168
+ };
169
+
170
+ const setClauses: string[] = [];
171
+ const params: any[] = [];
172
+
173
+ for (const [key, col] of Object.entries(allowedFields)) {
174
+ if (req.body[key] !== undefined) {
175
+ setClauses.push(`${col} = ?`);
176
+ params.push(req.body[key]);
177
+ }
178
+ }
179
+
180
+ if (setClauses.length === 0) {
181
+ res.status(400).json({ error: "No valid fields to update" });
182
+ return;
183
+ }
184
+
185
+ const now = nowIso();
186
+ setClauses.push(`"updatedAt" = ?`);
187
+ params.push(now);
188
+
189
+ params.push(id);
190
+
191
+ db.prepare(`UPDATE "Project" SET ${setClauses.join(", ")} WHERE "id" = ?`).run(...params);
192
+
193
+ const row = db.prepare(`SELECT * FROM "Project" WHERE "id" = ?`).get(id);
194
+ res.json(row);
195
+ } catch (err: unknown) {
196
+ res.status(500).json({ error: (err instanceof Error ? err.message : String(err)) });
197
+ }
198
+ });
199
+
200
+ // DELETE /api/features/:id
201
+ featuresRouter.delete("/:id", (req: Request, res: Response) => {
202
+ try {
203
+ const { id } = req.params;
204
+ const db = getDb(req.projectId);
205
+
206
+ const existing = db.prepare(`SELECT * FROM "Project" WHERE "id" = ?`).get(id);
207
+ if (!existing) {
208
+ res.status(404).json({ error: "Feature not found" });
209
+ return;
210
+ }
211
+
212
+ // Clean up attachment files from disk before deleting
213
+ const attachments = db
214
+ .prepare(
215
+ `SELECT a."id", a."filename" FROM "Attachment" a
216
+ JOIN "Issue" i ON a."issueId" = i."id"
217
+ WHERE i."projectId" = ?`
218
+ )
219
+ .all(id) as any[];
220
+
221
+ for (const att of attachments) {
222
+ const ext = path.extname(att.filename);
223
+ const filePath = path.join(uploadsDir, `${att.id}${ext}`);
224
+ try { fs.unlinkSync(filePath); } catch {}
225
+ }
226
+
227
+ db.prepare(`DELETE FROM "Project" WHERE "id" = ?`).run(id);
228
+
229
+ res.json({ success: true });
230
+ } catch (err: unknown) {
231
+ res.status(500).json({ error: (err instanceof Error ? err.message : String(err)) });
232
+ }
233
+ });