@programinglive/dev-workflow-mcp-server 1.1.7 → 1.1.16

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/web/server.js ADDED
@@ -0,0 +1,90 @@
1
+ import { createServer } from "http";
2
+ import { readFile, access } from "fs/promises";
3
+ import { fileURLToPath } from "url";
4
+ import { dirname, join } from "path";
5
+ import getPort from "get-port";
6
+ import { getHistoryForUser, getSummaryForUser, getHistorySummary } from "../db/index.js";
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+ const projectRoot = join(__dirname, "..");
11
+
12
+ const allowedDocs = {
13
+ "README.md": join(projectRoot, "README.md"),
14
+ "web-dashboard.md": join(projectRoot, "docs", "web-dashboard.md"),
15
+ };
16
+
17
+ async function start() {
18
+ const PORT = process.env.DEV_WORKFLOW_WEB_PORT || await getPort({ port: 3111 });
19
+
20
+ const server = createServer(async (req, res) => {
21
+ try {
22
+ const url = new URL(req.url, `http://${req.headers.host}`);
23
+ const userId = url.searchParams.get("user") || "default";
24
+
25
+ if (req.method === "GET" && url.pathname === "/") {
26
+ const html = await readFile(join(__dirname, "index.html"), "utf-8");
27
+ res.writeHead(200, { "Content-Type": "text/html" });
28
+ res.end(html);
29
+ } else if (req.method === "GET" && url.pathname === "/api/summary") {
30
+ const summary = getSummaryForUser(userId);
31
+ res.writeHead(200, { "Content-Type": "application/json" });
32
+ res.end(JSON.stringify({ summary }));
33
+ } else if (req.method === "GET" && url.pathname === "/api/history") {
34
+ const page = Number(url.searchParams.get("page") || "1");
35
+ const pageSize = Number(url.searchParams.get("pageSize") || "20");
36
+ const startDate = url.searchParams.get("startDate") || undefined;
37
+ const endDate = url.searchParams.get("endDate") || undefined;
38
+ const history = getHistoryForUser(userId, { page, pageSize, startDate, endDate });
39
+ res.writeHead(200, { "Content-Type": "application/json" });
40
+ res.end(JSON.stringify(history));
41
+ } else if (req.method === "GET" && url.pathname === "/api/history-summary") {
42
+ const startDate = url.searchParams.get("startDate") || undefined;
43
+ const endDate = url.searchParams.get("endDate") || undefined;
44
+ const frequency = url.searchParams.get("frequency") || "daily";
45
+ const summary = getHistorySummary(userId, { startDate, endDate, frequency });
46
+ res.writeHead(200, { "Content-Type": "application/json" });
47
+ res.end(JSON.stringify({ summary }));
48
+ } else if (req.method === "GET" && url.pathname === "/api/docs") {
49
+ const rawDocKey = url.searchParams.get("doc");
50
+ if (!rawDocKey) {
51
+ res.writeHead(200, { "Content-Type": "application/json" });
52
+ res.end(JSON.stringify({ docs: Object.keys(allowedDocs) }));
53
+ return;
54
+ }
55
+
56
+ const docKey = rawDocKey.trim();
57
+ const resolvedKey = Object.keys(allowedDocs).find((key) => key.toLowerCase() === docKey.toLowerCase());
58
+ if (!resolvedKey) {
59
+ res.writeHead(404, { "Content-Type": "application/json" });
60
+ res.end(JSON.stringify({ error: "Document not found", requested: docKey, available: Object.keys(allowedDocs) }));
61
+ return;
62
+ }
63
+
64
+ const docPath = allowedDocs[resolvedKey];
65
+
66
+ try {
67
+ await access(docPath);
68
+ const content = await readFile(docPath, "utf-8");
69
+ res.writeHead(200, { "Content-Type": "application/json" });
70
+ res.end(JSON.stringify({ doc: resolvedKey, content }));
71
+ } catch (error) {
72
+ res.writeHead(500, { "Content-Type": "application/json" });
73
+ res.end(JSON.stringify({ error: error.message }));
74
+ }
75
+ } else {
76
+ res.writeHead(404, { "Content-Type": "text/plain" });
77
+ res.end("Not found");
78
+ }
79
+ } catch (err) {
80
+ res.writeHead(500, { "Content-Type": "text/plain" });
81
+ res.end(err.message);
82
+ }
83
+ });
84
+
85
+ server.listen(PORT, () => {
86
+ console.log(`🌐 Dev Workflow Dashboard running at http://localhost:${PORT}`);
87
+ });
88
+ }
89
+
90
+ start();
package/workflow-state.js CHANGED
@@ -1,10 +1,249 @@
1
1
  import fs from "fs/promises";
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "fs";
2
3
  import path from "path";
