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,463 @@
1
+ /* ── Log page module ─────────────────────────────────────────────── */
2
+
3
+ import { escapeHtml, timeAgo } from '/shared-assets/ui-utils.js';
4
+
5
+ const HTML = `
6
+ <header>
7
+ <div class="brand" data-action="show-sessions">Log</div>
8
+ <div class="header-meta">
9
+ <span id="file-count-badge" class="badge">
10
+ <span id="file-count">0</span> app<span id="file-plural">s</span>
11
+ </span>
12
+ <button id="btn-clear-all" class="btn-danger" data-action="clear-all" disabled style="display:none"
13
+ title="Clear log files for all active sessions">
14
+ Clear all logs
15
+ </button>
16
+ <span>
17
+ auto-refresh 3s
18
+ <span class="refresh-indicator" id="refresh-dot"></span>
19
+ </span>
20
+ </div>
21
+ </header>
22
+
23
+ <main>
24
+ <!-- -- File list view --------------------------------------------- -->
25
+ <div id="view-sessions">
26
+ <div class="section-title">Log Files</div>
27
+ <div id="file-list" class="file-list">
28
+ <div class="empty" id="empty-state">
29
+ No log files visible.<br/>
30
+ Add <code>&lt;script src="http://localhost:7000/devtools.js?target=/path/to/app"&gt;&lt;/script&gt;</code> to any external app, or use the devtools middleware for DevGlide monorepo apps.
31
+ </div>
32
+ </div>
33
+ </div>
34
+
35
+ <!-- -- Log viewer view -------------------------------------------- -->
36
+ <div id="view-log" class="hidden">
37
+ <div class="viewer-header">
38
+ <div class="viewer-header-left">
39
+ <button class="btn-secondary" data-action="show-sessions">&larr; Back</button>
40
+ <span class="viewer-title" id="viewer-title"></span>
41
+ <span class="source-badge" id="viewer-source-badge"></span>
42
+ </div>
43
+ <button class="btn-danger" id="btn-clear-session" data-action="clear-session">Clear</button>
44
+ </div>
45
+ <div class="source-toggle" id="source-toggle">
46
+ <button class="active" data-source="all" aria-label="Show all sources" aria-pressed="true">All</button>
47
+ <button data-source="browser" aria-label="Show browser logs only" aria-pressed="false">Browser</button>
48
+ <button data-source="server" aria-label="Show server logs only" aria-pressed="false">Server</button>
49
+ <button data-source="file" aria-label="Show file logs only" aria-pressed="false">File</button>
50
+ </div>
51
+ <div class="filter-bar" id="filter-bar"></div>
52
+ <div class="log-entries" id="log-entries" role="log" aria-live="polite" aria-label="Log entries"></div>
53
+ </div>
54
+ </main>
55
+ `;
56
+
57
+ /* ── Constants ──────────────────────────────────────────────────────── */
58
+ const NOW_ACTIVE_MS = 10_000;
59
+ const NOW_IDLE_MS = 60_000;
60
+ const ERROR_TYPES = new Set(['ERROR', 'WINDOW_ERROR', 'UNHANDLED_REJECTION', 'SERVER_ERROR', 'FILE_ERROR']);
61
+ const WARN_TYPES = new Set(['WARN', 'SERVER_WARN', 'FILE_WARN']);
62
+
63
+ /* ── Helpers (pure) ─────────────────────────────────────────────────── */
64
+ const reltime = timeAgo;
65
+
66
+ function fileStatus(lastSeen) {
67
+ const diff = Date.now() - new Date(lastSeen).getTime();
68
+ if (diff < NOW_ACTIVE_MS) return 'active';
69
+ if (diff < NOW_IDLE_MS) return 'idle';
70
+ return 'stale';
71
+ }
72
+
73
+ const escHtml = escapeHtml;
74
+
75
+ function entrySource(entry) {
76
+ const t = entry.type || '';
77
+ if (t.startsWith('FILE_')) return 'file';
78
+ return t.startsWith('SERVER_') ? 'server' : 'browser';
79
+ }
80
+
81
+ function formatTime(ts) {
82
+ if (!ts) return '';
83
+ try {
84
+ const d = new Date(ts);
85
+ return d.toLocaleTimeString('en-GB', { hour12: false }) + '.' + String(d.getMilliseconds()).padStart(3, '0');
86
+ } catch { return ''; }
87
+ }
88
+
89
+ function appName(targetPath) {
90
+ const parts = targetPath.replace(/\\/g, '/').split('/').filter(Boolean);
91
+ const appsIdx = parts.lastIndexOf('apps');
92
+ if (appsIdx !== -1 && parts[appsIdx + 1]) return parts[appsIdx + 1];
93
+ return parts.length >= 2 ? parts[parts.length - 2] : parts[parts.length - 1] || targetPath;
94
+ }
95
+
96
+ function appKey(targetPath) {
97
+ const parts = targetPath.replace(/\\/g, '/').split('/').filter(Boolean);
98
+ const appsIdx = parts.lastIndexOf('apps');
99
+ if (appsIdx !== -1 && parts[appsIdx + 1]) {
100
+ return '/' + parts.slice(0, appsIdx + 2).join('/');
101
+ }
102
+ return '/' + parts.slice(0, -1).join('/');
103
+ }
104
+
105
+ function groupByApp(sessions) {
106
+ const map = new Map();
107
+ for (const s of sessions) {
108
+ const key = appKey(s.targetPath);
109
+ if (!map.has(key)) {
110
+ map.set(key, { key, name: appName(s.targetPath), targetPaths: new Set(), sources: new Set(), logCount: 0, errorCount: 0, lastSeen: s.lastSeen });
111
+ }
112
+ const app = map.get(key);
113
+ app.targetPaths.add(s.targetPath);
114
+ app.sources.add(s.source);
115
+ app.logCount += s.logCount || 0;
116
+ app.errorCount += s.errorCount || 0;
117
+ if (new Date(s.lastSeen) > new Date(app.lastSeen)) app.lastSeen = s.lastSeen;
118
+ }
119
+ return [...map.values()]
120
+ .map(a => ({ ...a, targetPaths: [...a.targetPaths], sources: [...a.sources] }))
121
+ .sort((a, b) => new Date(b.lastSeen) - new Date(a.lastSeen));
122
+ }
123
+
124
+ /* ── Module state (per-mount) ───────────────────────────────────────── */
125
+ let _container = null;
126
+ let _pollTimer = null;
127
+ let _visibilityHandler = null;
128
+
129
+ let currentView = 'sessions';
130
+ let currentTargetPaths = [];
131
+ let activeFilters = new Set();
132
+ let sourceFilter = 'all';
133
+ let allEntries = [];
134
+ let allSessions = [];
135
+ let activeProjectPath = null;
136
+
137
+ /* ── DOM helpers ────────────────────────────────────────────────────── */
138
+ function $(sel) { return _container.querySelector(sel); }
139
+ function $$(sel) { return _container.querySelectorAll(sel); }
140
+
141
+ /* ── Rendering ──────────────────────────────────────────────────────── */
142
+ function renderSessions(sessions) {
143
+ allSessions = sessions;
144
+ const apps = groupByApp(sessions);
145
+ const count = apps.length;
146
+
147
+ $('#file-count').textContent = count;
148
+ $('#file-plural').textContent = count === 1 ? '' : 's';
149
+
150
+ const btn = $('#btn-clear-all');
151
+ btn.style.display = count > 0 ? '' : 'none';
152
+ btn.disabled = count === 0;
153
+
154
+ const list = $('#file-list');
155
+
156
+ if (apps.length === 0) {
157
+ list.innerHTML = '<div class="empty" id="empty-state">No log files visible.<br/>Add <code>&lt;script src="http://localhost:7000/devtools.js?target=/path/to/app"&gt;&lt;/script&gt;</code> to any external app, or use the devtools middleware for DevGlide monorepo apps.</div>';
158
+ return;
159
+ }
160
+
161
+ list.innerHTML = apps.map(app => {
162
+ const status = fileStatus(app.lastSeen);
163
+ const hasErrors = app.errorCount > 0;
164
+ const pathsAttr = escHtml(JSON.stringify(app.targetPaths));
165
+ const sourceBadges = app.sources.map(s =>
166
+ `<span class="file-source-item ${s}">${s}</span>`
167
+ ).join('');
168
+ const fileNames = app.targetPaths.map(p => p.split('/').pop()).join(', ');
169
+ return `
170
+ <div class="file-card" data-action="open-viewer" data-paths="${pathsAttr}" data-name="${escHtml(app.name)}">
171
+ <div class="status-dot ${status}"></div>
172
+ <div class="file-main">
173
+ <div class="file-name">${escHtml(app.name)}</div>
174
+ <div class="file-path" title="${escHtml(fileNames)}">${escHtml(fileNames)}</div>
175
+ <div class="file-sources">${sourceBadges}</div>
176
+ </div>
177
+ <div class="file-stats">
178
+ <div class="stat ${hasErrors ? 'has-errors' : ''}">
179
+ ${hasErrors ? '&#9888; ' + app.errorCount + ' error' + (app.errorCount !== 1 ? 's' : '') : app.logCount + ' log' + (app.logCount !== 1 ? 's' : '')}
180
+ </div>
181
+ <div class="last-seen">${reltime(app.lastSeen)}</div>
182
+ </div>
183
+ </div>
184
+ `;
185
+ }).join('');
186
+ }
187
+
188
+ /* ── View switching ─────────────────────────────────────────────────── */
189
+ function showSessions() {
190
+ currentView = 'sessions';
191
+ currentTargetPaths = [];
192
+ $('#view-sessions').classList.remove('hidden');
193
+ $('#view-log').classList.add('hidden');
194
+ }
195
+
196
+ function openViewer(targetPaths, name) {
197
+ currentView = 'log';
198
+ currentTargetPaths = Array.isArray(targetPaths) ? targetPaths : [targetPaths];
199
+ activeFilters.clear();
200
+ sourceFilter = 'all';
201
+
202
+ $('#view-sessions').classList.add('hidden');
203
+ $('#view-log').classList.remove('hidden');
204
+
205
+ $('#viewer-title').textContent = name || appName(currentTargetPaths[0]);
206
+ $('#viewer-source-badge').textContent = '';
207
+ $('#viewer-source-badge').className = 'source-badge';
208
+
209
+ // Reset source toggle
210
+ $$('#source-toggle button').forEach(b => {
211
+ const isActive = b.dataset.source === 'all';
212
+ b.classList.toggle('active', isActive);
213
+ b.setAttribute('aria-pressed', isActive);
214
+ });
215
+
216
+ refreshLog();
217
+ }
218
+
219
+ /* ── Log viewer ─────────────────────────────────────────────────────── */
220
+ async function refreshLog() {
221
+ if (currentTargetPaths.length === 0) return;
222
+ try {
223
+ const fetches = currentTargetPaths.map(tp =>
224
+ fetch('/api/log/view?targetPath=' + encodeURIComponent(tp) + '&limit=500')
225
+ .then(r => r.ok ? r.json() : { entries: [] })
226
+ .then(d => d.entries || [])
227
+ .catch(() => [])
228
+ );
229
+ const results = await Promise.all(fetches);
230
+ allEntries = results.flat().sort((a, b) => {
231
+ const ta = a.ts || '';
232
+ const tb = b.ts || '';
233
+ return ta < tb ? -1 : ta > tb ? 1 : 0;
234
+ });
235
+ renderFilters();
236
+ renderEntries();
237
+ } catch (e) { /* silently ignore */ }
238
+ }
239
+
240
+ function renderFilters() {
241
+ const types = new Set(allEntries.map(e => e.type || 'LOG'));
242
+ const bar = $('#filter-bar');
243
+ const sorted = [...types].sort();
244
+ bar.innerHTML = sorted.map(t => {
245
+ const isActive = activeFilters.has(t);
246
+ const colorClass = ERROR_TYPES.has(t) ? ' red' : WARN_TYPES.has(t) ? ' yellow' : '';
247
+ return `<button class="filter-pill${isActive ? ' active' + colorClass : ''}" data-action="toggle-filter" data-type="${escHtml(t)}" aria-label="Filter by ${escHtml(t)}" aria-pressed="${isActive}">${escHtml(t)}</button>`;
248
+ }).join('');
249
+ }
250
+
251
+ function toggleFilter(type) {
252
+ if (activeFilters.has(type)) activeFilters.delete(type);
253
+ else activeFilters.add(type);
254
+ renderFilters();
255
+ renderEntries();
256
+ }
257
+
258
+ function setSourceFilter(src) {
259
+ sourceFilter = src;
260
+ $$('#source-toggle button').forEach(b => {
261
+ const isActive = b.dataset.source === src;
262
+ b.classList.toggle('active', isActive);
263
+ b.setAttribute('aria-pressed', isActive);
264
+ });
265
+ renderEntries();
266
+ }
267
+
268
+ function renderEntries() {
269
+ const el = $('#log-entries');
270
+ let filtered = allEntries;
271
+
272
+ if (activeFilters.size > 0) {
273
+ filtered = filtered.filter(e => activeFilters.has(e.type || 'LOG'));
274
+ }
275
+ if (sourceFilter !== 'all') {
276
+ filtered = filtered.filter(e => entrySource(e) === sourceFilter);
277
+ }
278
+
279
+ if (filtered.length === 0) {
280
+ el.innerHTML = '<div style="padding:var(--df-space-6);text-align:center;color:var(--df-color-text-muted);text-transform:uppercase;font-size:var(--df-font-size-xs);letter-spacing:var(--df-letter-spacing-wide)">No entries</div>';
281
+ return;
282
+ }
283
+
284
+ el.innerHTML = filtered.map(e => {
285
+ const type = e.type || 'LOG';
286
+ const src = entrySource(e);
287
+ const isErr = ERROR_TYPES.has(type);
288
+ const msg = e.message || (e.type === 'SESSION_START' ? 'Session started' : '');
289
+ return `<div class="log-entry${isErr ? ' log-entry-error' : ''}">
290
+ <span class="log-src ${src}">${src === 'file' ? 'F' : src === 'server' ? 'S' : 'B'}</span>
291
+ <span class="log-time">${formatTime(e.ts)}</span>
292
+ <span class="log-level ${type}">${type}</span>
293
+ <span class="log-message">${escHtml(msg)}</span>
294
+ </div>`;
295
+ }).join('');
296
+
297
+ // Auto-scroll to bottom only if user is already near the bottom
298
+ const isNearBottom = (el.scrollHeight - el.scrollTop - el.clientHeight) < 50;
299
+ if (isNearBottom) {
300
+ el.scrollTop = el.scrollHeight;
301
+ }
302
+ }
303
+
304
+ async function clearSessionLog() {
305
+ if (currentTargetPaths.length === 0) return;
306
+ try {
307
+ await Promise.all(currentTargetPaths.map(tp =>
308
+ fetch('/api/log?targetPath=' + encodeURIComponent(tp), { method: 'DELETE' }).catch(() => {})
309
+ ));
310
+ allEntries = [];
311
+ renderEntries();
312
+ } catch (e) { /* silently ignore */ }
313
+ }
314
+
315
+ async function clearAllLogs() {
316
+ const btn = $('#btn-clear-all');
317
+ btn.disabled = true;
318
+ try {
319
+ await fetch('/api/log/all', { method: 'DELETE' });
320
+ await refresh();
321
+ } catch (e) { /* silently ignore */ }
322
+ finally { btn.disabled = false; }
323
+ }
324
+
325
+ /* ── Refresh loop ───────────────────────────────────────────────────── */
326
+ async function refresh() {
327
+ try {
328
+ let statusUrl = '/api/log/status';
329
+ if (activeProjectPath) {
330
+ statusUrl += '?projectPath=' + encodeURIComponent(activeProjectPath);
331
+ }
332
+ const res = await fetch(statusUrl);
333
+ if (!res.ok) return;
334
+ const { sessions } = await res.json();
335
+ if (currentView === 'sessions') {
336
+ renderSessions(sessions);
337
+ } else {
338
+ allSessions = sessions;
339
+ refreshLog();
340
+ }
341
+ flash();
342
+ } catch (e) { /* silently ignore */ }
343
+ }
344
+
345
+ function flash() {
346
+ const dot = $('#refresh-dot');
347
+ if (!dot) return;
348
+ dot.classList.add('flash');
349
+ setTimeout(() => dot.classList.remove('flash'), 300);
350
+ }
351
+
352
+ /* ── Delegated event handler ────────────────────────────────────────── */
353
+ function handleClick(e) {
354
+ const action = e.target.closest('[data-action]');
355
+ if (!action) return;
356
+
357
+ switch (action.dataset.action) {
358
+ case 'show-sessions':
359
+ showSessions();
360
+ break;
361
+ case 'clear-all':
362
+ clearAllLogs();
363
+ break;
364
+ case 'clear-session':
365
+ clearSessionLog();
366
+ break;
367
+ case 'open-viewer': {
368
+ const paths = JSON.parse(action.dataset.paths);
369
+ const name = action.dataset.name;
370
+ openViewer(paths, name);
371
+ break;
372
+ }
373
+ case 'toggle-filter':
374
+ toggleFilter(action.dataset.type);
375
+ break;
376
+ }
377
+
378
+ // Source toggle buttons
379
+ if (action.closest('#source-toggle') && action.dataset.source) {
380
+ setSourceFilter(action.dataset.source);
381
+ }
382
+ }
383
+
384
+ function handleSourceToggle(e) {
385
+ const btn = e.target.closest('#source-toggle button[data-source]');
386
+ if (btn) {
387
+ setSourceFilter(btn.dataset.source);
388
+ }
389
+ }
390
+
391
+ /* ── Lifecycle ──────────────────────────────────────────────────────── */
392
+
393
+ export function mount(container, ctx) {
394
+ _container = container;
395
+ container.classList.add('page-log');
396
+
397
+ // Reset module state
398
+ currentView = 'sessions';
399
+ currentTargetPaths = [];
400
+ activeFilters = new Set();
401
+ sourceFilter = 'all';
402
+ allEntries = [];
403
+ allSessions = [];
404
+ activeProjectPath = ctx.project?.path || null;
405
+
406
+ // Build HTML
407
+ container.innerHTML = HTML;
408
+
409
+ // Attach delegated click handler
410
+ container.addEventListener('click', handleClick);
411
+ container.addEventListener('click', handleSourceToggle);
412
+
413
+ // Visibility change handler for pause/resume polling
414
+ _visibilityHandler = () => {
415
+ if (document.hidden) {
416
+ clearInterval(_pollTimer);
417
+ _pollTimer = null;
418
+ } else {
419
+ refresh();
420
+ _pollTimer = setInterval(refresh, 3000);
421
+ }
422
+ };
423
+ document.addEventListener('visibilitychange', _visibilityHandler);
424
+
425
+ // Initial fetch + start polling
426
+ refresh();
427
+ _pollTimer = setInterval(refresh, 3000);
428
+ }
429
+
430
+ export function unmount(container) {
431
+ // Stop polling
432
+ if (_pollTimer) {
433
+ clearInterval(_pollTimer);
434
+ _pollTimer = null;
435
+ }
436
+
437
+ // Remove visibility handler
438
+ if (_visibilityHandler) {
439
+ document.removeEventListener('visibilitychange', _visibilityHandler);
440
+ _visibilityHandler = null;
441
+ }
442
+
443
+ // Remove delegated listeners
444
+ if (container) {
445
+ container.removeEventListener('click', handleClick);
446
+ container.removeEventListener('click', handleSourceToggle);
447
+ }
448
+
449
+ // Clean up container
450
+ if (container) {
451
+ container.classList.remove('page-log');
452
+ container.innerHTML = '';
453
+ }
454
+
455
+ _container = null;
456
+ }
457
+
458
+ export function onProjectChange(project) {
459
+ activeProjectPath = project?.path || null;
460
+ // Return to sessions view on project switch to avoid showing stale logs
461
+ if (currentView === 'log') showSessions();
462
+ refresh();
463
+ }
@@ -0,0 +1,9 @@
1
+ import { createLogMcpServer } from "./mcp.js";
2
+ import { runStdio } from "@devglide/mcp-utils";
3
+
4
+ // ── Stdio MCP mode ──────────────────────────────────────────────────────────
5
+ if (process.argv.includes("--stdio")) {
6
+ const mcpServer = createLogMcpServer();
7
+ await runStdio(mcpServer);
8
+ console.error("Devglide Log MCP server running on stdio");
9
+ }
@@ -0,0 +1,122 @@
1
+ import { z } from "zod";
2
+ import fs from "fs/promises";
3
+ import path from "path";
4
+ import { createDevglideMcpServer } from "../../../packages/mcp-utils/src/index.js";
5
+ import { LogWriter } from "./services/log-writer.js";
6
+ import { getTargetPaths } from "./routes/log.js";
7
+ import { LOGS_DIR } from "../../../packages/paths.js";
8
+
9
+ const LOG_ROOT = LOGS_DIR;
10
+ const ALLOWED_EXTENSIONS = new Set(['.log', '.jsonl']);
11
+
12
+ function safeLogPath(targetPath: string): string {
13
+ const resolved = path.resolve(LOG_ROOT, targetPath.replace(/^\/+/, ''));
14
+ if (!resolved.startsWith(LOG_ROOT + path.sep)) {
15
+ throw new Error('Path traversal denied');
16
+ }
17
+ const ext = path.extname(resolved).toLowerCase();
18
+ if (ext && !ALLOWED_EXTENSIONS.has(ext)) {
19
+ throw new Error('Invalid log file extension');
20
+ }
21
+ return resolved;
22
+ }
23
+
24
+ const logWriter = new LogWriter();
25
+
26
+ export function createLogMcpServer() {
27
+ const server = createDevglideMcpServer(
28
+ "devglide-log",
29
+ "0.1.0",
30
+ "Browser console capture and log streaming. " +
31
+ "The unified server serves GET /devtools.js?target=/path/to/app — a central bootstrap for external apps. " +
32
+ "Add <script src=\"http://localhost:7000/devtools.js?target=/path/to/app\"></script> to any external app " +
33
+ "to inject console-sniffer and scenario-runner. The ?target param is required — it identifies the app directory for log capture and test scenarios."
34
+ );
35
+
36
+ server.tool(
37
+ "log_write",
38
+ "Append a log entry to a JSONL file",
39
+ {
40
+ targetPath: z.string().describe("Absolute path to the JSONL log file"),
41
+ type: z.string().optional().describe("Log type (e.g. LOG, ERROR, WARN). Default: LOG"),
42
+ message: z.string().optional().describe("Log message"),
43
+ source: z.string().optional().describe("Source file"),
44
+ line: z.number().optional().describe("Line number"),
45
+ col: z.number().optional().describe("Column number"),
46
+ stack: z.string().optional().describe("Stack trace"),
47
+ },
48
+ async ({ targetPath, type, message, source, line, col, stack }) => {
49
+ const safePath = safeLogPath(targetPath);
50
+ const entry: Record<string, unknown> = {
51
+ type: type || "LOG",
52
+ ts: new Date().toISOString(),
53
+ };
54
+ if (message) entry.message = message;
55
+ if (source) entry.source = source;
56
+ if (line !== undefined) entry.line = line;
57
+ if (col !== undefined) entry.col = col;
58
+ if (stack) entry.stack = stack;
59
+
60
+ await logWriter.append(safePath, entry);
61
+ return { content: [{ type: "text" as const, text: "Log entry written." }] };
62
+ }
63
+ );
64
+
65
+ server.tool(
66
+ "log_clear",
67
+ "Truncate a JSONL log file",
68
+ {
69
+ targetPath: z.string().describe("Absolute path to the JSONL log file"),
70
+ },
71
+ async ({ targetPath }) => {
72
+ const safePath = safeLogPath(targetPath);
73
+ await logWriter.clear(safePath);
74
+ return {
75
+ content: [{ type: "text" as const, text: `Log file cleared: ${safePath}` }],
76
+ };
77
+ }
78
+ );
79
+
80
+ server.tool(
81
+ "log_clear_all",
82
+ "Truncate log files for all currently tracked sessions",
83
+ {},
84
+ async () => {
85
+ const paths = getTargetPaths();
86
+ await Promise.all(paths.map((p) => logWriter.clear(p).catch(() => {})));
87
+ return {
88
+ content: [
89
+ { type: "text" as const, text: `Cleared ${paths.length} log file(s): ${paths.join(", ") || "(none)"}` },
90
+ ],
91
+ };
92
+ }
93
+ );
94
+
95
+ server.tool(
96
+ "log_read",
97
+ "Read recent log entries from a JSONL file",
98
+ {
99
+ targetPath: z.string().describe("Absolute path to the JSONL log file"),
100
+ lines: z.number().optional().describe("Number of recent lines to return (default: 50)"),
101
+ },
102
+ async ({ targetPath, lines }) => {
103
+ const safePath = safeLogPath(targetPath);
104
+ const limit = lines ?? 50;
105
+ try {
106
+ const content = await fs.readFile(safePath, "utf-8");
107
+ const allLines = content.trim().split("\n").filter(Boolean);
108
+ const recent = allLines.slice(-limit);
109
+ return {
110
+ content: [{ type: "text" as const, text: recent.join("\n") || "(empty)" }],
111
+ };
112
+ } catch (err: unknown) {
113
+ if (err instanceof Error && 'code' in err && (err as NodeJS.ErrnoException).code === "ENOENT") {
114
+ return { content: [{ type: "text" as const, text: "(file does not exist)" }] };
115
+ }
116
+ throw err;
117
+ }
118
+ }
119
+ );
120
+
121
+ return server;
122
+ }