claude-code-kanban 3.2.4 → 3.4.0
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/cli.js +458 -0
- package/lib/parsers.js +69 -33
- package/package.json +2 -1
- package/plugin/plugins/claude-code-kanban/.claude-plugin/plugin.json +1 -1
- package/plugin/plugins/claude-code-kanban/skills/kanban/SKILL.md +52 -0
- package/public/app.js +419 -13
- package/public/index.html +29 -1
- package/public/style.css +93 -0
- package/server.js +151 -43
package/public/style.css
CHANGED
|
@@ -739,6 +739,10 @@ body::before {
|
|
|
739
739
|
background-position: top;
|
|
740
740
|
font-size: 10px;
|
|
741
741
|
color: var(--text-muted);
|
|
742
|
+
display: flex;
|
|
743
|
+
align-items: center;
|
|
744
|
+
justify-content: space-between;
|
|
745
|
+
gap: 8px;
|
|
742
746
|
}
|
|
743
747
|
|
|
744
748
|
.sidebar-footer a {
|
|
@@ -747,6 +751,11 @@ body::before {
|
|
|
747
751
|
transition: color 0.15s;
|
|
748
752
|
}
|
|
749
753
|
|
|
754
|
+
.sidebar-footer .footer-limits strong {
|
|
755
|
+
color: var(--text-secondary);
|
|
756
|
+
font-weight: 600;
|
|
757
|
+
}
|
|
758
|
+
|
|
750
759
|
.sidebar-footer a:hover {
|
|
751
760
|
color: var(--text-secondary);
|
|
752
761
|
}
|
|
@@ -2489,6 +2498,28 @@ body::before {
|
|
|
2489
2498
|
cursor: default;
|
|
2490
2499
|
}
|
|
2491
2500
|
|
|
2501
|
+
.linked-docs-badge,
|
|
2502
|
+
.bookmarks-badge {
|
|
2503
|
+
display: inline-flex;
|
|
2504
|
+
align-items: center;
|
|
2505
|
+
gap: 2px;
|
|
2506
|
+
font-size: 10px;
|
|
2507
|
+
padding: 2px 6px;
|
|
2508
|
+
background: var(--bg-elevated);
|
|
2509
|
+
border: 1px solid var(--border);
|
|
2510
|
+
border-radius: 10px;
|
|
2511
|
+
color: var(--text-secondary);
|
|
2512
|
+
cursor: pointer;
|
|
2513
|
+
flex-shrink: 0;
|
|
2514
|
+
line-height: 1;
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
.linked-docs-badge:hover,
|
|
2518
|
+
.bookmarks-badge:hover {
|
|
2519
|
+
border-color: var(--accent);
|
|
2520
|
+
color: var(--text-primary);
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2492
2523
|
/* #endregion */
|
|
2493
2524
|
|
|
2494
2525
|
/* #region PERMISSION_PENDING */
|
|
@@ -2689,6 +2720,68 @@ body.light .msg-assistant .msg-text {
|
|
|
2689
2720
|
max-height: 92vh;
|
|
2690
2721
|
}
|
|
2691
2722
|
|
|
2723
|
+
.modal.preview-modal-dialog {
|
|
2724
|
+
width: 90vw;
|
|
2725
|
+
max-width: 1200px;
|
|
2726
|
+
max-height: 90vh;
|
|
2727
|
+
display: flex;
|
|
2728
|
+
flex-direction: column;
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
.modal.preview-modal-dialog.fullscreen {
|
|
2732
|
+
width: 90vw;
|
|
2733
|
+
max-width: 90vw;
|
|
2734
|
+
height: 90vh;
|
|
2735
|
+
max-height: 90vh;
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
.preview-fm {
|
|
2739
|
+
margin: 0 0 14px;
|
|
2740
|
+
border: 1px solid var(--border);
|
|
2741
|
+
border-radius: 6px;
|
|
2742
|
+
background: var(--bg-elevated, rgba(127, 127, 127, 0.06));
|
|
2743
|
+
font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
|
|
2744
|
+
font-size: 11px;
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
.preview-fm > summary {
|
|
2748
|
+
cursor: pointer;
|
|
2749
|
+
padding: 6px 10px;
|
|
2750
|
+
color: var(--text-muted);
|
|
2751
|
+
text-transform: uppercase;
|
|
2752
|
+
letter-spacing: 0.06em;
|
|
2753
|
+
font-size: 10px;
|
|
2754
|
+
user-select: none;
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
.preview-fm[open] > summary {
|
|
2758
|
+
border-bottom: 1px solid var(--border);
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
.preview-fm .fm-grid {
|
|
2762
|
+
padding: 8px 10px;
|
|
2763
|
+
display: grid;
|
|
2764
|
+
gap: 4px 12px;
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
.preview-fm .fm-row {
|
|
2768
|
+
display: grid;
|
|
2769
|
+
grid-template-columns: minmax(80px, 140px) 1fr;
|
|
2770
|
+
gap: 12px;
|
|
2771
|
+
align-items: baseline;
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
.preview-fm .fm-k {
|
|
2775
|
+
color: var(--accent, #7aa2f7);
|
|
2776
|
+
font-weight: 600;
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
.preview-fm .fm-v {
|
|
2780
|
+
color: var(--text, inherit);
|
|
2781
|
+
word-break: break-word;
|
|
2782
|
+
white-space: pre-wrap;
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2692
2785
|
.modal-sm {
|
|
2693
2786
|
max-width: 440px;
|
|
2694
2787
|
}
|
package/server.js
CHANGED
|
@@ -15,19 +15,22 @@ const {
|
|
|
15
15
|
readMessagesPage: _readMessagesPageUncached,
|
|
16
16
|
readSessionInfoFromJsonl,
|
|
17
17
|
buildAgentProgressMap,
|
|
18
|
+
buildSessionDigest,
|
|
18
19
|
readCompactSummaries,
|
|
19
20
|
findTerminatedTeammates,
|
|
20
|
-
extractPromptFromTranscript
|
|
21
|
+
extractPromptFromTranscript,
|
|
22
|
+
extractModelFromTranscript
|
|
21
23
|
} = require('./lib/parsers');
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const { runInstall, runUninstall } = require('./install');
|
|
27
|
-
(process.argv.includes('--install') ? runInstall() : runUninstall())
|
|
25
|
+
if (process.argv.includes("--install") || process.argv.includes("--uninstall")) {
|
|
26
|
+
const { runInstall, runUninstall } = require("./install");
|
|
27
|
+
(process.argv.includes("--install") ? runInstall() : runUninstall())
|
|
28
28
|
.then(() => process.exit(0))
|
|
29
29
|
.catch(e => { console.error(e.message); process.exit(1); });
|
|
30
|
+
return;
|
|
30
31
|
}
|
|
32
|
+
if (require("./cli").runCli(process.argv)) return;
|
|
33
|
+
|
|
31
34
|
|
|
32
35
|
const app = express();
|
|
33
36
|
const PORT = process.env.PORT || 3456;
|
|
@@ -79,7 +82,10 @@ const WAITING_RESOLVE_GRACE_MS = 15000;
|
|
|
79
82
|
|
|
80
83
|
function persistAgent(dir, agent) {
|
|
81
84
|
const file = path.join(dir, agent.agentId + '.json');
|
|
82
|
-
|
|
85
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
86
|
+
fs.writeFile(tmp, JSON.stringify(agent), 'utf8')
|
|
87
|
+
.then(() => fs.rename(tmp, file))
|
|
88
|
+
.catch(() => { fs.unlink(tmp).catch(() => {}); });
|
|
83
89
|
}
|
|
84
90
|
|
|
85
91
|
function checkWaitingForUser(agentDir, logMtime) {
|
|
@@ -209,8 +215,6 @@ app.use(express.static(path.join(__dirname, 'public')));
|
|
|
209
215
|
const messageCache = new Map();
|
|
210
216
|
const MESSAGE_CACHE_TTL = 5000;
|
|
211
217
|
const MAX_CACHE_ENTRIES = 200;
|
|
212
|
-
const progressMapCache = new Map();
|
|
213
|
-
const terminatedCache = new Map();
|
|
214
218
|
const compactSummaryCache = new Map();
|
|
215
219
|
const taskCountsCache = new Map();
|
|
216
220
|
const contextStatusCache = new Map();
|
|
@@ -324,12 +328,17 @@ function cachedByMtime(cache, cacheKey, filePath, loadFn, fallback) {
|
|
|
324
328
|
} catch (_) { return fallback; }
|
|
325
329
|
}
|
|
326
330
|
|
|
331
|
+
const sessionDigestCache = new Map();
|
|
332
|
+
function getSessionDigest(jsonlPath) {
|
|
333
|
+
return cachedByMtime(sessionDigestCache, jsonlPath, jsonlPath, () => buildSessionDigest(jsonlPath), { progressMap: {}, terminated: new Map() });
|
|
334
|
+
}
|
|
335
|
+
|
|
327
336
|
function getProgressMap(jsonlPath) {
|
|
328
|
-
return
|
|
337
|
+
return getSessionDigest(jsonlPath).progressMap;
|
|
329
338
|
}
|
|
330
339
|
|
|
331
340
|
function getTerminatedTeammates(jsonlPath) {
|
|
332
|
-
return
|
|
341
|
+
return getSessionDigest(jsonlPath).terminated;
|
|
333
342
|
}
|
|
334
343
|
|
|
335
344
|
function readRecentMessages(jsonlPath, limit = 10) {
|
|
@@ -791,6 +800,12 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
791
800
|
let sessions = Array.from(sessionsMap.values());
|
|
792
801
|
sessions.sort((a, b) => new Date(b.modifiedAt) - new Date(a.modifiedAt));
|
|
793
802
|
|
|
803
|
+
// Apply project filter before limit so the limit is per-project
|
|
804
|
+
const projectFilter = req.query.project;
|
|
805
|
+
if (projectFilter) {
|
|
806
|
+
sessions = sessions.filter(s => s.project === projectFilter);
|
|
807
|
+
}
|
|
808
|
+
|
|
794
809
|
// Apply limit if specified, but always include pinned sessions
|
|
795
810
|
if (limit !== null && limit > 0) {
|
|
796
811
|
const top = sessions.slice(0, limit);
|
|
@@ -958,10 +973,14 @@ app.post('/api/open-folder', (req, res) => {
|
|
|
958
973
|
}
|
|
959
974
|
});
|
|
960
975
|
|
|
961
|
-
// API: Open
|
|
976
|
+
// API: Open file in editor — either an existing path ({ file }) or content as a temp file ({ content, title })
|
|
962
977
|
app.post('/api/open-in-editor', (req, res) => {
|
|
963
978
|
try {
|
|
964
|
-
const { content, title } = req.body;
|
|
979
|
+
const { content, title, file } = req.body;
|
|
980
|
+
if (file) {
|
|
981
|
+
openInEditor(file);
|
|
982
|
+
return res.json({ success: true, path: file });
|
|
983
|
+
}
|
|
965
984
|
if (!content) return res.status(400).json({ error: 'No content provided' });
|
|
966
985
|
|
|
967
986
|
const safeName = (title || 'message').replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 50);
|
|
@@ -1049,14 +1068,11 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
|
|
|
1049
1068
|
} catch (_) {}
|
|
1050
1069
|
}
|
|
1051
1070
|
|
|
1052
|
-
|
|
1053
|
-
agent.prompt = prompt;
|
|
1054
|
-
persistAgent(agentDir, agent);
|
|
1055
|
-
}
|
|
1071
|
+
const dirty = new Set();
|
|
1056
1072
|
|
|
1057
|
-
const agentsNeedingPrompt = agents.filter(a => !a.prompt);
|
|
1058
|
-
const agentsNeedingName = agents.filter(a => !a.agentName);
|
|
1059
|
-
const agentsNeedingDesc = agents.filter(a => !a.description);
|
|
1073
|
+
const agentsNeedingPrompt = agents.filter(a => !a.prompt && !a.promptUnavailable);
|
|
1074
|
+
const agentsNeedingName = agents.filter(a => !a.agentName && !a.agentNameUnavailable);
|
|
1075
|
+
const agentsNeedingDesc = agents.filter(a => !a.description && !a.descriptionUnavailable);
|
|
1060
1076
|
if ((agentsNeedingPrompt.length || agentsNeedingName.length || agentsNeedingDesc.length) && meta.jsonlPath) {
|
|
1061
1077
|
let byAgentId = {};
|
|
1062
1078
|
let nameByAgentId = {};
|
|
@@ -1072,37 +1088,34 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
|
|
|
1072
1088
|
for (const agent of agentsNeedingPrompt) {
|
|
1073
1089
|
const prompt = byAgentId[agent.agentId]
|
|
1074
1090
|
|| (() => { try { return extractPromptFromTranscript(subagentJsonlPath(meta, agent.agentId)); } catch (_) { return null; } })();
|
|
1075
|
-
if (prompt)
|
|
1091
|
+
if (prompt) agent.prompt = prompt;
|
|
1092
|
+
else agent.promptUnavailable = true;
|
|
1093
|
+
dirty.add(agent);
|
|
1076
1094
|
}
|
|
1077
1095
|
for (const agent of agentsNeedingName) {
|
|
1078
1096
|
if (nameByAgentId[agent.agentId]) agent.agentName = nameByAgentId[agent.agentId];
|
|
1097
|
+
else agent.agentNameUnavailable = true;
|
|
1098
|
+
dirty.add(agent);
|
|
1079
1099
|
}
|
|
1080
1100
|
for (const agent of agentsNeedingDesc) {
|
|
1081
1101
|
if (descByAgentId[agent.agentId]) agent.description = descByAgentId[agent.agentId];
|
|
1102
|
+
else agent.descriptionUnavailable = true;
|
|
1103
|
+
dirty.add(agent);
|
|
1082
1104
|
}
|
|
1083
1105
|
}
|
|
1084
1106
|
|
|
1085
|
-
const agentsNeedingModel = agents.filter(a => !a.model);
|
|
1107
|
+
const agentsNeedingModel = agents.filter(a => !a.model && !a.modelUnavailable);
|
|
1086
1108
|
if (agentsNeedingModel.length && meta.jsonlPath) {
|
|
1087
1109
|
for (const agent of agentsNeedingModel) {
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
try {
|
|
1094
|
-
const obj = JSON.parse(line);
|
|
1095
|
-
const model = obj.model || (obj.message && obj.message.model);
|
|
1096
|
-
if (model) {
|
|
1097
|
-
agent.model = model;
|
|
1098
|
-
persistAgent(agentDir, agent);
|
|
1099
|
-
break;
|
|
1100
|
-
}
|
|
1101
|
-
} catch (_) {}
|
|
1102
|
-
}
|
|
1103
|
-
} catch (_) {}
|
|
1110
|
+
let model = null;
|
|
1111
|
+
try { model = extractModelFromTranscript(subagentJsonlPath(meta, agent.agentId)); } catch (_) {}
|
|
1112
|
+
if (model) agent.model = model;
|
|
1113
|
+
else agent.modelUnavailable = true;
|
|
1114
|
+
dirty.add(agent);
|
|
1104
1115
|
}
|
|
1105
1116
|
}
|
|
1117
|
+
|
|
1118
|
+
for (const agent of dirty) persistAgent(agentDir, agent);
|
|
1106
1119
|
const teamColors = {};
|
|
1107
1120
|
if (teamConfig?.members) {
|
|
1108
1121
|
for (const m of teamConfig.members) {
|
|
@@ -1303,6 +1316,7 @@ app.get('/api/tasks/all', async (req, res) => {
|
|
|
1303
1316
|
}
|
|
1304
1317
|
|
|
1305
1318
|
const metadata = loadSessionMetadata();
|
|
1319
|
+
const { listToSessions } = loadAllTaskMaps();
|
|
1306
1320
|
const sessionDirs = readdirSync(TASKS_DIR, { withFileTypes: true })
|
|
1307
1321
|
.filter(d => d.isDirectory());
|
|
1308
1322
|
|
|
@@ -1313,6 +1327,19 @@ app.get('/api/tasks/all', async (req, res) => {
|
|
|
1313
1327
|
const taskFiles = readdirSync(sessionPath).filter(f => f.endsWith('.json'));
|
|
1314
1328
|
const meta = metadata[sessionDir.name] || {};
|
|
1315
1329
|
|
|
1330
|
+
// For custom task list directories (non-UUID dirs), resolve project from the
|
|
1331
|
+
// mapped sessions since those dirs don't have their own metadata entry.
|
|
1332
|
+
let project = meta.project || null;
|
|
1333
|
+
if (!project) {
|
|
1334
|
+
const mappedSessions = listToSessions[sessionDir.name];
|
|
1335
|
+
if (mappedSessions) {
|
|
1336
|
+
for (const [sid, info] of Object.entries(mappedSessions)) {
|
|
1337
|
+
project = metadata[sid]?.project || info.project || null;
|
|
1338
|
+
if (project) break;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1316
1343
|
for (const file of taskFiles) {
|
|
1317
1344
|
try {
|
|
1318
1345
|
const task = JSON.parse(readFileSync(path.join(sessionPath, file), 'utf8'));
|
|
@@ -1320,7 +1347,7 @@ app.get('/api/tasks/all', async (req, res) => {
|
|
|
1320
1347
|
...task,
|
|
1321
1348
|
sessionId: sessionDir.name,
|
|
1322
1349
|
sessionName: getSessionDisplayName(sessionDir.name, meta),
|
|
1323
|
-
project
|
|
1350
|
+
project
|
|
1324
1351
|
});
|
|
1325
1352
|
} catch (e) {
|
|
1326
1353
|
// Skip invalid files
|
|
@@ -1431,6 +1458,91 @@ app.delete('/api/tasks/:sessionId/:taskId', async (req, res) => {
|
|
|
1431
1458
|
}
|
|
1432
1459
|
});
|
|
1433
1460
|
|
|
1461
|
+
// API: Markdown preview — read file and broadcast to clients
|
|
1462
|
+
async function readMarkdownFile(absPath) {
|
|
1463
|
+
const ext = path.extname(absPath).toLowerCase();
|
|
1464
|
+
if (ext !== '.md' && ext !== '.markdown') {
|
|
1465
|
+
const err = new Error('Only .md/.markdown files are allowed');
|
|
1466
|
+
err.status = 400;
|
|
1467
|
+
throw err;
|
|
1468
|
+
}
|
|
1469
|
+
try {
|
|
1470
|
+
return await fs.readFile(absPath, 'utf8');
|
|
1471
|
+
} catch (e) {
|
|
1472
|
+
if (e.code === 'ENOENT') { const err = new Error('File not found'); err.status = 404; throw err; }
|
|
1473
|
+
if (e.code === 'EISDIR') { const err = new Error('Not a file'); err.status = 400; throw err; }
|
|
1474
|
+
throw e;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
function resolvePreviewPath(filePath) {
|
|
1479
|
+
if (!filePath || typeof filePath !== 'string') return null;
|
|
1480
|
+
return path.isAbsolute(filePath) ? filePath : path.resolve(filePath);
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
app.post('/api/preview', async (req, res) => {
|
|
1484
|
+
try {
|
|
1485
|
+
const { path: filePath, sessionId } = req.body || {};
|
|
1486
|
+
const abs = resolvePreviewPath(filePath);
|
|
1487
|
+
if (!abs) return res.status(400).json({ error: 'path is required' });
|
|
1488
|
+
const content = await readMarkdownFile(abs);
|
|
1489
|
+
broadcast({ type: 'preview:open', path: abs, content, sessionId: sessionId || null });
|
|
1490
|
+
res.json({ success: true });
|
|
1491
|
+
} catch (error) {
|
|
1492
|
+
console.error('Error in /api/preview:', error);
|
|
1493
|
+
res.status(error.status || 500).json({ error: error.message || 'Preview failed' });
|
|
1494
|
+
}
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1497
|
+
app.get('/api/session/resolve', (req, res) => {
|
|
1498
|
+
try {
|
|
1499
|
+
const idArg = (req.query.id || '').toString();
|
|
1500
|
+
if (!idArg) return res.status(400).json({ error: 'id is required' });
|
|
1501
|
+
const metadata = loadSessionMetadata();
|
|
1502
|
+
const ids = Object.keys(metadata);
|
|
1503
|
+
if (Object.hasOwn(metadata, idArg)) {
|
|
1504
|
+
const m = metadata[idArg];
|
|
1505
|
+
return res.json({ id: idArg, customTitle: m?.customTitle || null });
|
|
1506
|
+
}
|
|
1507
|
+
const matches = ids.filter(id => id.startsWith(idArg));
|
|
1508
|
+
if (matches.length === 0) return res.status(404).json({ matches: [] });
|
|
1509
|
+
if (matches.length > 1) {
|
|
1510
|
+
return res.status(409).json({
|
|
1511
|
+
matches: matches.slice(0, 50).map(id => ({ id, customTitle: metadata[id]?.customTitle || null }))
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
const id = matches[0];
|
|
1515
|
+
res.json({ id, customTitle: metadata[id]?.customTitle || null });
|
|
1516
|
+
} catch (error) {
|
|
1517
|
+
console.error('Error in /api/session/resolve:', error);
|
|
1518
|
+
res.status(500).json({ error: error.message || 'Failed' });
|
|
1519
|
+
}
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1522
|
+
app.post('/api/session/open', async (req, res) => {
|
|
1523
|
+
try {
|
|
1524
|
+
const { id } = req.body || {};
|
|
1525
|
+
if (!id || typeof id !== 'string') return res.status(400).json({ error: 'id is required' });
|
|
1526
|
+
broadcast({ type: 'session:open', id });
|
|
1527
|
+
res.json({ success: true, id });
|
|
1528
|
+
} catch (error) {
|
|
1529
|
+
console.error('Error in /api/session/open:', error);
|
|
1530
|
+
res.status(500).json({ error: error.message || 'Failed' });
|
|
1531
|
+
}
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1534
|
+
app.get('/api/preview', async (req, res) => {
|
|
1535
|
+
try {
|
|
1536
|
+
const abs = resolvePreviewPath(req.query.path);
|
|
1537
|
+
if (!abs) return res.status(400).json({ error: 'path is required' });
|
|
1538
|
+
const content = await readMarkdownFile(abs);
|
|
1539
|
+
res.json({ path: abs, content });
|
|
1540
|
+
} catch (error) {
|
|
1541
|
+
console.error('Error in GET /api/preview:', error);
|
|
1542
|
+
res.status(error.status || 500).json({ error: error.message || 'Preview failed' });
|
|
1543
|
+
}
|
|
1544
|
+
});
|
|
1545
|
+
|
|
1434
1546
|
// SSE endpoint for live updates
|
|
1435
1547
|
app.get('/api/events', (req, res) => {
|
|
1436
1548
|
res.setHeader('Content-Type', 'text/event-stream');
|
|
@@ -1468,9 +1580,6 @@ app.use('/api', (req, res) => {
|
|
|
1468
1580
|
res.status(404).json({ error: 'Not found' });
|
|
1469
1581
|
});
|
|
1470
1582
|
|
|
1471
|
-
// File watchers and server startup (skip for --install/--uninstall)
|
|
1472
|
-
if (!isSetupCommand) {
|
|
1473
|
-
|
|
1474
1583
|
// Watch for file changes (chokidar handles non-existent paths)
|
|
1475
1584
|
const watcher = chokidar.watch(TASKS_DIR, {
|
|
1476
1585
|
persistent: true,
|
|
@@ -1719,4 +1828,3 @@ const server = app.listen(PORT, () => {
|
|
|
1719
1828
|
}
|
|
1720
1829
|
});
|
|
1721
1830
|
|
|
1722
|
-
} // end if (!isSetupCommand)
|