3
4
  import { fileURLToPath } from "url";
4
5
 
5
6
  const __filename = fileURLToPath(import.meta.url);
6
7
  const __dirname = path.dirname(__filename);
7
- const STATE_FILENAME = ".workflow-state.json";
8
+ const STATE_DIR = ".state";
9
+ const STATE_FILENAME = "workflow-state.json";
10
+ const LEGACY_STATE_FILENAME = ".workflow-state.json";
11
+ const PROJECT_SUMMARY_FILENAME = "project-summary.json";
12
+ const USER_ID_FILENAME = "user-id";
13
+
14
+ let cachedUserId = null;
15
+ let cachedUserStateDir = null;
16
+
17
+ function generateUserId() {
18
+ const timestamp = Date.now().toString(16);
19
+ const randomSuffix = Math.random().toString(16).slice(2, 10);
20
+ return `user-${timestamp}-${randomSuffix}`;
21
+ }
22
+
23
+ function resolveUserId(stateDir) {
24
+ const envUserId = (process.env.DEV_WORKFLOW_USER_ID || "").trim();
25
+ if (envUserId) {
26
+ cachedUserId = envUserId;
27
+ cachedUserStateDir = stateDir;
28
+ return envUserId;
29
+ }
30
+
31
+ if (cachedUserId && cachedUserStateDir === stateDir) {
32
+ return cachedUserId;
33
+ }
34
+
35
+ const userIdFile = path.join(stateDir, USER_ID_FILENAME);
36
+
37
+ try {
38
+ if (existsSync(userIdFile)) {
39
+ const stored = readFileSync(userIdFile, "utf-8").trim();
40
+ if (stored) {
41
+ cachedUserId = stored;
42
+ cachedUserStateDir = stateDir;
43
+ return stored;
44
+ }
45
+ }
46
+ } catch {
47
+ // Ignore read errors; we'll generate a new ID below.
48
+ }
49
+
50
+ const usersDir = path.join(stateDir, "users");
51
+
52
+ try {
53
+ const entries = readdirSync(usersDir, { withFileTypes: true });
54
+ const candidateDirs = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
55
+ if (candidateDirs.length === 1) {
56
+ const legacyId = candidateDirs[0];
57
+ try {
58
+ mkdirSync(stateDir, { recursive: true });
59
+ writeFileSync(userIdFile, legacyId, "utf-8");
60
+ } catch {
61
+ // Persisting the legacy ID is best-effort.
62
+ }
63
+ cachedUserId = legacyId;
64
+ cachedUserStateDir = stateDir;
65
+ return legacyId;
66
+ }
67
+ } catch (error) {
68
+ if (error?.code !== "ENOENT") {
69
+ // Unexpected error accessing users directory – fall through to generate new ID.
70
+ }
71
+ }
72
+
73
+ const legacyDefaultPath = path.join(usersDir, "default", STATE_FILENAME);
74
+ if (existsSync(legacyDefaultPath)) {
75
+ try {
76
+ mkdirSync(stateDir, { recursive: true });
77
+ writeFileSync(userIdFile, "default", "utf-8");
78
+ } catch {
79
+ // Best-effort persistence; continue with in-memory fallback.
80
+ }
81
+ cachedUserId = "default";
82
+ cachedUserStateDir = stateDir;
83
+ return "default";
84
+ }
85
+
86
+ const generatedId = generateUserId();
87
+ try {
88
+ mkdirSync(stateDir, { recursive: true });
89
+ writeFileSync(userIdFile, generatedId, "utf-8");
90
+ } catch {
91
+ // Ignore persistence errors; continue with in-memory ID.
92
+ }
93
+
94
+ cachedUserId = generatedId;
95
+ cachedUserStateDir = stateDir;
96
+ return generatedId;
97
+ }
98
+
99
+ function hasProjectMarkers(candidate) {
100
+ if (!candidate) {
101
+ return false;
102
+ }
103
+
104
+ const resolved = path.resolve(candidate);
105
+ return (
106
+ existsSync(path.join(resolved, "package.json")) || existsSync(path.join(resolved, ".git"))
107
+ );
108
+ }
109
+
110
+ function normalizeAndStoreCandidate(list, candidate) {
111
+ if (!candidate) {
112
+ return;
113
+ }
114
+
115
+ const resolved = path.resolve(candidate);
116
+ if (!list.includes(resolved)) {
117
+ list.push(resolved);
118
+ }
119
+ }
120
+
121
+ function addAncestorCandidates(list, start) {
122
+ if (!start) {
123
+ return;
124
+ }
125
+
126
+ let current = path.resolve(start);
127
+ while (!isRootPath(current)) {
128
+ const parent = path.dirname(current);
129
+ if (parent === current) {
130
+ break;
131
+ }
132
+ if (!isRootPath(parent)) {
133
+ normalizeAndStoreCandidate(list, parent);
134
+ }
135
+ current = parent;
136
+ }
137
+ }
138
+
139
+ function selectProjectRoot(candidates, moduleRoot) {
140
+ const cwdResolved = path.resolve(process.cwd());
141
+ const prioritized = [];
142
+
143
+ for (const candidate of candidates) {
144
+ if (!candidate) {
145
+ continue;
146
+ }
147
+
148
+ const resolved = path.resolve(candidate);
149
+ if (prioritized.some((entry) => entry.resolved === resolved)) {
150
+ continue;
151
+ }
152
+
153
+ prioritized.push({
154
+ resolved,
155
+ hasMarkers: hasProjectMarkers(resolved),
156
+ isRoot: isRootPath(resolved),
157
+ isCwdAncestor: cwdResolved === resolved || cwdResolved.startsWith(`${resolved}${path.sep}`),
158
+ });
159
+ }
160
+
161
+ const ancestorWithMarkers = prioritized.find((entry) => entry.isCwdAncestor && entry.hasMarkers);
162
+ if (ancestorWithMarkers) {
163
+ return ancestorWithMarkers.resolved;
164
+ }
165
+
166
+ const ancestorWithoutMarkers = prioritized
167
+ .filter((entry) => entry.isCwdAncestor && !entry.hasMarkers && !entry.isRoot)
168
+ .sort((a, b) => b.resolved.length - a.resolved.length);
169
+ if (ancestorWithoutMarkers.length > 0) {
170
+ return ancestorWithoutMarkers[0].resolved;
171
+ }
172
+
173
+ if (moduleRoot) {
174
+ const moduleResolved = path.resolve(moduleRoot);
175
+ const moduleEntry = prioritized.find((entry) => entry.resolved === moduleResolved && !entry.isRoot);
176
+ if (moduleEntry) {
177
+ return moduleEntry.resolved;
178
+ }
179
+ }
180
+
181
+ const withMarkers = prioritized.find((entry) => entry.hasMarkers);
182
+ if (withMarkers) {
183
+ return withMarkers.resolved;
184
+ }
185
+
186
+ const nonRoot = prioritized.find((entry) => !entry.isRoot);
187
+ if (nonRoot) {
188
+ return nonRoot.resolved;
189
+ }
190
+
191
+ return prioritized.length > 0 ? prioritized[0].resolved : null;
192
+ }
193
+
194
+ function getUserScopedDir(baseDir) {
195
+ const userId = resolveUserId(baseDir);
196
+ return path.join(baseDir, "users", userId);
197
+ }
198
+
199
+ function getCurrentUserId() {
200
+ const envUserId = (process.env.DEV_WORKFLOW_USER_ID || "").trim();
201
+ if (envUserId) {
202
+ return envUserId;
203
+ }
204
+
205
+ if (cachedUserId) {
206
+ return cachedUserId;
207
+ }
208
+
209
+ if (cachedUserStateDir) {
210
+ return resolveUserId(cachedUserStateDir);
211
+ }
212
+
213
+ return "default";
214
+ }
215
+
216
+ async function migrateLegacyStateFile(targetPath) {
217
+ const stateDir = path.dirname(targetPath);
218
+ if (path.basename(stateDir) !== STATE_DIR) {
219
+ return;
220
+ }
221
+
222
+ const projectRoot = path.dirname(stateDir);
223
+ const legacyPath = path.join(projectRoot, LEGACY_STATE_FILENAME);
224
+
225
+ if (legacyPath === targetPath) {
226
+ return;
227
+ }
228
+
229
+ try {
230
+ await fs.access(legacyPath);
231
+ } catch {
232
+ return;
233
+ }
234
+
235
+ await fs.mkdir(stateDir, { recursive: true });
236
+
237
+ try {
238
+ await fs.access(targetPath);
239
+ await fs.unlink(legacyPath);
240
+ return;
241
+ } catch {
242
+ // New state file doesn't exist yet; fall through to migrate.
243
+ }
244
+
245
+ await fs.rename(legacyPath, targetPath);
246
+ }
8
247
 
