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.
- package/LICENSE +21 -0
- package/README.md +338 -0
- package/bin/claude-md-template.js +94 -0
- package/bin/devglide.js +387 -0
- package/package.json +85 -0
- package/pnpm-workspace.yaml +3 -0
- package/src/apps/coder/.turbo/turbo-lint.log +5 -0
- package/src/apps/coder/package.json +16 -0
- package/src/apps/coder/public/favicon.svg +7 -0
- package/src/apps/coder/public/page.css +275 -0
- package/src/apps/coder/public/page.js +528 -0
- package/src/apps/coder/server.js +3 -0
- package/src/apps/documentation/public/page.css +597 -0
- package/src/apps/documentation/public/page.js +609 -0
- package/src/apps/kanban/.turbo/turbo-lint.log +97 -0
- package/src/apps/kanban/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/kanban/package.json +32 -0
- package/src/apps/kanban/public/favicon.svg +7 -0
- package/src/apps/kanban/public/page.css +1010 -0
- package/src/apps/kanban/public/page.js +1730 -0
- package/src/apps/kanban/public/vendor/marked.min.js +6 -0
- package/src/apps/kanban/public/vendor/sortable.min.js +2 -0
- package/src/apps/kanban/src/db.ts +319 -0
- package/src/apps/kanban/src/index.ts +14 -0
- package/src/apps/kanban/src/mcp-helpers.test.ts +88 -0
- package/src/apps/kanban/src/mcp-helpers.ts +60 -0
- package/src/apps/kanban/src/mcp.ts +59 -0
- package/src/apps/kanban/src/routes/attachments.ts +161 -0
- package/src/apps/kanban/src/routes/features.ts +233 -0
- package/src/apps/kanban/src/routes/issues.ts +373 -0
- package/src/apps/kanban/src/tools/feature-tools.ts +164 -0
- package/src/apps/kanban/src/tools/item-tools.ts +307 -0
- package/src/apps/kanban/src/tools/versioned-entry-tools.ts +72 -0
- package/src/apps/kanban/tsconfig.check.json +9 -0
- package/src/apps/kanban/tsconfig.json +9 -0
- package/src/apps/keymap/.turbo/turbo-lint.log +5 -0
- package/src/apps/keymap/package.json +16 -0
- package/src/apps/keymap/public/page.css +275 -0
- package/src/apps/keymap/public/page.js +294 -0
- package/src/apps/keymap/server.js +25 -0
- package/src/apps/log/.turbo/turbo-build.log +5 -0
- package/src/apps/log/.turbo/turbo-lint.log +45 -0
- package/src/apps/log/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/log/node_modules/.bin/tsc +21 -0
- package/src/apps/log/node_modules/.bin/tsserver +21 -0
- package/src/apps/log/node_modules/.bin/tsx +21 -0
- package/src/apps/log/package.json +36 -0
- package/src/apps/log/public/console-sniffer.js +221 -0
- package/src/apps/log/public/favicon.svg +7 -0
- package/src/apps/log/public/page.css +322 -0
- package/src/apps/log/public/page.js +463 -0
- package/src/apps/log/src/index.ts +9 -0
- package/src/apps/log/src/mcp.ts +122 -0
- package/src/apps/log/src/routes/log.ts +333 -0
- package/src/apps/log/src/routes/status.ts +25 -0
- package/src/apps/log/src/server-sniffer.ts +118 -0
- package/src/apps/log/src/services/file-patterns.ts +39 -0
- package/src/apps/log/src/services/file-tailer.ts +228 -0
- package/src/apps/log/src/services/line-parser.ts +94 -0
- package/src/apps/log/src/services/log-writer.ts +39 -0
- package/src/apps/log/tsconfig.json +8 -0
- package/src/apps/prompts/.turbo/turbo-build.log +5 -0
- package/src/apps/prompts/.turbo/turbo-lint.log +24 -0
- package/src/apps/prompts/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/prompts/mcp.ts +175 -0
- package/src/apps/prompts/node_modules/.bin/tsc +21 -0
- package/src/apps/prompts/node_modules/.bin/tsserver +21 -0
- package/src/apps/prompts/node_modules/.bin/tsx +21 -0
- package/src/apps/prompts/package.json +25 -0
- package/src/apps/prompts/public/page.css +315 -0
- package/src/apps/prompts/public/page.js +541 -0
- package/src/apps/prompts/services/prompt-store.ts +212 -0
- package/src/apps/prompts/src/index.ts +9 -0
- package/src/apps/prompts/tsconfig.json +8 -0
- package/src/apps/prompts/types.ts +27 -0
- package/src/apps/shell/.turbo/turbo-build.log +5 -0
- package/src/apps/shell/.turbo/turbo-lint.log +34 -0
- package/src/apps/shell/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/shell/package.json +35 -0
- package/src/apps/shell/public/favicon.svg +7 -0
- package/src/apps/shell/public/page.css +407 -0
- package/src/apps/shell/public/page.js +1577 -0
- package/src/apps/shell/src/index.ts +150 -0
- package/src/apps/shell/src/mcp.ts +398 -0
- package/src/apps/shell/src/shell-types.ts +41 -0
- package/src/apps/shell/tsconfig.json +8 -0
- package/src/apps/test/.turbo/turbo-build.log +5 -0
- package/src/apps/test/.turbo/turbo-lint.log +27 -0
- package/src/apps/test/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/test/node_modules/.bin/tsc +21 -0
- package/src/apps/test/node_modules/.bin/tsserver +21 -0
- package/src/apps/test/node_modules/.bin/tsx +21 -0
- package/src/apps/test/node_modules/.bin/uuid +21 -0
- package/src/apps/test/package.json +35 -0
- package/src/apps/test/public/favicon.svg +7 -0
- package/src/apps/test/public/page.css +499 -0
- package/src/apps/test/public/page.js +417 -0
- package/src/apps/test/public/scenario-runner.js +450 -0
- package/src/apps/test/src/index.ts +9 -0
- package/src/apps/test/src/mcp.ts +192 -0
- package/src/apps/test/src/routes/trigger.ts +285 -0
- package/src/apps/test/src/services/scenario-broadcaster.ts +60 -0
- package/src/apps/test/src/services/scenario-manager.ts +361 -0
- package/src/apps/test/src/services/scenario-store.ts +145 -0
- package/src/apps/test/tsconfig.json +8 -0
- package/src/apps/vocabulary/.turbo/turbo-build.log +5 -0
- package/src/apps/vocabulary/.turbo/turbo-lint.log +25 -0
- package/src/apps/vocabulary/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/vocabulary/mcp.ts +173 -0
- package/src/apps/vocabulary/node_modules/.bin/tsc +21 -0
- package/src/apps/vocabulary/node_modules/.bin/tsserver +21 -0
- package/src/apps/vocabulary/node_modules/.bin/tsx +21 -0
- package/src/apps/vocabulary/package.json +25 -0
- package/src/apps/vocabulary/public/page.css +247 -0
- package/src/apps/vocabulary/public/page.js +444 -0
- package/src/apps/vocabulary/services/vocabulary-store.ts +179 -0
- package/src/apps/vocabulary/src/index.ts +10 -0
- package/src/apps/vocabulary/tsconfig.json +8 -0
- package/src/apps/vocabulary/types.ts +22 -0
- package/src/apps/voice/.turbo/turbo-build.log +5 -0
- package/src/apps/voice/.turbo/turbo-lint.log +43 -0
- package/src/apps/voice/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/voice/node_modules/.bin/openai +21 -0
- package/src/apps/voice/node_modules/.bin/tsc +21 -0
- package/src/apps/voice/node_modules/.bin/tsserver +21 -0
- package/src/apps/voice/node_modules/.bin/tsx +21 -0
- package/src/apps/voice/package.json +35 -0
- package/src/apps/voice/public/favicon.svg +7 -0
- package/src/apps/voice/public/page.css +388 -0
- package/src/apps/voice/public/page.js +718 -0
- package/src/apps/voice/src/index.ts +10 -0
- package/src/apps/voice/src/mcp.ts +70 -0
- package/src/apps/voice/src/providers/index.ts +85 -0
- package/src/apps/voice/src/providers/openai-compatible.ts +94 -0
- package/src/apps/voice/src/providers/types.ts +27 -0
- package/src/apps/voice/src/routes/config.ts +118 -0
- package/src/apps/voice/src/routes/transcribe.ts +90 -0
- package/src/apps/voice/src/services/config-store.ts +129 -0
- package/src/apps/voice/src/services/stats.ts +108 -0
- package/src/apps/voice/src/transcribe.ts +11 -0
- package/src/apps/voice/src/utils/mime.ts +16 -0
- package/src/apps/voice/tsconfig.json +8 -0
- package/src/apps/workflow/.turbo/turbo-build.log +5 -0
- package/src/apps/workflow/.turbo/turbo-lint.log +96 -0
- package/src/apps/workflow/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/workflow/engine/executors/decision-executor.ts +87 -0
- package/src/apps/workflow/engine/executors/file-executor.ts +90 -0
- package/src/apps/workflow/engine/executors/git-executor.ts +137 -0
- package/src/apps/workflow/engine/executors/http-executor.ts +65 -0
- package/src/apps/workflow/engine/executors/index.ts +28 -0
- package/src/apps/workflow/engine/executors/kanban-executor.ts +154 -0
- package/src/apps/workflow/engine/executors/llm-executor.ts +46 -0
- package/src/apps/workflow/engine/executors/log-executor.ts +62 -0
- package/src/apps/workflow/engine/executors/loop-executor.ts +14 -0
- package/src/apps/workflow/engine/executors/shell-executor.ts +107 -0
- package/src/apps/workflow/engine/executors/sub-workflow-executor.ts +61 -0
- package/src/apps/workflow/engine/executors/test-executor.ts +73 -0
- package/src/apps/workflow/engine/executors/trigger-executor.ts +39 -0
- package/src/apps/workflow/engine/expression-evaluator.ts +117 -0
- package/src/apps/workflow/engine/graph-runner.ts +438 -0
- package/src/apps/workflow/engine/node-executor.ts +104 -0
- package/src/apps/workflow/engine/node-registry.ts +15 -0
- package/src/apps/workflow/engine/variable-resolver.ts +109 -0
- package/src/apps/workflow/mcp.ts +223 -0
- package/src/apps/workflow/node_modules/.bin/tsc +21 -0
- package/src/apps/workflow/node_modules/.bin/tsserver +21 -0
- package/src/apps/workflow/node_modules/.bin/tsx +21 -0
- package/src/apps/workflow/package.json +25 -0
- package/src/apps/workflow/public/editor/canvas.js +366 -0
- package/src/apps/workflow/public/editor/drag-manager.js +326 -0
- package/src/apps/workflow/public/editor/edge-renderer.js +235 -0
- package/src/apps/workflow/public/editor/history-manager.js +147 -0
- package/src/apps/workflow/public/editor/layout-engine.js +159 -0
- package/src/apps/workflow/public/editor/node-renderer.js +199 -0
- package/src/apps/workflow/public/editor/selection-manager.js +193 -0
- package/src/apps/workflow/public/favicon.svg +7 -0
- package/src/apps/workflow/public/models/node-types.js +300 -0
- package/src/apps/workflow/public/models/workflow-model.js +257 -0
- package/src/apps/workflow/public/page.css +406 -0
- package/src/apps/workflow/public/page.js +658 -0
- package/src/apps/workflow/public/panels/inspector.js +360 -0
- package/src/apps/workflow/public/panels/palette.js +106 -0
- package/src/apps/workflow/public/panels/run-view.js +275 -0
- package/src/apps/workflow/public/panels/toolbar.js +232 -0
- package/src/apps/workflow/public/panels/workflow-list.js +237 -0
- package/src/apps/workflow/public/state/store.js +47 -0
- package/src/apps/workflow/services/custom-node-loader.ts +48 -0
- package/src/apps/workflow/services/legacy-converter.ts +72 -0
- package/src/apps/workflow/services/run-manager.ts +190 -0
- package/src/apps/workflow/services/workflow-store.ts +424 -0
- package/src/apps/workflow/services/workflow-validator.test.ts +103 -0
- package/src/apps/workflow/services/workflow-validator.ts +98 -0
- package/src/apps/workflow/src/index.ts +10 -0
- package/src/apps/workflow/templates/ci-pipeline.json +18 -0
- package/src/apps/workflow/templates/code-review.json +22 -0
- package/src/apps/workflow/templates/kanban-testing.json +24 -0
- package/src/apps/workflow/tsconfig.json +8 -0
- package/src/apps/workflow/types.ts +268 -0
- package/src/packages/auth-middleware.ts +14 -0
- package/src/packages/design-tokens/.turbo/turbo-build.log +10 -0
- package/src/packages/design-tokens/STYLEGUIDE.md +414 -0
- package/src/packages/design-tokens/build.js +413 -0
- package/src/packages/design-tokens/demo/index.html +1367 -0
- package/src/packages/design-tokens/demo/proposition-a.html +717 -0
- package/src/packages/design-tokens/demo/proposition-b.html +1239 -0
- package/src/packages/design-tokens/demo/proposition-c.html +1049 -0
- package/src/packages/design-tokens/dist/tailwind-preset.js +115 -0
- package/src/packages/design-tokens/dist/tokens.css +345 -0
- package/src/packages/design-tokens/dist/tokens.d.ts +229 -0
- package/src/packages/design-tokens/dist/tokens.js +386 -0
- package/src/packages/design-tokens/package.json +25 -0
- package/src/packages/design-tokens/tokens.json +228 -0
- package/src/packages/devtools-middleware.ts +22 -0
- package/src/packages/eslint-config/index.js +63 -0
- package/src/packages/eslint-config/node_modules/.bin/eslint +21 -0
- package/src/packages/eslint-config/package.json +18 -0
- package/src/packages/json-file-store.ts +232 -0
- package/src/packages/mcp-utils/.turbo/turbo-build.log +5 -0
- package/src/packages/mcp-utils/dist/index.d.ts +33 -0
- package/src/packages/mcp-utils/dist/index.d.ts.map +1 -0
- package/src/packages/mcp-utils/dist/index.js +126 -0
- package/src/packages/mcp-utils/dist/index.js.map +1 -0
- package/src/packages/mcp-utils/node_modules/.bin/tsc +21 -0
- package/src/packages/mcp-utils/node_modules/.bin/tsserver +21 -0
- package/src/packages/mcp-utils/package.json +32 -0
- package/src/packages/mcp-utils/src/index.ts +171 -0
- package/src/packages/mcp-utils/tsconfig.json +9 -0
- package/src/packages/paths.ts +18 -0
- package/src/packages/project-context/index.js +55 -0
- package/src/packages/project-context/package.json +13 -0
- package/src/packages/project-store.ts +127 -0
- package/src/packages/server-sniffer.ts +132 -0
- package/src/packages/shared-assets/favicon.svg +7 -0
- package/src/packages/shared-assets/keymap-registry.js +512 -0
- package/src/packages/shared-assets/logo.svg +6 -0
- package/src/packages/shared-assets/package.json +11 -0
- package/src/packages/shared-assets/ui-utils.js +48 -0
- package/src/packages/shared-assets/voice-widget.d.ts +37 -0
- package/src/packages/shared-assets/voice-widget.js +695 -0
- package/src/packages/shared-types/.turbo/turbo-build.log +5 -0
- package/src/packages/shared-types/dist/index.d.ts +39 -0
- package/src/packages/shared-types/dist/index.d.ts.map +1 -0
- package/src/packages/shared-types/node_modules/.bin/tsc +21 -0
- package/src/packages/shared-types/node_modules/.bin/tsserver +21 -0
- package/src/packages/shared-types/package.json +25 -0
- package/src/packages/shared-types/src/index.ts +41 -0
- package/src/packages/shared-types/tsconfig.json +11 -0
- package/src/packages/tsconfig/base.json +15 -0
- package/src/packages/tsconfig/next.json +14 -0
- package/src/packages/tsconfig/node.json +11 -0
- package/src/packages/tsconfig/package.json +10 -0
- 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><script src="http://localhost:7000/devtools.js?target=/path/to/app"></script></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">← 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><script src="http://localhost:7000/devtools.js?target=/path/to/app"></script></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 ? '⚠ ' + 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
|
+
}
|