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,150 @@
1
+ import pty, { type IPty } from 'node-pty';
2
+ import fs from 'fs';
3
+ import { createShellMcpServer } from './mcp.js';
4
+ import { runStdio } from '@devglide/mcp-utils';
5
+
6
+ import type { PtyEntry, PaneInfo, DashboardState, ShellConfig, McpState } from './shell-types.js';
7
+
8
+ export type { PtyEntry, PaneInfo, DashboardState, ShellConfig, McpState };
9
+
10
+ // ── Helpers ───────────────────────────────────────────────────────────────────
11
+
12
+ function readCwd(pid: number): Promise<string | null> {
13
+ return new Promise((resolve) => {
14
+ fs.readlink(`/proc/${pid}/cwd`, (err: NodeJS.ErrnoException | null, linkPath: string) => resolve(err ? null : linkPath));
15
+ });
16
+ }
17
+
18
+ const ENV_ALLOWLIST: string[] = ['HOME', 'PATH', 'USER', 'SHELL', 'LANG', 'LC_ALL', 'SSH_AUTH_SOCK'];
19
+
20
+ function safeEnv(extra: Record<string, string> = {}): Record<string, string> {
21
+ const env: Record<string, string> = { TERM: 'xterm-256color' };
22
+ for (const key of ENV_ALLOWLIST) {
23
+ if (process.env[key] !== undefined) env[key] = process.env[key]!;
24
+ }
25
+ return { ...env, ...extra };
26
+ }
27
+
28
+ // ── No-op emitter (standalone MCP mode has no socket clients) ────────────────
29
+ const io = { to(_room: string) { return this; }, emit(_event: string, _data?: unknown) { return this; }, close() {} };
30
+
31
+ // ── Shell configs ─────────────────────────────────────────────────────────────
32
+
33
+ const SHELL_CONFIGS: Record<string, ShellConfig> = {
34
+ bash: {
35
+ command: 'bash',
36
+ args: [],
37
+ env: safeEnv()
38
+ },
39
+ };
40
+
41
+ // ── Global shared state (survives individual socket disconnects) ───────────────
42
+
43
+ const globalPtys: Map<string, PtyEntry> = new Map(); // id -> { ptyProcess, chunks, totalLen }
44
+
45
+ const dashboardState: DashboardState = {
46
+ panes: [], // [{ id, shellType, title, num, cwd }] — ordered
47
+ activeTab: 'grid',
48
+ activePaneId: null,
49
+ };
50
+
51
+ let paneIdCounter: number = 0; // strictly for unique IDs, never reused
52
+ function nextPaneId(): string { return `pane-${++paneIdCounter}`; }
53
+ const SCROLLBACK_LIMIT: number = 200_000; // bytes
54
+ const MAX_PANES: number = 9;
55
+
56
+ // ── Multi-client resize arbitration ──────────────────────────────────────────
57
+ const paneActiveSocket: Map<string, string> = new Map(); // paneId -> socketId
58
+ const socketDimensions: Map<string, Map<string, { cols: number; rows: number }>> = new Map(); // socketId -> Map<paneId, {cols, rows}>
59
+
60
+ // ── PTY lifecycle ─────────────────────────────────────────────────────────────
61
+
62
+ function spawnGlobalPty(
63
+ id: string,
64
+ command: string,
65
+ args: string[],
66
+ env: Record<string, string>,
67
+ cols: number,
68
+ rows: number,
69
+ trackCwd: boolean,
70
+ oscOnly: boolean,
71
+ startCwd: string | null
72
+ ): IPty {
73
+ cols = Number.isInteger(cols) && cols >= 1 ? Math.min(cols, 500) : 80;
74
+ rows = Number.isInteger(rows) && rows >= 1 ? Math.min(rows, 500) : 24;
75
+
76
+ const ptyProcess: IPty = pty.spawn(command, args, {
77
+ name: 'xterm-256color',
78
+ cols,
79
+ rows,
80
+ cwd: startCwd || process.env.HOME || '/',
81
+ env
82
+ });
83
+
84
+ const entry: PtyEntry = { ptyProcess, chunks: [], totalLen: 0 };
85
+ globalPtys.set(id, entry);
86
+
87
+ let cwdTimer: ReturnType<typeof setTimeout> | null = null;
88
+
89
+ ptyProcess.onData((data: string) => {
90
+ entry.chunks.push(data);
91
+ entry.totalLen += data.length;
92
+ if (entry.totalLen > SCROLLBACK_LIMIT * 1.5) {
93
+ const joined = entry.chunks.join('').slice(-SCROLLBACK_LIMIT);
94
+ entry.chunks = [joined];
95
+ entry.totalLen = joined.length;
96
+ }
97
+
98
+ io.to(`pane:${id}`).emit('terminal:data', { id, data });
99
+
100
+ if (trackCwd) {
101
+ const oscMatch = data.match(/\x1b\]7;([^\x07\x1b]+)\x07/);
102
+ if (oscMatch) {
103
+ const cwd = oscMatch[1];
104
+ _updatePaneCwd(id, cwd);
105
+ io.emit('terminal:cwd', { id, cwd });
106
+ } else if (!oscOnly) {
107
+ if (cwdTimer) clearTimeout(cwdTimer);
108
+ cwdTimer = setTimeout(async () => {
109
+ if (!globalPtys.has(id)) return; // PTY exited before timer fired
110
+ const cwd = await readCwd(ptyProcess.pid);
111
+ if (cwd) {
112
+ _updatePaneCwd(id, cwd);
113
+ io.emit('terminal:cwd', { id, cwd });
114
+ }
115
+ }, 300);
116
+ }
117
+ }
118
+ });
119
+
120
+ ptyProcess.onExit(({ exitCode }: { exitCode: number }) => {
121
+ if (cwdTimer) clearTimeout(cwdTimer);
122
+ if (!globalPtys.delete(id)) return; // already cleaned up by terminal:close
123
+ io.emit('terminal:exit', { id, code: exitCode });
124
+ });
125
+
126
+ return ptyProcess;
127
+ }
128
+
129
+ function _updatePaneCwd(id: string, cwd: string): void {
130
+ const pane = dashboardState.panes.find((p: PaneInfo) => p.id === id);
131
+ if (pane) pane.cwd = cwd;
132
+ }
133
+
134
+ // ── Graceful shutdown ──────────────────────────────────────────────────────────
135
+ function shutdown(): void {
136
+ console.log('\nShutting down — killing PTY processes...');
137
+ for (const { ptyProcess } of globalPtys.values()) {
138
+ try { ptyProcess.kill(); } catch {}
139
+ }
140
+ globalPtys.clear();
141
+ process.exit(0);
142
+ }
143
+ process.on('SIGTERM', shutdown);
144
+ process.on('SIGINT', shutdown);
145
+
146
+ // ── Stdio MCP mode ───────────────────────────────────────────────────────────
147
+ const mcpState: McpState = { globalPtys, dashboardState, io, spawnGlobalPty, SHELL_CONFIGS, MAX_PANES, nextPaneId, paneActiveSocket, socketDimensions };
148
+ const mcpServer = createShellMcpServer(mcpState);
149
+ await runStdio(mcpServer);
150
+ console.error('Devglide Shell MCP server running on stdio');
@@ -0,0 +1,398 @@
1
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
2
+ import { randomUUID } from 'crypto';
3
+ import { z } from 'zod';
4
+ import { createDevglideMcpServer } from '../../../packages/mcp-utils/src/index.js';
5
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import type { Express, Request, Response } from 'express';
9
+ import type { McpState, PaneInfo } from './shell-types.js';
10
+ import { getActiveProject } from '../../../project-context.js';
11
+
12
+ /** Send SIGHUP, then SIGKILL after 2 s if still alive. */
13
+ function killPty(pty: { pid: number; kill(signal?: string): void }): void {
14
+ try {
15
+ pty.kill();
16
+ } catch {
17
+ return;
18
+ }
19
+ const { pid } = pty;
20
+ setTimeout(() => {
21
+ try {
22
+ process.kill(pid, 0);
23
+ process.kill(pid, 'SIGKILL');
24
+ } catch { /* already exited */ }
25
+ }, 2000).unref();
26
+ }
27
+
28
+ interface McpSession {
29
+ transport: StreamableHTTPServerTransport;
30
+ server: McpServer;
31
+ }
32
+
33
+ /**
34
+ * Create a shell MCP server with terminal management tools.
35
+ */
36
+ export function createShellMcpServer(state: McpState): McpServer {
37
+ const server = createDevglideMcpServer('devglide-shell', '0.1.0');
38
+
39
+ server.tool(
40
+ 'shell_list_panes',
41
+ 'List active terminal panes with CWD',
42
+ {},
43
+ async () => {
44
+ const panes = state.dashboardState.panes.map((p) => ({
45
+ id: p.id,
46
+ num: p.num,
47
+ shellType: p.shellType,
48
+ title: p.title,
49
+ cwd: p.cwd,
50
+ }));
51
+ return {
52
+ content: [{ type: 'text' as const, text: JSON.stringify(panes, null, 2) }],
53
+ };
54
+ }
55
+ );
56
+
57
+ server.tool(
58
+ 'shell_create_pane',
59
+ 'Create a new terminal pane (uses system default shell, or specify bash)',
60
+ {
61
+ shellType: z
62
+ .enum(['default', 'bash'])
63
+ .optional()
64
+ .describe('Shell type (default: system shell from $SHELL)'),
65
+ cwd: z.string().optional().describe('Working directory'),
66
+ },
67
+ async ({ shellType = 'default', cwd }) => {
68
+ if (state.globalPtys.size >= state.MAX_PANES) {
69
+ return {
70
+ content: [
71
+ {
72
+ type: 'text' as const,
73
+ text: `Maximum pane limit (${state.MAX_PANES}) reached`,
74
+ },
75
+ ],
76
+ isError: true,
77
+ };
78
+ }
79
+
80
+ const id = state.nextPaneId();
81
+ const num = state.dashboardState.panes.length + 1;
82
+ const title = String(num);
83
+ const config = state.SHELL_CONFIGS[shellType] || state.SHELL_CONFIGS.default;
84
+ let args = config.args;
85
+ let startCwd = process.env.HOME || '/';
86
+
87
+ if (cwd) {
88
+ if (!path.isAbsolute(cwd) || cwd.includes('\0') || /\.\.[\\/]/.test(cwd)) {
89
+ return {
90
+ content: [{ type: 'text' as const, text: 'Invalid cwd: must be absolute without traversal or null bytes' }],
91
+ isError: true,
92
+ };
93
+ }
94
+ try {
95
+ const stat = fs.statSync(cwd);
96
+ if (!stat.isDirectory()) throw new Error('not a directory');
97
+ } catch {
98
+ return {
99
+ content: [{ type: 'text' as const, text: 'cwd path does not exist or is not a directory' }],
100
+ isError: true,
101
+ };
102
+ }
103
+ startCwd = cwd;
104
+ }
105
+
106
+ try {
107
+ console.log(`[shell:mcp] create_pane shell=${shellType} cwd=${startCwd}`);
108
+
109
+ state.spawnGlobalPty(
110
+ id,
111
+ config.command,
112
+ args,
113
+ config.env,
114
+ 80,
115
+ 24,
116
+ true,
117
+ false,
118
+ startCwd
119
+ );
120
+
121
+ const paneInfo: PaneInfo = { id, shellType, title, num, cwd: startCwd, projectId: getActiveProject()?.id || null };
122
+ state.dashboardState.panes.push(paneInfo);
123
+ state.dashboardState.activePaneId = id;
124
+ state.io.emit('state:pane-added', paneInfo);
125
+ state.io.emit('state:active-pane', { paneId: id });
126
+
127
+ return {
128
+ content: [
129
+ { type: 'text' as const, text: JSON.stringify(paneInfo, null, 2) },
130
+ ],
131
+ };
132
+ } catch (err: unknown) {
133
+ return {
134
+ content: [
135
+ { type: 'text' as const, text: `Failed to start ${shellType}: ${(err as Error).message}` },
136
+ ],
137
+ isError: true,
138
+ };
139
+ }
140
+ }
141
+ );
142
+
143
+ server.tool(
144
+ 'shell_close_pane',
145
+ 'Close a terminal pane',
146
+ {
147
+ paneId: z.string().describe("Pane ID (e.g. 'pane-1')"),
148
+ },
149
+ async ({ paneId }) => {
150
+ const entry = state.globalPtys.get(paneId);
151
+ const existed = state.dashboardState.panes.some((p) => p.id === paneId);
152
+ if (!entry && !existed) {
153
+ return {
154
+ content: [{ type: 'text' as const, text: 'Pane not found' }],
155
+ isError: true,
156
+ };
157
+ }
158
+
159
+ console.log(`[shell:mcp] close_pane pane=${paneId}`);
160
+
161
+ if (entry) {
162
+ killPty(entry.ptyProcess);
163
+ state.globalPtys.delete(paneId);
164
+ }
165
+
166
+ // Find index of closing pane before removal so we can select the previous one
167
+ const closedIdx = state.dashboardState.panes.findIndex(
168
+ (p) => p.id === paneId
169
+ );
170
+
171
+ state.dashboardState.panes = state.dashboardState.panes.filter(
172
+ (p) => p.id !== paneId
173
+ );
174
+ state.io.emit('state:pane-removed', { id: paneId });
175
+
176
+ // Clean up resize arbitration
177
+ state.paneActiveSocket.delete(paneId);
178
+ if (state.socketDimensions) {
179
+ for (const dims of state.socketDimensions.values()) dims.delete(paneId);
180
+ }
181
+
182
+ // Renumber remaining panes
183
+ state.dashboardState.panes.forEach((p, i) => {
184
+ p.num = i + 1;
185
+ p.title = String(i + 1);
186
+ });
187
+ if (state.dashboardState.panes.length > 0) {
188
+ state.io.emit(
189
+ 'state:panes-renumbered',
190
+ state.dashboardState.panes.map(({ id, num }) => ({ id, num }))
191
+ );
192
+ }
193
+
194
+ // Select the previous pane (or next if closing the first one)
195
+ const prevIdx = Math.max(0, closedIdx - 1);
196
+ const nextPane =
197
+ state.dashboardState.panes.length > 0
198
+ ? state.dashboardState.panes[prevIdx].id
199
+ : null;
200
+
201
+ if (state.dashboardState.activeTab === paneId) {
202
+ // The closed pane was the focused tab — navigate to previous pane or back to grid
203
+ const next = nextPane ?? 'grid';
204
+ state.dashboardState.activeTab = next;
205
+ state.dashboardState.activePaneId = nextPane;
206
+ state.io.emit('state:active-tab', { tabId: next });
207
+ }
208
+
209
+ // Always update active pane highlight
210
+ state.dashboardState.activePaneId = nextPane;
211
+ state.io.emit('state:active-pane', { paneId: nextPane });
212
+
213
+ return {
214
+ content: [{ type: 'text' as const, text: `Pane ${paneId} closed.` }],
215
+ };
216
+ }
217
+ );
218
+
219
+ server.tool(
220
+ 'shell_run_command',
221
+ 'Send input to a terminal pane and return output after a timeout',
222
+ {
223
+ paneId: z.string().describe("Pane ID (e.g. 'pane-1')"),
224
+ command: z.string().describe('Command to execute'),
225
+ timeout: z
226
+ .number()
227
+ .optional()
228
+ .describe('Seconds to wait for output (default: 3, max: 30)'),
229
+ },
230
+ async ({ paneId, command, timeout }) => {
231
+ const entry = state.globalPtys.get(paneId);
232
+ if (!entry) {
233
+ return {
234
+ content: [{ type: 'text' as const, text: 'Pane not found' }],
235
+ isError: true,
236
+ };
237
+ }
238
+
239
+ console.log(`[shell:mcp] run_command pane=${paneId} command=${JSON.stringify(command.slice(0, 200))}`);
240
+
241
+ const maxMs = Math.min((timeout ?? 3) * 1000, 30000);
242
+ // Use tracked totalLen (O(1)) instead of joining all chunks (O(n)) for polling
243
+ const beforeLen = entry.totalLen;
244
+
245
+ entry.ptyProcess.write(command + '\r');
246
+
247
+ // Poll for output quiescence instead of waiting the full timeout
248
+ let lastLen = beforeLen;
249
+ let stableCount = 0;
250
+ const POLL_MS = 100;
251
+ const STABLE_THRESHOLD = 3; // 300ms of no new output = done
252
+
253
+ await new Promise<void>((resolve) => {
254
+ let elapsed = 0;
255
+ const interval = setInterval(() => {
256
+ elapsed += POLL_MS;
257
+ const currentLen = entry.totalLen;
258
+ if (currentLen > lastLen) {
259
+ lastLen = currentLen;
260
+ stableCount = 0;
261
+ } else {
262
+ stableCount++;
263
+ }
264
+ if (stableCount >= STABLE_THRESHOLD || elapsed >= maxMs) {
265
+ clearInterval(interval);
266
+ resolve();
267
+ }
268
+ }, POLL_MS);
269
+ });
270
+
271
+ // Join chunks once at the end to extract new output
272
+ const fullOutput = entry.chunks.join('');
273
+ let newOutput = fullOutput.slice(Math.min(beforeLen, fullOutput.length));
274
+
275
+ // Strip the echoed command line (first line) and ANSI escape sequences
276
+ const lines = newOutput.split('\n');
277
+ if (lines.length > 1) lines.shift(); // remove echoed command
278
+ newOutput = lines.join('\n').replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '').trim();
279
+
280
+ return {
281
+ content: [{ type: 'text' as const, text: newOutput || '(no output)' }],
282
+ };
283
+ }
284
+ );
285
+
286
+ server.tool(
287
+ 'shell_get_scrollback',
288
+ 'Get recent scrollback buffer from a terminal pane',
289
+ {
290
+ paneId: z.string().describe("Pane ID (e.g. 'pane-1')"),
291
+ lines: z
292
+ .number()
293
+ .optional()
294
+ .describe('Number of recent lines to return (default: 100)'),
295
+ },
296
+ async ({ paneId, lines }) => {
297
+ const entry = state.globalPtys.get(paneId);
298
+ if (!entry) {
299
+ return {
300
+ content: [{ type: 'text' as const, text: 'Pane not found' }],
301
+ isError: true,
302
+ };
303
+ }
304
+
305
+ const limit = lines ?? 100;
306
+ const fullOutput = entry.chunks.join('');
307
+ const allLines = fullOutput.split('\n');
308
+ const recent = allLines.slice(-limit).join('\n');
309
+
310
+ return {
311
+ content: [{ type: 'text' as const, text: recent || '(empty)' }],
312
+ };
313
+ }
314
+ );
315
+
316
+ return server;
317
+ }
318
+
319
+ /**
320
+ * Mount MCP StreamableHTTP endpoint on an Express app.
321
+ */
322
+ export function mountShellMcp(app: Express, state: McpState): void {
323
+ const sessions = new Map<string, McpSession>();
324
+
325
+ function isInitReq(body: unknown): boolean {
326
+ if (Array.isArray(body))
327
+ return body.some((m: unknown) => m && typeof m === 'object' && (m as Record<string, unknown>).method === 'initialize');
328
+ return body !== null && typeof body === 'object' && (body as Record<string, unknown>).method === 'initialize';
329
+ }
330
+
331
+ app.post('/mcp', async (req: Request, res: Response) => {
332
+ const sessionId = req.headers['mcp-session-id'] as string | undefined;
333
+
334
+ if (sessionId && sessions.has(sessionId)) {
335
+ const session = sessions.get(sessionId)!;
336
+ await session.transport.handleRequest(req, res, req.body);
337
+ return;
338
+ }
339
+
340
+ if (!sessionId && isInitReq(req.body)) {
341
+ const transport = new StreamableHTTPServerTransport({
342
+ sessionIdGenerator: () => randomUUID(),
343
+ onsessioninitialized: (id: string) => {
344
+ sessions.set(id, { transport, server });
345
+ },
346
+ });
347
+ transport.onclose = () => {
348
+ if (transport.sessionId) sessions.delete(transport.sessionId);
349
+ };
350
+
351
+ const server = createShellMcpServer(state);
352
+ await server.connect(transport);
353
+ await transport.handleRequest(req, res, req.body);
354
+ return;
355
+ }
356
+
357
+ res.writeHead(400, { 'Content-Type': 'application/json' });
358
+ res.end(
359
+ JSON.stringify({
360
+ jsonrpc: '2.0',
361
+ error: { code: -32000, message: 'Bad Request: No valid session ID' },
362
+ id: null,
363
+ })
364
+ );
365
+ });
366
+
367
+ app.get('/mcp', async (req: Request, res: Response) => {
368
+ const sessionId = req.headers['mcp-session-id'] as string | undefined;
369
+ if (!sessionId || !sessions.has(sessionId)) {
370
+ res.writeHead(400, { 'Content-Type': 'application/json' });
371
+ res.end(
372
+ JSON.stringify({
373
+ jsonrpc: '2.0',
374
+ error: { code: -32000, message: 'Bad Request: No valid session ID' },
375
+ id: null,
376
+ })
377
+ );
378
+ return;
379
+ }
380
+ await sessions.get(sessionId)!.transport.handleRequest(req, res);
381
+ });
382
+
383
+ app.delete('/mcp', async (req: Request, res: Response) => {
384
+ const sessionId = req.headers['mcp-session-id'] as string | undefined;
385
+ if (!sessionId || !sessions.has(sessionId)) {
386
+ res.writeHead(400, { 'Content-Type': 'application/json' });
387
+ res.end(
388
+ JSON.stringify({
389
+ jsonrpc: '2.0',
390
+ error: { code: -32000, message: 'Bad Request: No valid session ID' },
391
+ id: null,
392
+ })
393
+ );
394
+ return;
395
+ }
396
+ await sessions.get(sessionId)!.transport.handleRequest(req, res);
397
+ });
398
+ }
@@ -0,0 +1,41 @@
1
+ import type { IPty } from 'node-pty';
2
+
3
+ export interface PtyEntry {
4
+ ptyProcess: IPty;
5
+ chunks: string[];
6
+ totalLen: number;
7
+ }
8
+
9
+ export interface PaneInfo {
10
+ id: string;
11
+ shellType: string;
12
+ title: string;
13
+ num: number;
14
+ cwd: string | null;
15
+ url?: string;
16
+ projectId: string | null;
17
+ }
18
+
19
+ export interface DashboardState {
20
+ panes: PaneInfo[];
21
+ activeTab: string;
22
+ activePaneId: string | null;
23
+ }
24
+
25
+ export interface ShellConfig {
26
+ command: string;
27
+ args: string[];
28
+ env: Record<string, string>;
29
+ }
30
+
31
+ export interface McpState {
32
+ globalPtys: Map<string, PtyEntry>;
33
+ dashboardState: DashboardState;
34
+ io: any;
35
+ spawnGlobalPty: (id: string, command: string, args: string[], env: Record<string, string>, cols: number, rows: number, trackCwd: boolean, oscOnly: boolean, startCwd: string | null) => IPty;
36
+ SHELL_CONFIGS: Record<string, ShellConfig>;
37
+ MAX_PANES: number;
38
+ nextPaneId: () => string;
39
+ paneActiveSocket: Map<string, string>;
40
+ socketDimensions: Map<string, Map<string, { cols: number; rows: number }>>;
41
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../packages/tsconfig/node.json",
3
+ "compilerOptions": {
4
+ "noEmit": true
5
+ },
6
+ "include": ["src"],
7
+ "exclude": ["node_modules", "dist"]
8
+ }
@@ -0,0 +1,5 @@
1
+  WARN  Issue while reading "/home/runner/_work/devglide/devglide/.npmrc". Failed to replace env in config: ${NODE_AUTH_TOKEN}
2
+
3
+ > @devglide/test@0.1.0 build /home/runner/_work/devglide/devglide/src/apps/test
4
+ > tsc
5
+
@@ -0,0 +1,27 @@
1
+  WARN  Issue while reading "/home/runner/_work/devglide/devglide/.npmrc". Failed to replace env in config: ${NODE_AUTH_TOKEN}
2
+
3
+ > @devglide/test@0.1.0 lint /home/runner/_work/devglide/devglide/src/apps/test
4
+ > eslint .
5
+
6
+
7
+ /home/runner/_work/devglide/devglide/src/apps/test/src/index.ts
8
+ 1:1 warning There should be at least one empty line between import groups import/order
9
+ 2:1 warning `@devglide/mcp-utils` import should occur before import of `./mcp.js` import/order
10
+
11
+ /home/runner/_work/devglide/devglide/src/apps/test/src/mcp.ts
12
+ 1:1 warning There should be at least one empty line between import groups import/order
13
+ 2:1 warning There should be at least one empty line between import groups import/order
14
+
15
+ /home/runner/_work/devglide/devglide/src/apps/test/src/routes/trigger.ts
16
+ 1:1 warning There should be at least one empty line between import groups import/order
17
+ 3:1 warning There should be at least one empty line between import groups import/order
18
+
19
+ /home/runner/_work/devglide/devglide/src/apps/test/src/services/scenario-manager.ts
20
+ 1:1 warning There should be at least one empty line between import groups import/order
21
+
22
+ /home/runner/_work/devglide/devglide/src/apps/test/src/services/scenario-store.ts
23
+ 3:1 warning There should be at least one empty line between import groups import/order
24
+
25
+ ✖ 8 problems (0 errors, 8 warnings)
26
+ 0 errors and 8 warnings potentially fixable with the `--fix` option.
27
+
@@ -0,0 +1,5 @@
1
+  WARN  Issue while reading "/home/runner/_work/devglide/devglide/.npmrc". Failed to replace env in config: ${NODE_AUTH_TOKEN}
2
+
3
+ > @devglide/test@0.1.0 typecheck /home/runner/_work/devglide/devglide/src/apps/test
4
+ > tsc --noEmit
5
+
@@ -0,0 +1,21 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+
4
+ case `uname` in
5
+ *CYGWIN*|*MINGW*|*MSYS*)
6
+ if command -v cygpath > /dev/null 2>&1; then
7
+ basedir=`cygpath -w "$basedir"`
8
+ fi
9
+ ;;
10
+ esac
11
+
12
+ if [ -z "$NODE_PATH" ]; then
13
+ export NODE_PATH="/home/runner/_work/devglide/devglide/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/_work/devglide/devglide/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/_work/devglide/devglide/node_modules/.pnpm/node_modules"
14
+ else
15
+ export NODE_PATH="/home/runner/_work/devglide/devglide/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/_work/devglide/devglide/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/_work/devglide/devglide/node_modules/.pnpm/node_modules:$NODE_PATH"
16
+ fi
17
+ if [ -x "$basedir/node" ]; then
18
+ exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
19
+ else
20
+ exec node "$basedir/../typescript/bin/tsc" "$@"
21
+ fi
@@ -0,0 +1,21 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+
4
+ case `uname` in
5
+ *CYGWIN*|*MINGW*|*MSYS*)
6
+ if command -v cygpath > /dev/null 2>&1; then
7
+ basedir=`cygpath -w "$basedir"`
8
+ fi
9
+ ;;
10
+ esac
11
+
12
+ if [ -z "$NODE_PATH" ]; then
13
+ export NODE_PATH="/home/runner/_work/devglide/devglide/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/_work/devglide/devglide/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/_work/devglide/devglide/node_modules/.pnpm/node_modules"
14
+ else
15
+ export NODE_PATH="/home/runner/_work/devglide/devglide/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/_work/devglide/devglide/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/_work/devglide/devglide/node_modules/.pnpm/node_modules:$NODE_PATH"
16
+ fi
17
+ if [ -x "$basedir/node" ]; then
18
+ exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@"
19
+ else
20
+ exec node "$basedir/../typescript/bin/tsserver" "$@"
21
+ fi