9
248
  function isRootPath(candidate) {
10
249
  if (!candidate) {
@@ -15,30 +254,193 @@ function isRootPath(candidate) {
15
254
  return path.parse(resolved).root === resolved;
16
255
  }
17
256
 
18
- function resolveDefaultStateFile() {
19
- if (process.env.DEV_WORKFLOW_STATE_FILE) {
20
- return path.resolve(process.env.DEV_WORKFLOW_STATE_FILE);
257
+ function findProjectRoot(startDir) {
258
+ if (!startDir) {
259
+ return null;
21
260
  }
22
261
 
23
- const cwd = process.cwd();
24
- if (!cwd.includes(`node_modules${path.sep}`) && !isRootPath(cwd)) {
25
- return path.join(cwd, STATE_FILENAME);
262
+ let current = path.resolve(startDir);
263
+ const { root } = path.parse(current);
264
+
265
+ while (true) {
266
+ const hasPackageJson = existsSync(path.join(current, "package.json"));
267
+ const hasGitDir = existsSync(path.join(current, ".git"));
268
+ if (hasPackageJson || hasGitDir) {
269
+ return current;
270
+ }
271
+
272
+ if (current === root) {
273
+ return null;
274
+ }
275
+
276
+ const parent = path.dirname(current);
277
+ if (parent === current) {
278
+ return null;
279
+ }
280
+
281
+ current = parent;
26
282
  }
283
+ }
284
+
285
+ function getCompatibilityPaths(primaryPath) {
286
+ const resolvedPrimary = path.resolve(primaryPath);
287
+ const candidates = new Set();
288
+ const moduleDir = __dirname;
27
289
 
28
- if (process.env.INIT_CWD && !isRootPath(process.env.INIT_CWD)) {
29
- return path.join(process.env.INIT_CWD, STATE_FILENAME);
290
+ const directCandidate = path.resolve(moduleDir, STATE_DIR, STATE_FILENAME);
291
+ if (directCandidate !== resolvedPrimary) {
292
+ candidates.add(directCandidate);
30
293
  }
31
294
 
32
- const [beforeNodeModules] = cwd.split(`node_modules${path.sep}`);
33
- if (beforeNodeModules && !isRootPath(beforeNodeModules)) {
34
- return path.join(beforeNodeModules, STATE_FILENAME);
295
+ const possibleDistDirs = [
296
+ path.join(moduleDir, "dist"),
297
+ path.join(path.dirname(moduleDir), "dist"),
298
+ ];
299
+
300
+ for (const distDir of possibleDistDirs) {
301
+ const candidate = path.resolve(distDir, STATE_DIR, STATE_FILENAME);
302
+ if (candidate !== resolvedPrimary) {
303
+ candidates.add(candidate);
304
+ }
35
305
  }
36
306
 
37
- return path.join(__dirname, STATE_FILENAME);
307
+ return Array.from(candidates);
308
+ }
309
+
310
+ async function pathExists(filePath) {
311
+ try {
312
+ await fs.access(filePath);
313
+ return true;
314
+ } catch {
315
+ return false;
316
+ }
317
+ }
318
+
319
+ async function ensureCompatibilityLinks(primaryPath) {
320
+ const resolvedPrimary = path.resolve(primaryPath);
321
+
322
+ if (!(await pathExists(resolvedPrimary))) {
323
+ return;
324
+ }
325
+
326
+ const compatibilityPaths = getCompatibilityPaths(resolvedPrimary);
327
+
328
+ for (const compatPath of compatibilityPaths) {
329
+ try {
330
+ await fs.mkdir(path.dirname(compatPath), { recursive: true });
331
+ } catch {
332
+ // Ignore directory creation errors; we'll surface them on actual writes.
333
+ }
334
+
335
+ try {
336
+ const compatStat = await fs.lstat(compatPath);
337
+ if (compatStat.isSymbolicLink()) {
338
+ const target = await fs.readlink(compatPath);
339
+ if (path.resolve(path.dirname(compatPath), target) === resolvedPrimary) {
340
+ continue;
341
+ }
342
+ await fs.unlink(compatPath);
343
+ } else if (compatStat.isFile()) {
344
+ const [primaryStat, existingStat] = await Promise.all([
345
+ fs.stat(resolvedPrimary),
346
+ fs.stat(compatPath),
347
+ ]);
348
+ if (primaryStat.dev === existingStat.dev && primaryStat.ino === existingStat.ino) {
349
+ continue;
350
+ }
351
+ await fs.unlink(compatPath);
352
+ } else {
353
+ await fs.unlink(compatPath);
354
+ }
355
+ } catch (error) {
356
+ if (error.code !== "ENOENT") {
357
+ // Unexpected error accessing legacy path; skip mirroring to avoid breaking primary file.
358
+ continue;
359
+ }
360
+ }
361
+
362
+ try {
363
+ await fs.link(resolvedPrimary, compatPath);
364
+ continue;
365
+ } catch (error) {
366
+ if (!["EEXIST", "EXDEV", "EPERM"].includes(error.code)) {
367
+ continue;
368
+ }
369
+ if (error.code === "EEXIST") {
370
+ continue;
371
+ }
372
+ }
373
+
374
+ try {
375
+ await fs.symlink(resolvedPrimary, compatPath);
376
+ continue;
377
+ } catch (error) {
378
+ if (error.code === "EEXIST") {
379
+ continue;
380
+ }
381
+ if (!["EPERM", "ENOTSUP", "EEXIST"].includes(error.code)) {
382
+ // If symlink fails for another reason, skip to avoid interfering with primary file.
383
+ continue;
384
+ }
385
+ }
386
+
387
+ try {
388
+ await fs.copyFile(resolvedPrimary, compatPath);
389
+ } catch {
390
+ // Final fallback failed; ignore to avoid disrupting primary save.
391
+ }
392
+ }
393
+ }
394
+
395
+ export function resolveStateFile() {
396
+ const customPath = process.env.DEV_WORKFLOW_STATE_FILE;
397
+ if (customPath) {
398
+ return path.resolve(customPath);
399
+ }
400
+
401
+ const initCwd = process.env.INIT_CWD || process.cwd();
402
+ const resolvedInitCwd = path.resolve(initCwd);
403
+ const moduleRoot = path.resolve(__dirname, "..");
404
+ const cwdResolved = path.resolve(process.cwd());
405
+
406
+ const candidateRoots = [];
407
+
408
+ const initProjectRoot = findProjectRoot(resolvedInitCwd);
409
+ const cwdProjectRoot = findProjectRoot(cwdResolved);
410
+ const moduleProjectRoot = findProjectRoot(moduleRoot);
411
+
412
+ normalizeAndStoreCandidate(candidateRoots, initProjectRoot);
413
+ normalizeAndStoreCandidate(candidateRoots, cwdProjectRoot);
414
+ if (!isRootPath(resolvedInitCwd)) {
415
+ normalizeAndStoreCandidate(candidateRoots, resolvedInitCwd);
416
+ addAncestorCandidates(candidateRoots, resolvedInitCwd);
417
+ }
418
+ if (!isRootPath(cwdResolved)) {
419
+ normalizeAndStoreCandidate(candidateRoots, cwdResolved);
420
+ addAncestorCandidates(candidateRoots, cwdResolved);
421
+ }
422
+
423
+ normalizeAndStoreCandidate(candidateRoots, moduleProjectRoot);
424
+ if (!isRootPath(moduleRoot)) {
425
+ normalizeAndStoreCandidate(candidateRoots, moduleRoot);
426
+ }
427
+ if (!isRootPath(__dirname)) {
428
+ normalizeAndStoreCandidate(candidateRoots, __dirname);
429
+ }
430
+
431
+ let projectRoot = selectProjectRoot(candidateRoots, moduleRoot);
432
+
433
+ if (!projectRoot) {
434
+ projectRoot = path.resolve(__dirname);
435
+ }
436
+
437
+ const stateDir = path.join(projectRoot, STATE_DIR);
438
+ const userDir = getUserScopedDir(stateDir);
439
+ return path.join(userDir, STATE_FILENAME);
38
440
  }
39
441
 
40
442
  export class WorkflowState {
41
- constructor(stateFilePath = resolveDefaultStateFile()) {
443
+ constructor(stateFilePath = resolveStateFile()) {
42
444
  this.stateFile = stateFilePath;
43
445
  this.state = {
44
446
  currentPhase: "idle",
@@ -58,10 +460,12 @@ export class WorkflowState {
58
460
  lastCommitMessage: "",
59
461
  lastPushBranch: "",
60
462
  history: [],
463
+ taskType: "",
61
464
  };
62
465
  }
63
466
 
64
467
  async load() {
468
+ await migrateLegacyStateFile(this.stateFile);
65
469
  try {
66
470
  const data = await fs.readFile(this.stateFile, "utf-8");
67
471
  const parsed = JSON.parse(data);
@@ -69,6 +473,8 @@ export class WorkflowState {
69
473
  ...this.state,
70
474
  ...parsed,
71
475
  };
476
+ // Ensure compatibility mirrors exist whenever the primary file is present
477
+ await ensureCompatibilityLinks(this.stateFile);
72
478
  if (typeof this.state.testsCreated !== "boolean") {
73
479
  this.state.testsCreated = false;
74
480
  }
@@ -100,12 +506,93 @@ export class WorkflowState {
100
506
  this.state.lastPushBranch = "";
101
507
  }
102
508
  } catch (error) {
103
- // File doesn't exist yet, use default state
509
+ if (error?.code !== "ENOENT") {
510
+ throw error;
511
+ }
512
+ // Missing file is fine; bootstrap fresh artifacts for downstream consumers.
513
+ await this.ensurePrimaryFile();
104
514
  }
105
515
  }
106
516
 
517
+ async ensurePrimaryFile() {
518
+ let created = false;
519
+ try {
520
+ await fs.access(this.stateFile);
521
+ } catch {
522
+ await fs.mkdir(path.dirname(this.stateFile), { recursive: true });
523
+ await fs.writeFile(this.stateFile, JSON.stringify(this.state, null, 2));
524
+ created = true;
525
+ }
526
+
527
+ // Always ensure a fresh project summary so dependent tooling can read it immediately.
528
+ await this.updateProjectSummary();
529
+ await ensureCompatibilityLinks(this.stateFile);
530
+
531
+ return created;
532
+ }
533
+
107
534
  async save() {
108
535
  await fs.writeFile(this.stateFile, JSON.stringify(this.state, null, 2));
536
+ await ensureCompatibilityLinks(this.stateFile);
537
+ await this.updateProjectSummary();
538
+ await this.syncToDatabase();
539
+ }
540
+
541
+ async updateProjectSummary() {
542
+ const summaryPath = path.join(path.dirname(this.stateFile), PROJECT_SUMMARY_FILENAME);
543
+ const summary = this.generateProjectSummaryData();
544
+ await fs.mkdir(path.dirname(summaryPath), { recursive: true });
545
+ await fs.writeFile(summaryPath, JSON.stringify(summary, null, 2));
546
+ }
547
+
548
+ async syncToDatabase() {
549
+ try {
550
+ const { insertHistoryEntry, updateSummaryForUser } = await import("./db/index.js");
551
+ const userId = getCurrentUserId();
552
+ for (const entry of this.state.history || []) {
553
+ insertHistoryEntry(userId, entry);
554
+ }
555
+ updateSummaryForUser(userId);
556
+ } catch {
557
+ // SQLite not available; skip sync
558
+ }
559
+ }
560
+
561
+ generateProjectSummaryData() {
562
+ const history = this.state.history || [];
563
+ if (!history || history.length === 0) {
564
+ return {
565
+ totalTasks: 0,
566
+ taskTypes: {},
567
+ lastActive: null,
568
+ recentTasks: [],
569
+ updatedAt: new Date().toISOString(),
570
+ };
571
+ }
572
+
573
+ const taskTypes = {};
574
+ const recentTasks = history.slice(-20);
575
+
576
+ for (const entry of recentTasks) {
577
+ const type = entry.taskType || "other";
578
+ taskTypes[type] = (taskTypes[type] || 0) + 1;
579
+ }
580
+
581
+ const totalTasks = history.length;
582
+ const lastTask = history[history.length - 1];
583
+ const lastActive = lastTask ? lastTask.timestamp : null;
584
+
585
+ return {
586
+ totalTasks,
587
+ taskTypes,
588
+ lastActive,
589
+ recentTasks: history.slice(-5).reverse().map((e) => ({
590
+ description: e.taskDescription,
591
+ type: e.taskType,
592
+ timestamp: e.timestamp,
593
+ })),
594
+ updatedAt: new Date().toISOString(),
595
+ };
109
596
  }
110
597
 
111
598
  reset() {