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,333 @@
1
+ import { Router } from "express";
2
+ import type { Request, Response, Router as RouterType } from "express";
3
+ import fs from "fs/promises";
4
+ import path from "path";
5
+ import { LogWriter } from "../services/log-writer.js";
6
+ import { LOGS_DIR } from "../../../../packages/paths.js";
7
+
8
+ const LOG_ROOT = LOGS_DIR;
9
+ const ALLOWED_EXTENSIONS = new Set(['.log', '.jsonl']);
10
+
11
+ function safeLogPath(targetPath: string): string {
12
+ const resolved = path.resolve(LOG_ROOT, targetPath.replace(/^\/+/, ''));
13
+ if (!resolved.startsWith(LOG_ROOT + path.sep)) {
14
+ throw new Error('Path traversal denied');
15
+ }
16
+ const ext = path.extname(resolved).toLowerCase();
17
+ if (ext && !ALLOWED_EXTENSIONS.has(ext)) {
18
+ throw new Error('Invalid log file extension');
19
+ }
20
+ return resolved;
21
+ }
22
+
23
+ export const logRouter: RouterType = Router();
24
+ const logWriter = new LogWriter();
25
+
26
+ // ── Session tracking ──────────────────────────────────────────────────────────
27
+
28
+ interface SessionInfo {
29
+ sessionId: string;
30
+ targetPath: string;
31
+ url: string;
32
+ ua: string;
33
+ firstSeen: string;
34
+ lastSeen: string;
35
+ logCount: number;
36
+ errorCount: number;
37
+ source: "browser" | "server" | "file";
38
+ }
39
+
40
+ const STALE_MS = 5 * 60 * 1000;
41
+
42
+ const sessions = new Map<string, SessionInfo>();
43
+
44
+ // Periodic cleanup — evict stale sessions even when getSessions() isn't called
45
+ const sessionCleanupTimer = setInterval(() => {
46
+ const cutoff = Date.now() - STALE_MS;
47
+ for (const [id, session] of sessions) {
48
+ if (new Date(session.lastSeen).getTime() < cutoff) {
49
+ sessions.delete(id);
50
+ if (persistentByTarget.get(session.targetPath) === id) {
51
+ persistentByTarget.delete(session.targetPath);
52
+ }
53
+ }
54
+ }
55
+ }, 60_000);
56
+ sessionCleanupTimer.unref();
57
+ // For persistent sessions: targetPath → canonical sessionId
58
+ const persistentByTarget = new Map<string, string>();
59
+ // Remap transient sessionId → canonical sessionId for persistent reconnects
60
+ const SESSION_ID_REMAP_MAX = 1000;
61
+ const sessionIdRemap = new Map<string, string>();
62
+
63
+ export function recordSession(entry: Record<string, unknown>): void {
64
+ let sessionId = entry.session as string | undefined;
65
+ if (!sessionId) return;
66
+
67
+ const targetPath = (entry.targetPath as string) || "";
68
+ const now = new Date().toISOString();
69
+ const type = (entry.type as string) || "";
70
+ const isPersistent = entry.persistent === true;
71
+ const isFileType = type.startsWith("FILE_");
72
+ const isServerType = type.startsWith("SERVER_");
73
+ const source: "browser" | "server" | "file" = isFileType ? "file" : isServerType ? "server" : "browser";
74
+ const isError =
75
+ type === "ERROR" || type === "WINDOW_ERROR" || type === "UNHANDLED_REJECTION" || type === "SERVER_ERROR" || type === "FILE_ERROR";
76
+
77
+ if (type === "SESSION_START" && isPersistent) {
78
+ const canonicalId = persistentByTarget.get(targetPath);
79
+ if (canonicalId && sessions.has(canonicalId)) {
80
+ // Cap remap size to prevent unbounded growth
81
+ if (sessionIdRemap.size >= SESSION_ID_REMAP_MAX) {
82
+ const firstKey = sessionIdRemap.keys().next().value!;
83
+ sessionIdRemap.delete(firstKey);
84
+ }
85
+ sessionIdRemap.set(sessionId, canonicalId);
86
+ const existing = sessions.get(canonicalId)!;
87
+ existing.lastSeen = now;
88
+ if (entry.url) existing.url = entry.url as string;
89
+ return;
90
+ }
91
+ persistentByTarget.set(targetPath, sessionId);
92
+ }
93
+
94
+ const remapped = sessionIdRemap.get(sessionId);
95
+ if (remapped) sessionId = remapped;
96
+
97
+ const isSessionMeta = type === "SESSION_START";
98
+ const existing = sessions.get(sessionId);
99
+ if (existing) {
100
+ existing.lastSeen = now;
101
+ if (!isSessionMeta) existing.logCount++;
102
+ if (isError) existing.errorCount++;
103
+ if (entry.url) existing.url = entry.url as string;
104
+ } else {
105
+ sessions.set(sessionId, {
106
+ sessionId,
107
+ targetPath,
108
+ url: (entry.url as string) || "",
109
+ ua: (entry.ua as string) || "",
110
+ firstSeen: now,
111
+ lastSeen: now,
112
+ logCount: isSessionMeta ? 0 : 1,
113
+ errorCount: isError ? 1 : 0,
114
+ source,
115
+ });
116
+ }
117
+ }
118
+
119
+ export function getSessions(): SessionInfo[] {
120
+ const cutoff = Date.now() - STALE_MS;
121
+ const result: SessionInfo[] = [];
122
+ for (const [id, session] of sessions) {
123
+ if (new Date(session.lastSeen).getTime() < cutoff) {
124
+ sessions.delete(id);
125
+ if (persistentByTarget.get(session.targetPath) === id) {
126
+ persistentByTarget.delete(session.targetPath);
127
+ }
128
+ } else {
129
+ result.push(session);
130
+ }
131
+ }
132
+ return result.sort(
133
+ (a, b) => new Date(b.lastSeen).getTime() - new Date(a.lastSeen).getTime()
134
+ );
135
+ }
136
+
137
+ export function getTargetPaths(): string[] {
138
+ return [...new Set(getSessions().map((s) => s.targetPath))];
139
+ }
140
+
141
+ function resetSessionCounters(targetPath?: string): void {
142
+ for (const session of sessions.values()) {
143
+ if (!targetPath || session.targetPath === targetPath) {
144
+ session.logCount = 0;
145
+ session.errorCount = 0;
146
+ }
147
+ }
148
+ }
149
+
150
+ // ── Tail reader ──────────────────────────────────────────────────────────────
151
+
152
+ /** Read the last `n` non-empty lines from a file without loading the whole file. */
153
+ async function tailLines(filePath: string, n: number): Promise<string[]> {
154
+ const stat = await fs.stat(filePath);
155
+ const size = stat.size;
156
+ if (size === 0) return [];
157
+
158
+ // For small files (< 128 KB), just read the whole thing
159
+ const SMALL_THRESHOLD = 128 * 1024;
160
+ if (size <= SMALL_THRESHOLD) {
161
+ const content = await fs.readFile(filePath, "utf-8");
162
+ return content.trim().split("\n").filter(Boolean).slice(-n);
163
+ }
164
+
165
+ // For larger files, read chunks from the end
166
+ let chunkSize = 64 * 1024;
167
+ const fh = await fs.open(filePath, "r");
168
+ try {
169
+ let collected: string[] = [];
170
+ let offset = size;
171
+ let trailing = "";
172
+
173
+ while (offset > 0 && collected.length < n) {
174
+ const readSize = Math.min(chunkSize, offset);
175
+ offset -= readSize;
176
+ const buf = Buffer.alloc(readSize);
177
+ await fh.read(buf, 0, readSize, offset);
178
+ const chunk = buf.toString("utf-8") + trailing;
179
+ const lines = chunk.split("\n").filter(Boolean);
180
+ collected = lines.concat(collected);
181
+ trailing = "";
182
+ // Double chunk size for next iteration if we still need more lines
183
+ chunkSize = Math.min(chunkSize * 2, 1024 * 1024);
184
+ }
185
+
186
+ return collected.slice(-n);
187
+ } finally {
188
+ await fh.close();
189
+ }
190
+ }
191
+
192
+ // ── Routes ────────────────────────────────────────────────────────────────────
193
+
194
+ interface LogRequestBody {
195
+ type?: string;
196
+ session?: string;
197
+ seq?: number;
198
+ ts?: string;
199
+ url?: string;
200
+ ua?: string;
201
+ message?: string;
202
+ source?: string;
203
+ line?: number;
204
+ col?: number;
205
+ stack?: string;
206
+ targetPath?: string;
207
+ persistent?: boolean;
208
+ }
209
+
210
+ /**
211
+ * POST /api/log — Append a log entry to the target JSONL file.
212
+ */
213
+ logRouter.post("/", async (req: Request, res: Response) => {
214
+ const body = req.body as LogRequestBody;
215
+ const targetPath = body.targetPath;
216
+
217
+ if (!targetPath || targetPath.trim() === "") {
218
+ res.status(400).end();
219
+ return;
220
+ }
221
+
222
+ const type = body.type && body.type.trim() !== "" ? body.type : "LOG";
223
+ const ts = body.ts && body.ts.trim() !== "" ? body.ts : new Date().toISOString();
224
+
225
+ const entry: Record<string, unknown> = { type };
226
+ if (body.session) entry.session = body.session;
227
+ if (body.seq !== undefined && body.seq !== null) entry.seq = body.seq;
228
+ entry.ts = ts;
229
+ if (body.url) entry.url = body.url;
230
+ if (body.ua) entry.ua = body.ua;
231
+ if (body.message) entry.message = body.message;
232
+ if (body.source) entry.source = body.source;
233
+ if (body.line !== undefined && body.line !== null) entry.line = body.line;
234
+ if (body.col !== undefined && body.col !== null) entry.col = body.col;
235
+ if (body.stack) entry.stack = body.stack;
236
+ if (body.persistent !== undefined) entry.persistent = body.persistent;
237
+
238
+ let safePath: string;
239
+ try {
240
+ safePath = safeLogPath(targetPath);
241
+ } catch {
242
+ res.status(403).json({ error: "Path traversal denied" });
243
+ return;
244
+ }
245
+
246
+ try {
247
+ recordSession({ ...entry, targetPath });
248
+ // Server-side sniffers write directly to disk — skip file write to avoid duplicates.
249
+ // Browser entries (non-persistent or non-server types) still need the log service to write.
250
+ const isServerEntry = type.startsWith("SERVER_") || (type === "SESSION_START" && body.persistent);
251
+ if (!isServerEntry) {
252
+ await logWriter.append(safePath, entry);
253
+ }
254
+ res.status(200).end();
255
+ } catch (err) {
256
+ console.error("[log] Failed to write log:", (err as Error).message);
257
+ res.status(500).end();
258
+ }
259
+ });
260
+
261
+ /**
262
+ * DELETE /api/log/all — Truncate log files for all tracked sessions.
263
+ */
264
+ logRouter.delete("/all", async (_req: Request, res: Response) => {
265
+ const paths = getTargetPaths();
266
+ await Promise.all(paths.map((p) => logWriter.clear(p).catch(() => {})));
267
+ resetSessionCounters();
268
+ res.status(200).json({ cleared: paths.length });
269
+ });
270
+
271
+ /**
272
+ * DELETE /api/log?targetPath=... — Truncate (clear) the log file.
273
+ */
274
+ logRouter.delete("/", async (req: Request, res: Response) => {
275
+ const targetPath = req.query.targetPath as string | undefined;
276
+
277
+ if (!targetPath || targetPath.trim() === "") {
278
+ res.status(400).end();
279
+ return;
280
+ }
281
+
282
+ let safePath: string;
283
+ try {
284
+ safePath = safeLogPath(targetPath);
285
+ } catch {
286
+ res.status(403).json({ error: "Path traversal denied" });
287
+ return;
288
+ }
289
+
290
+ try {
291
+ await logWriter.clear(safePath);
292
+ resetSessionCounters(targetPath);
293
+ res.status(200).end();
294
+ } catch (err) {
295
+ console.error("[log] Failed to clear log:", (err as Error).message);
296
+ res.status(500).end();
297
+ }
298
+ });
299
+
300
+ /**
301
+ * GET /api/log/view?targetPath=...&limit=200 — Read parsed JSONL entries.
302
+ */
303
+ logRouter.get("/view", async (req: Request, res: Response) => {
304
+ const targetPath = req.query.targetPath as string | undefined;
305
+ const limit = Math.min(parseInt(req.query.limit as string) || 500, 2000);
306
+
307
+ if (!targetPath || targetPath.trim() === "") {
308
+ res.status(400).json({ error: "targetPath is required" });
309
+ return;
310
+ }
311
+
312
+ let safePath: string;
313
+ try {
314
+ safePath = safeLogPath(targetPath);
315
+ } catch {
316
+ res.status(403).json({ error: "Path traversal denied" });
317
+ return;
318
+ }
319
+
320
+ try {
321
+ const lines = await tailLines(safePath, limit);
322
+ const entries = lines.map((line) => {
323
+ try { return JSON.parse(line); } catch { return { type: "PARSE_ERROR", message: line }; }
324
+ });
325
+ res.json({ entries });
326
+ } catch (err: unknown) {
327
+ if (err instanceof Error && 'code' in err && (err as NodeJS.ErrnoException).code === "ENOENT") {
328
+ res.json({ entries: [] });
329
+ return;
330
+ }
331
+ res.status(500).json({ error: "Failed to read log file" });
332
+ }
333
+ });
@@ -0,0 +1,25 @@
1
+ import { Router } from "express";
2
+ import type { Request, Response, Router as RouterType } from "express";
3
+ import { getSessions } from "./log.js";
4
+ import { getActiveProject } from "../../../../project-context.js";
5
+
6
+ export const statusRouter: RouterType = Router();
7
+
8
+ /**
9
+ * GET /api/status — Return active sessions.
10
+ *
11
+ * Optional query parameter `projectPath` filters sessions to those whose
12
+ * targetPath starts with the given path. When omitted, the server falls
13
+ * back to the active project tracked via the Shell Socket.io connection.
14
+ * If neither is set, all sessions are returned.
15
+ */
16
+ statusRouter.get("/", (req: Request, res: Response) => {
17
+ let sessions = getSessions();
18
+
19
+ const projectPath = (req.query.projectPath as string | undefined) || getActiveProject()?.path || null;
20
+ if (projectPath) {
21
+ sessions = sessions.filter((s) => s.targetPath?.startsWith(projectPath));
22
+ }
23
+
24
+ res.json({ sessions });
25
+ });
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Server-side console sniffer for Devglide apps.
3
+ * Writes log entries directly to disk and forwards them to the Log service.
4
+ *
5
+ * Usage:
6
+ * import { initServerSniffer } from '@devglide/log/server-sniffer';
7
+ * initServerSniffer({ service: 'kanban', targetPath: '/abs/path/server.log' });
8
+ */
9
+
10
+ import { writeFileSync, appendFileSync, mkdirSync } from "fs";
11
+ import { dirname } from "path";
12
+
13
+
14
+ interface ServerSnifferOptions {
15
+ /** Service name (e.g. 'kanban', 'voice') */
16
+ service: string;
17
+ /** Absolute path to the JSONL log file */
18
+ targetPath: string;
19
+ /** Log service port (default: 7001) */
20
+ logPort?: number;
21
+ }
22
+
23
+ let _initialized = false;
24
+
25
+ export function initServerSniffer(opts: ServerSnifferOptions): void {
26
+ if (_initialized) return;
27
+ _initialized = true;
28
+
29
+ const { service, targetPath, logPort = 7000 } = opts;
30
+ const baseUrl = `http://localhost:${logPort}`;
31
+ const sessionId = `${service}-server`;
32
+
33
+ const origLog = console.log;
34
+ const origWarn = console.warn;
35
+ const origError = console.error;
36
+
37
+ let seq = 0;
38
+
39
+ // Ensure target directory exists
40
+ mkdirSync(dirname(targetPath), { recursive: true });
41
+
42
+ function writeEntry(entry: Record<string, unknown>): void {
43
+ try {
44
+ appendFileSync(targetPath, JSON.stringify(entry) + "\n");
45
+ } catch {}
46
+ }
47
+
48
+ function forward(entry: Record<string, unknown>): void {
49
+ fetch(`${baseUrl}/api/log`, {
50
+ method: "POST",
51
+ headers: { "Content-Type": "application/json" },
52
+ body: JSON.stringify(entry),
53
+ signal: AbortSignal.timeout(500),
54
+ }).catch(() => {});
55
+ }
56
+
57
+ function send(type: string, args: unknown[]): void {
58
+ const message = args
59
+ .map((a) => {
60
+ if (typeof a === "string") return a;
61
+ try { return JSON.stringify(a); } catch { return String(a); }
62
+ })
63
+ .join(" ");
64
+
65
+ const entry: Record<string, unknown> = {
66
+ type,
67
+ session: sessionId,
68
+ seq: seq++,
69
+ ts: new Date().toISOString(),
70
+ message,
71
+ persistent: true,
72
+ };
73
+
74
+ writeEntry(entry);
75
+ forward({ ...entry, targetPath });
76
+ }
77
+
78
+ // Truncate log file and write SESSION_START directly
79
+ const sessionStart: Record<string, unknown> = {
80
+ type: "SESSION_START",
81
+ session: sessionId,
82
+ seq: seq++,
83
+ ts: new Date().toISOString(),
84
+ url: `server://${service}`,
85
+ ua: `node/${process.version}`,
86
+ persistent: true,
87
+ };
88
+ writeFileSync(targetPath, JSON.stringify(sessionStart) + "\n");
89
+ forward({ ...sessionStart, targetPath });
90
+
91
+ console.log = function (...args: unknown[]) {
92
+ origLog.apply(console, args);
93
+ send("SERVER_LOG", args);
94
+ };
95
+
96
+ console.warn = function (...args: unknown[]) {
97
+ origWarn.apply(console, args);
98
+ send("SERVER_WARN", args);
99
+ };
100
+
101
+ console.error = function (...args: unknown[]) {
102
+ origError.apply(console, args);
103
+ send("SERVER_ERROR", args);
104
+ };
105
+
106
+ process.on("uncaughtException", (err) => {
107
+ send("SERVER_ERROR", [`Uncaught Exception: ${err.message}\n${err.stack || ""}`]);
108
+ // Allow the write to complete, then exit
109
+ setTimeout(() => process.exit(1), 1000).unref();
110
+ });
111
+
112
+ process.on("unhandledRejection", (reason) => {
113
+ const msg = reason instanceof Error
114
+ ? `Unhandled Rejection: ${reason.message}\n${reason.stack || ""}`
115
+ : `Unhandled Rejection: ${String(reason)}`;
116
+ send("SERVER_ERROR", [msg]);
117
+ });
118
+ }
@@ -0,0 +1,39 @@
1
+ // ── File discovery patterns for log file tailing ────────────────────────────
2
+
3
+ /** Glob patterns to include when scanning for log files */
4
+ export const INCLUDE_PATTERNS = [
5
+ "*.log",
6
+ "logs/*.log",
7
+ "log/*.log",
8
+ "tmp/*.log",
9
+ "storage/logs/*.log",
10
+ "var/log/*.log",
11
+ ];
12
+
13
+ /** Directories to ignore during file discovery */
14
+ export const IGNORE_DIRS = [
15
+ "node_modules",
16
+ "dist",
17
+ "build",
18
+ ".git",
19
+ ".next",
20
+ ".turbo",
21
+ "vendor",
22
+ "venv",
23
+ "__pycache__",
24
+ ];
25
+
26
+ /** Chokidar-compatible ignored patterns */
27
+ export const IGNORED_GLOBS = [
28
+ ...IGNORE_DIRS.map((d) => `**/${d}/**`),
29
+ "**/*.filetail.log", // our own output files
30
+ ];
31
+
32
+ /** Maximum directory depth for file scanning */
33
+ export const MAX_DEPTH = 3;
34
+
35
+ /** Maximum number of files to watch simultaneously */
36
+ export const MAX_WATCHED_FILES = 20;
37
+
38
+ /** Skip files larger than this (bytes) */
39
+ export const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB