@zeliper/zscode-mcp-server 1.0.7 → 2.0.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.
Files changed (52) hide show
  1. package/dist/state/manager.d.ts +274 -3
  2. package/dist/state/manager.d.ts.map +1 -1
  3. package/dist/state/manager.js +1211 -2
  4. package/dist/state/manager.js.map +1 -1
  5. package/dist/state/schema.d.ts +6535 -545
  6. package/dist/state/schema.d.ts.map +1 -1
  7. package/dist/state/schema.js +341 -0
  8. package/dist/state/schema.js.map +1 -1
  9. package/dist/state/types.d.ts +62 -1
  10. package/dist/state/types.d.ts.map +1 -1
  11. package/dist/state/types.js.map +1 -1
  12. package/dist/templates/index.d.ts +79 -0
  13. package/dist/templates/index.d.ts.map +1 -0
  14. package/dist/templates/index.js +472 -0
  15. package/dist/templates/index.js.map +1 -0
  16. package/dist/tools/bulk.d.ts +6 -0
  17. package/dist/tools/bulk.d.ts.map +1 -0
  18. package/dist/tools/bulk.js +273 -0
  19. package/dist/tools/bulk.js.map +1 -0
  20. package/dist/tools/context.d.ts.map +1 -1
  21. package/dist/tools/context.js +59 -0
  22. package/dist/tools/context.js.map +1 -1
  23. package/dist/tools/index.d.ts.map +1 -1
  24. package/dist/tools/index.js +15 -0
  25. package/dist/tools/index.js.map +1 -1
  26. package/dist/tools/navigate.d.ts +6 -0
  27. package/dist/tools/navigate.d.ts.map +1 -0
  28. package/dist/tools/navigate.js +334 -0
  29. package/dist/tools/navigate.js.map +1 -0
  30. package/dist/tools/plan.d.ts.map +1 -1
  31. package/dist/tools/plan.js +134 -2
  32. package/dist/tools/plan.js.map +1 -1
  33. package/dist/tools/rollback.d.ts +6 -0
  34. package/dist/tools/rollback.d.ts.map +1 -0
  35. package/dist/tools/rollback.js +278 -0
  36. package/dist/tools/rollback.js.map +1 -0
  37. package/dist/tools/search.d.ts +6 -0
  38. package/dist/tools/search.d.ts.map +1 -0
  39. package/dist/tools/search.js +252 -0
  40. package/dist/tools/search.js.map +1 -0
  41. package/dist/tools/staging.d.ts.map +1 -1
  42. package/dist/tools/staging.js +74 -22
  43. package/dist/tools/staging.js.map +1 -1
  44. package/dist/tools/template.d.ts +6 -0
  45. package/dist/tools/template.d.ts.map +1 -0
  46. package/dist/tools/template.js +348 -0
  47. package/dist/tools/template.js.map +1 -0
  48. package/dist/utils/format.d.ts +173 -0
  49. package/dist/utils/format.d.ts.map +1 -1
  50. package/dist/utils/format.js +336 -0
  51. package/dist/utils/format.js.map +1 -1
  52. package/package.json +1 -1
@@ -15,8 +15,6 @@ const VALID_TASK_TRANSITIONS = {
15
15
  done: [], // Terminal state - no transitions allowed
16
16
  cancelled: [], // Terminal state - no transitions allowed
17
17
  };
18
- // ============ ID Generator ============
19
- // Uses cryptographically secure random generation
20
18
  const idGenerator = {
21
19
  generatePlanId: () => `plan-${generateSecureId(8)}`,
22
20
  generateStagingId: () => `staging-${generateSecureId(4)}`,
@@ -24,6 +22,8 @@ const idGenerator = {
24
22
  generateHistoryId: () => `hist-${Date.now()}-${generateSecureId(4)}`,
25
23
  generateDecisionId: () => `dec-${Date.now()}-${generateSecureId(4)}`,
26
24
  generateMemoryId: () => `mem-${generateSecureId(8)}`,
25
+ generateTemplateId: () => `tpl-${generateSecureId(8)}`,
26
+ generateSnapshotId: () => `snap-${generateSecureId(8)}`,
27
27
  };
28
28
  // ============ StateManager Class ============
29
29
  export class StateManager {
@@ -211,6 +211,8 @@ export class StateManager {
211
211
  plans: {},
212
212
  stagings: {},
213
213
  tasks: {},
214
+ templates: {},
215
+ snapshots: {},
214
216
  history: [],
215
217
  context: {
216
218
  lastUpdated: now,
@@ -223,6 +225,26 @@ export class StateManager {
223
225
  await this.save();
224
226
  return project;
225
227
  }
228
+ async updateProject(updates) {
229
+ const state = this.ensureInitialized();
230
+ const now = new Date().toISOString();
231
+ if (updates.name !== undefined) {
232
+ state.project.name = updates.name;
233
+ }
234
+ if (updates.description !== undefined) {
235
+ state.project.description = updates.description;
236
+ }
237
+ if (updates.goals !== undefined) {
238
+ state.project.goals = updates.goals;
239
+ }
240
+ if (updates.constraints !== undefined) {
241
+ state.project.constraints = updates.constraints;
242
+ }
243
+ state.project.updatedAt = now;
244
+ await this.addHistory("project_updated", { updates: Object.keys(updates) });
245
+ await this.save();
246
+ return state.project;
247
+ }
226
248
  // ============ Plan Operations ============
227
249
  async createPlan(title, description, stagingConfigs) {
228
250
  const state = this.ensureInitialized();
@@ -394,6 +416,7 @@ export class StateManager {
394
416
  if (notes) {
395
417
  task.notes = notes;
396
418
  }
419
+ let result = {};
397
420
  if (status === "in_progress") {
398
421
  task.startedAt = now;
399
422
  await this.addHistory("task_started", { taskId, taskTitle: task.title });
@@ -410,6 +433,14 @@ export class StateManager {
410
433
  });
411
434
  if (allTasksDone) {
412
435
  await this.completeStaging(staging.id);
436
+ result.stagingCompleted = true;
437
+ result.completedStagingId = staging.id;
438
+ // Find next staging
439
+ const allStagings = this.getStagingsByPlan(staging.planId);
440
+ const currentIndex = allStagings.findIndex(s => s.id === staging.id);
441
+ result.nextStaging = currentIndex >= 0 && currentIndex < allStagings.length - 1
442
+ ? allStagings[currentIndex + 1]
443
+ : null;
413
444
  }
414
445
  }
415
446
  }
@@ -417,6 +448,124 @@ export class StateManager {
417
448
  await this.addHistory("task_blocked", { taskId, taskTitle: task.title, notes });
418
449
  }
419
450
  await this.save();
451
+ return result;
452
+ }
453
+ /**
454
+ * Batch update multiple tasks' status at once.
455
+ * This is more efficient than calling updateTaskStatus multiple times
456
+ * and ensures atomic updates for parallel staging execution.
457
+ */
458
+ async updateTasksStatus(updates) {
459
+ const now = new Date().toISOString();
460
+ const results = [];
461
+ const affectedPlanIds = new Set();
462
+ const affectedStagingIds = new Set();
463
+ // Process all updates first (validation and state change)
464
+ for (const update of updates) {
465
+ const task = this.getTask(update.taskId);
466
+ if (!task) {
467
+ results.push({
468
+ taskId: update.taskId,
469
+ taskTitle: "unknown",
470
+ previousStatus: "pending",
471
+ newStatus: update.status,
472
+ success: false,
473
+ error: `Task not found: ${update.taskId}`,
474
+ });
475
+ continue;
476
+ }
477
+ const previousStatus = task.status;
478
+ // Validate state transition
479
+ const allowedTransitions = VALID_TASK_TRANSITIONS[task.status];
480
+ if (!allowedTransitions.includes(update.status)) {
481
+ results.push({
482
+ taskId: update.taskId,
483
+ taskTitle: task.title,
484
+ previousStatus,
485
+ newStatus: update.status,
486
+ success: false,
487
+ error: `Invalid transition: ${task.status} -> ${update.status}`,
488
+ });
489
+ continue;
490
+ }
491
+ // Apply the update
492
+ task.status = update.status;
493
+ task.updatedAt = now;
494
+ if (update.notes) {
495
+ task.notes = update.notes;
496
+ }
497
+ affectedPlanIds.add(task.planId);
498
+ affectedStagingIds.add(task.stagingId);
499
+ if (update.status === "in_progress") {
500
+ task.startedAt = now;
501
+ }
502
+ else if (update.status === "done") {
503
+ task.completedAt = now;
504
+ }
505
+ results.push({
506
+ taskId: update.taskId,
507
+ taskTitle: task.title,
508
+ previousStatus,
509
+ newStatus: update.status,
510
+ success: true,
511
+ });
512
+ }
513
+ // Invalidate cache for affected plans
514
+ for (const planId of affectedPlanIds) {
515
+ this.invalidatePlanCache(planId);
516
+ }
517
+ // Add history entries for successful updates
518
+ for (const result of results) {
519
+ if (result.success) {
520
+ if (result.newStatus === "in_progress") {
521
+ await this.addHistory("task_started", { taskId: result.taskId, taskTitle: result.taskTitle });
522
+ }
523
+ else if (result.newStatus === "done") {
524
+ await this.addHistory("task_completed", { taskId: result.taskId, taskTitle: result.taskTitle });
525
+ }
526
+ else if (result.newStatus === "blocked") {
527
+ const update = updates.find(u => u.taskId === result.taskId);
528
+ await this.addHistory("task_blocked", { taskId: result.taskId, taskTitle: result.taskTitle, notes: update?.notes });
529
+ }
530
+ }
531
+ }
532
+ // Check for staging completion
533
+ let stagingCompleted = false;
534
+ let completedStagingId;
535
+ let nextStaging = null;
536
+ let planCompleted = false;
537
+ for (const stagingId of affectedStagingIds) {
538
+ const staging = this.getStaging(stagingId);
539
+ if (staging && staging.status === "in_progress") {
540
+ const allTasksDone = staging.tasks.every(id => {
541
+ const t = this.getTask(id);
542
+ return t?.status === "done";
543
+ });
544
+ if (allTasksDone) {
545
+ await this.completeStaging(staging.id);
546
+ stagingCompleted = true;
547
+ completedStagingId = staging.id;
548
+ // Find next staging
549
+ const allStagings = this.getStagingsByPlan(staging.planId);
550
+ const currentIndex = allStagings.findIndex(s => s.id === staging.id);
551
+ if (currentIndex >= 0 && currentIndex < allStagings.length - 1) {
552
+ nextStaging = allStagings[currentIndex + 1];
553
+ }
554
+ else {
555
+ planCompleted = true;
556
+ }
557
+ break; // Only report first staging completion
558
+ }
559
+ }
560
+ }
561
+ await this.save();
562
+ return {
563
+ results,
564
+ stagingCompleted: stagingCompleted ? true : undefined,
565
+ completedStagingId,
566
+ nextStaging,
567
+ planCompleted: planCompleted ? true : undefined,
568
+ };
420
569
  }
421
570
  async saveTaskOutput(taskId, output) {
422
571
  // Validate taskId to prevent path traversal
@@ -818,6 +967,15 @@ export class StateManager {
818
967
  lines.push(`\n## Constraints`);
819
968
  project.constraints.forEach(c => lines.push(`- ${c}`));
820
969
  }
970
+ // Features (MCP Tools) - Always include
971
+ lines.push(`\n## Features`);
972
+ lines.push(`- **Plan Management**: create_plan, update_plan, sync_plan, zscode:cancel, zscode:archive, zscode:unarchive`);
973
+ lines.push(`- **Staging Management**: zscode:start, add_staging, update_staging, remove_staging, complete_staging`);
974
+ lines.push(`- **Task Management**: add_task, update_task, update_task_details, remove_task, save_task_output, get_staging_artifacts`);
975
+ lines.push(`- **Memory System**: add_memory, list_memories, update_memory, remove_memory, get_memories_for_context, list_categories`);
976
+ lines.push(`- **Context & Status**: get_full_context, zscode:status, init_project, update_project, add_decision`);
977
+ lines.push(`- **Summary**: generate_summary, get_project_summary, delete_project_summary`);
978
+ lines.push(`- **File Operations**: zscode:read, zscode:write`);
821
979
  // Current status
822
980
  lines.push(`\n## Status`);
823
981
  lines.push(`- Active Plans: ${activePlans.length}`);
@@ -1211,6 +1369,1057 @@ export class StateManager {
1211
1369
  this._planStatusCache.clear();
1212
1370
  this._statusCacheValid = false;
1213
1371
  }
1372
+ // ============ Template Operations ============
1373
+ /**
1374
+ * Create a new template
1375
+ */
1376
+ async createTemplate(config) {
1377
+ const state = this.ensureInitialized();
1378
+ const now = new Date().toISOString();
1379
+ const templateId = idGenerator.generateTemplateId();
1380
+ const template = {
1381
+ id: templateId,
1382
+ name: config.name,
1383
+ description: config.description,
1384
+ category: config.category ?? "custom",
1385
+ tags: config.tags ?? [],
1386
+ stagings: config.stagings ?? [],
1387
+ variables: (config.variables ?? []).map(v => ({
1388
+ name: v.name,
1389
+ description: v.description,
1390
+ defaultValue: v.defaultValue,
1391
+ required: v.required ?? false,
1392
+ })),
1393
+ usageCount: 0,
1394
+ isBuiltIn: false,
1395
+ createdAt: now,
1396
+ updatedAt: now,
1397
+ };
1398
+ state.templates[templateId] = template;
1399
+ await this.addHistory("template_created", { templateId, name: config.name });
1400
+ await this.save();
1401
+ return template;
1402
+ }
1403
+ /**
1404
+ * Get a template by ID
1405
+ */
1406
+ getTemplate(templateId) {
1407
+ return this.state?.templates[templateId];
1408
+ }
1409
+ /**
1410
+ * Get all templates
1411
+ */
1412
+ getAllTemplates() {
1413
+ if (!this.state)
1414
+ return [];
1415
+ return Object.values(this.state.templates);
1416
+ }
1417
+ /**
1418
+ * List templates with optional filtering
1419
+ */
1420
+ listTemplates(options) {
1421
+ let templates = this.getAllTemplates();
1422
+ if (options?.category) {
1423
+ templates = templates.filter(t => t.category === options.category);
1424
+ }
1425
+ if (options?.tags && options.tags.length > 0) {
1426
+ templates = templates.filter(t => options.tags.some(tag => t.tags.includes(tag)));
1427
+ }
1428
+ if (options?.includeBuiltIn === false) {
1429
+ templates = templates.filter(t => !t.isBuiltIn);
1430
+ }
1431
+ // Sort by usage count (descending), then by name
1432
+ return templates.sort((a, b) => {
1433
+ if (b.usageCount !== a.usageCount) {
1434
+ return b.usageCount - a.usageCount;
1435
+ }
1436
+ return a.name.localeCompare(b.name);
1437
+ });
1438
+ }
1439
+ /**
1440
+ * Update a template
1441
+ */
1442
+ async updateTemplate(templateId, updates) {
1443
+ const state = this.ensureInitialized();
1444
+ const template = state.templates[templateId];
1445
+ if (!template) {
1446
+ throw new Error(`Template not found: ${templateId}`);
1447
+ }
1448
+ if (template.isBuiltIn) {
1449
+ throw new Error(`Cannot modify built-in template: ${templateId}`);
1450
+ }
1451
+ const now = new Date().toISOString();
1452
+ if (updates.name !== undefined)
1453
+ template.name = updates.name;
1454
+ if (updates.description !== undefined)
1455
+ template.description = updates.description;
1456
+ if (updates.category !== undefined)
1457
+ template.category = updates.category;
1458
+ if (updates.tags !== undefined)
1459
+ template.tags = updates.tags;
1460
+ if (updates.stagings !== undefined)
1461
+ template.stagings = updates.stagings;
1462
+ if (updates.variables !== undefined) {
1463
+ template.variables = updates.variables.map(v => ({
1464
+ name: v.name,
1465
+ description: v.description,
1466
+ defaultValue: v.defaultValue,
1467
+ required: v.required ?? false,
1468
+ }));
1469
+ }
1470
+ template.updatedAt = now;
1471
+ await this.addHistory("template_updated", { templateId, updates: Object.keys(updates) });
1472
+ await this.save();
1473
+ return template;
1474
+ }
1475
+ /**
1476
+ * Delete a template
1477
+ */
1478
+ async deleteTemplate(templateId) {
1479
+ const state = this.ensureInitialized();
1480
+ const template = state.templates[templateId];
1481
+ if (!template) {
1482
+ throw new Error(`Template not found: ${templateId}`);
1483
+ }
1484
+ if (template.isBuiltIn) {
1485
+ throw new Error(`Cannot delete built-in template: ${templateId}`);
1486
+ }
1487
+ delete state.templates[templateId];
1488
+ await this.addHistory("template_removed", { templateId, name: template.name });
1489
+ await this.save();
1490
+ }
1491
+ /**
1492
+ * Apply a template to create a new plan
1493
+ */
1494
+ async applyTemplate(templateId, planTitle, planDescription, variables) {
1495
+ const state = this.ensureInitialized();
1496
+ const template = state.templates[templateId];
1497
+ if (!template) {
1498
+ throw new Error(`Template not found: ${templateId}`);
1499
+ }
1500
+ // Check required variables
1501
+ for (const variable of template.variables) {
1502
+ if (variable.required && !variables?.[variable.name] && !variable.defaultValue) {
1503
+ throw new Error(`Required variable '${variable.name}' not provided`);
1504
+ }
1505
+ }
1506
+ // Function to substitute variables in text
1507
+ const substituteVariables = (text) => {
1508
+ let result = text;
1509
+ for (const variable of template.variables) {
1510
+ const value = variables?.[variable.name] ?? variable.defaultValue ?? "";
1511
+ result = result.replace(new RegExp(`\\$\\{${variable.name}\\}`, "g"), value);
1512
+ }
1513
+ return result;
1514
+ };
1515
+ // Build staging configs from template
1516
+ const stagingConfigs = template.stagings.map(stagingDef => ({
1517
+ name: substituteVariables(stagingDef.name),
1518
+ description: stagingDef.description ? substituteVariables(stagingDef.description) : undefined,
1519
+ execution_type: stagingDef.execution_type,
1520
+ default_model: stagingDef.default_model,
1521
+ session_budget: stagingDef.session_budget,
1522
+ recommended_sessions: stagingDef.recommended_sessions,
1523
+ tasks: stagingDef.tasks.map(taskDef => ({
1524
+ title: substituteVariables(taskDef.title),
1525
+ description: taskDef.description ? substituteVariables(taskDef.description) : undefined,
1526
+ priority: taskDef.priority,
1527
+ execution_mode: taskDef.execution_mode,
1528
+ model: taskDef.model,
1529
+ depends_on_index: taskDef.depends_on_index,
1530
+ })),
1531
+ }));
1532
+ // Create the plan
1533
+ const plan = await this.createPlan(substituteVariables(planTitle), planDescription ? substituteVariables(planDescription) : undefined, stagingConfigs);
1534
+ // Update template usage stats
1535
+ template.usageCount++;
1536
+ template.lastUsedAt = new Date().toISOString();
1537
+ await this.addHistory("template_applied", { templateId, planId: plan.id });
1538
+ await this.save();
1539
+ return plan;
1540
+ }
1541
+ // ============ Snapshot Operations ============
1542
+ /**
1543
+ * Create a snapshot of current state
1544
+ */
1545
+ async createSnapshot(config) {
1546
+ const state = this.ensureInitialized();
1547
+ const now = new Date().toISOString();
1548
+ const snapshotId = idGenerator.generateSnapshotId();
1549
+ const type = config.type ?? "full";
1550
+ // Build snapshot data based on type
1551
+ const data = {};
1552
+ if (type === "full") {
1553
+ // Full snapshot - copy everything
1554
+ data.plans = { ...state.plans };
1555
+ data.stagings = { ...state.stagings };
1556
+ data.tasks = { ...state.tasks };
1557
+ data.templates = { ...state.templates };
1558
+ data.memories = [...state.context.memories];
1559
+ }
1560
+ else if (type === "plan" && config.planId) {
1561
+ // Plan snapshot - copy specific plan and its related data
1562
+ const plan = state.plans[config.planId];
1563
+ if (!plan) {
1564
+ throw new PlanNotFoundError(config.planId);
1565
+ }
1566
+ data.plans = { [config.planId]: plan };
1567
+ data.stagings = {};
1568
+ data.tasks = {};
1569
+ for (const stagingId of plan.stagings) {
1570
+ const staging = state.stagings[stagingId];
1571
+ if (staging) {
1572
+ data.stagings[stagingId] = staging;
1573
+ for (const taskId of staging.tasks) {
1574
+ const task = state.tasks[taskId];
1575
+ if (task) {
1576
+ data.tasks[taskId] = task;
1577
+ }
1578
+ }
1579
+ }
1580
+ }
1581
+ }
1582
+ else if (type === "staging" && config.stagingId) {
1583
+ // Staging snapshot - copy specific staging and its tasks
1584
+ const staging = state.stagings[config.stagingId];
1585
+ if (!staging) {
1586
+ throw new StagingNotFoundError(config.stagingId);
1587
+ }
1588
+ data.stagings = { [config.stagingId]: staging };
1589
+ data.tasks = {};
1590
+ for (const taskId of staging.tasks) {
1591
+ const task = state.tasks[taskId];
1592
+ if (task) {
1593
+ data.tasks[taskId] = task;
1594
+ }
1595
+ }
1596
+ }
1597
+ const snapshot = {
1598
+ id: snapshotId,
1599
+ name: config.name,
1600
+ description: config.description,
1601
+ type,
1602
+ trigger: config.trigger ?? "manual",
1603
+ planId: config.planId,
1604
+ stagingId: config.stagingId,
1605
+ data,
1606
+ stateVersion: STATE_VERSION,
1607
+ createdAt: now,
1608
+ expiresAt: config.expiresAt,
1609
+ tags: config.tags ?? [],
1610
+ };
1611
+ state.snapshots[snapshotId] = snapshot;
1612
+ await this.addHistory("snapshot_created", { snapshotId, name: config.name, type });
1613
+ await this.save();
1614
+ return snapshot;
1615
+ }
1616
+ /**
1617
+ * Get a snapshot by ID
1618
+ */
1619
+ getSnapshot(snapshotId) {
1620
+ return this.state?.snapshots[snapshotId];
1621
+ }
1622
+ /**
1623
+ * List snapshots with optional filtering
1624
+ */
1625
+ listSnapshots(options) {
1626
+ if (!this.state)
1627
+ return [];
1628
+ let snapshots = Object.values(this.state.snapshots);
1629
+ if (options?.type) {
1630
+ snapshots = snapshots.filter(s => s.type === options.type);
1631
+ }
1632
+ if (options?.planId) {
1633
+ snapshots = snapshots.filter(s => s.planId === options.planId);
1634
+ }
1635
+ if (options?.tags && options.tags.length > 0) {
1636
+ snapshots = snapshots.filter(s => options.tags.some(tag => s.tags.includes(tag)));
1637
+ }
1638
+ // Sort by creation date (newest first)
1639
+ snapshots.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
1640
+ if (options?.limit) {
1641
+ snapshots = snapshots.slice(0, options.limit);
1642
+ }
1643
+ return snapshots;
1644
+ }
1645
+ /**
1646
+ * Restore state from a snapshot
1647
+ */
1648
+ async restoreSnapshot(snapshotId, options) {
1649
+ const state = this.ensureInitialized();
1650
+ const snapshot = state.snapshots[snapshotId];
1651
+ if (!snapshot) {
1652
+ throw new Error(`Snapshot not found: ${snapshotId}`);
1653
+ }
1654
+ const opts = {
1655
+ restorePlans: options?.restorePlans ?? true,
1656
+ restoreStagings: options?.restoreStagings ?? true,
1657
+ restoreTasks: options?.restoreTasks ?? true,
1658
+ restoreTemplates: options?.restoreTemplates ?? false,
1659
+ restoreMemories: options?.restoreMemories ?? false,
1660
+ createBackup: options?.createBackup ?? true,
1661
+ };
1662
+ // Create backup before restore if requested
1663
+ let backupSnapshotId;
1664
+ if (opts.createBackup) {
1665
+ const backup = await this.createSnapshot({
1666
+ name: `Backup before restore from ${snapshot.name}`,
1667
+ type: "full",
1668
+ trigger: "auto",
1669
+ tags: ["backup", "pre-restore"],
1670
+ });
1671
+ backupSnapshotId = backup.id;
1672
+ }
1673
+ const restoredItems = {
1674
+ plans: 0,
1675
+ stagings: 0,
1676
+ tasks: 0,
1677
+ templates: 0,
1678
+ memories: 0,
1679
+ };
1680
+ // Restore plans
1681
+ if (opts.restorePlans && snapshot.data.plans) {
1682
+ for (const [planId, plan] of Object.entries(snapshot.data.plans)) {
1683
+ state.plans[planId] = plan;
1684
+ restoredItems.plans += 1;
1685
+ this.invalidatePlanCache(planId);
1686
+ }
1687
+ }
1688
+ // Restore stagings
1689
+ if (opts.restoreStagings && snapshot.data.stagings) {
1690
+ for (const [stagingId, staging] of Object.entries(snapshot.data.stagings)) {
1691
+ state.stagings[stagingId] = staging;
1692
+ restoredItems.stagings += 1;
1693
+ }
1694
+ }
1695
+ // Restore tasks
1696
+ if (opts.restoreTasks && snapshot.data.tasks) {
1697
+ for (const [taskId, task] of Object.entries(snapshot.data.tasks)) {
1698
+ state.tasks[taskId] = task;
1699
+ restoredItems.tasks += 1;
1700
+ }
1701
+ }
1702
+ // Restore templates
1703
+ if (opts.restoreTemplates && snapshot.data.templates) {
1704
+ for (const [templateId, template] of Object.entries(snapshot.data.templates)) {
1705
+ state.templates[templateId] = template;
1706
+ restoredItems.templates += 1;
1707
+ }
1708
+ }
1709
+ // Restore memories
1710
+ if (opts.restoreMemories && snapshot.data.memories) {
1711
+ state.context.memories = snapshot.data.memories;
1712
+ restoredItems.memories = snapshot.data.memories.length;
1713
+ }
1714
+ await this.addHistory("snapshot_restored", { snapshotId, restoredItems, backupSnapshotId });
1715
+ await this.save();
1716
+ return { restoredItems, backupSnapshotId };
1717
+ }
1718
+ /**
1719
+ * Delete a snapshot
1720
+ */
1721
+ async deleteSnapshot(snapshotId) {
1722
+ const state = this.ensureInitialized();
1723
+ const snapshot = state.snapshots[snapshotId];
1724
+ if (!snapshot) {
1725
+ throw new Error(`Snapshot not found: ${snapshotId}`);
1726
+ }
1727
+ delete state.snapshots[snapshotId];
1728
+ await this.addHistory("snapshot_removed", { snapshotId, name: snapshot.name });
1729
+ await this.save();
1730
+ }
1731
+ /**
1732
+ * Clean up expired snapshots
1733
+ */
1734
+ async cleanupExpiredSnapshots() {
1735
+ const state = this.ensureInitialized();
1736
+ const now = new Date();
1737
+ let deletedCount = 0;
1738
+ const snapshotIds = Object.keys(state.snapshots);
1739
+ for (const snapshotId of snapshotIds) {
1740
+ const snapshot = state.snapshots[snapshotId];
1741
+ if (snapshot?.expiresAt && new Date(snapshot.expiresAt) < now) {
1742
+ delete state.snapshots[snapshotId];
1743
+ deletedCount++;
1744
+ }
1745
+ }
1746
+ if (deletedCount > 0) {
1747
+ await this.save();
1748
+ }
1749
+ return deletedCount;
1750
+ }
1751
+ // ============ Search Operations ============
1752
+ /**
1753
+ * Search across all entities
1754
+ */
1755
+ search(query) {
1756
+ const startTime = Date.now();
1757
+ const state = this.ensureInitialized();
1758
+ const results = [];
1759
+ const entityTypes = query.entityTypes ?? ["plan", "staging", "task", "template", "memory", "decision", "snapshot"];
1760
+ // Search plans
1761
+ if (entityTypes.includes("plan")) {
1762
+ for (const plan of Object.values(state.plans)) {
1763
+ if (!query.includeArchived && plan.status === "archived")
1764
+ continue;
1765
+ const result = this.matchEntity("plan", plan.id, plan, query);
1766
+ if (result)
1767
+ results.push(result);
1768
+ }
1769
+ }
1770
+ // Search stagings
1771
+ if (entityTypes.includes("staging")) {
1772
+ for (const staging of Object.values(state.stagings)) {
1773
+ const result = this.matchEntity("staging", staging.id, staging, query);
1774
+ if (result)
1775
+ results.push(result);
1776
+ }
1777
+ }
1778
+ // Search tasks
1779
+ if (entityTypes.includes("task")) {
1780
+ for (const task of Object.values(state.tasks)) {
1781
+ const result = this.matchEntity("task", task.id, task, query);
1782
+ if (result)
1783
+ results.push(result);
1784
+ }
1785
+ }
1786
+ // Search templates
1787
+ if (entityTypes.includes("template")) {
1788
+ for (const template of Object.values(state.templates)) {
1789
+ const result = this.matchEntity("template", template.id, template, query);
1790
+ if (result)
1791
+ results.push(result);
1792
+ }
1793
+ }
1794
+ // Search memories
1795
+ if (entityTypes.includes("memory")) {
1796
+ for (const memory of state.context.memories) {
1797
+ const result = this.matchEntity("memory", memory.id, memory, query);
1798
+ if (result)
1799
+ results.push(result);
1800
+ }
1801
+ }
1802
+ // Search decisions
1803
+ if (entityTypes.includes("decision")) {
1804
+ for (const decision of state.context.decisions) {
1805
+ const result = this.matchEntity("decision", decision.id, decision, query);
1806
+ if (result)
1807
+ results.push(result);
1808
+ }
1809
+ }
1810
+ // Search snapshots
1811
+ if (entityTypes.includes("snapshot")) {
1812
+ for (const snapshot of Object.values(state.snapshots)) {
1813
+ const result = this.matchEntity("snapshot", snapshot.id, snapshot, query);
1814
+ if (result)
1815
+ results.push(result);
1816
+ }
1817
+ }
1818
+ // Sort results
1819
+ if (query.sort && query.sort.length > 0) {
1820
+ results.sort((a, b) => {
1821
+ for (const sort of query.sort) {
1822
+ const aVal = this.getNestedValue(a.data, sort.field);
1823
+ const bVal = this.getNestedValue(b.data, sort.field);
1824
+ const cmp = this.compareValues(aVal, bVal);
1825
+ if (cmp !== 0) {
1826
+ return sort.order === "desc" ? -cmp : cmp;
1827
+ }
1828
+ }
1829
+ return b.score - a.score; // Default: sort by score
1830
+ });
1831
+ }
1832
+ else {
1833
+ // Default: sort by score (descending)
1834
+ results.sort((a, b) => b.score - a.score);
1835
+ }
1836
+ // Apply pagination
1837
+ const total = results.length;
1838
+ const offset = query.offset ?? 0;
1839
+ const limit = query.limit ?? 20;
1840
+ const paginatedResults = results.slice(offset, offset + limit);
1841
+ return {
1842
+ query,
1843
+ results: paginatedResults,
1844
+ total,
1845
+ executionTimeMs: Date.now() - startTime,
1846
+ };
1847
+ }
1848
+ /**
1849
+ * Match an entity against search query
1850
+ */
1851
+ matchEntity(entityType, entityId, entity, query) {
1852
+ let score = 0;
1853
+ const matches = [];
1854
+ // Text search
1855
+ if (query.query) {
1856
+ const searchText = query.query.toLowerCase();
1857
+ const textFields = this.getTextFields(entity);
1858
+ for (const { field, value } of textFields) {
1859
+ const lowerValue = value.toLowerCase();
1860
+ if (lowerValue.includes(searchText)) {
1861
+ score += 1;
1862
+ // Create snippet
1863
+ const idx = lowerValue.indexOf(searchText);
1864
+ const start = Math.max(0, idx - 20);
1865
+ const end = Math.min(value.length, idx + searchText.length + 20);
1866
+ let snippet = value.substring(start, end);
1867
+ if (start > 0)
1868
+ snippet = "..." + snippet;
1869
+ if (end < value.length)
1870
+ snippet = snippet + "...";
1871
+ matches.push({ field, snippet });
1872
+ }
1873
+ }
1874
+ }
1875
+ // Apply filters
1876
+ for (const filter of query.filters) {
1877
+ const fieldValue = this.getNestedValue(entity, filter.field);
1878
+ if (!this.matchFilter(fieldValue, filter)) {
1879
+ return null; // Filter not matched
1880
+ }
1881
+ score += 0.5; // Bonus for matching filter
1882
+ }
1883
+ // If no query provided but filters matched, still include
1884
+ if (!query.query && query.filters.length > 0) {
1885
+ score = 1;
1886
+ }
1887
+ // Skip if no matches
1888
+ if (score === 0) {
1889
+ return null;
1890
+ }
1891
+ // Build lightweight data
1892
+ const lightweightData = {
1893
+ id: entityId,
1894
+ };
1895
+ // Add common fields
1896
+ if ("title" in entity)
1897
+ lightweightData.title = entity.title;
1898
+ if ("name" in entity)
1899
+ lightweightData.name = entity.name;
1900
+ if ("status" in entity)
1901
+ lightweightData.status = entity.status;
1902
+ if ("createdAt" in entity)
1903
+ lightweightData.createdAt = entity.createdAt;
1904
+ if ("updatedAt" in entity)
1905
+ lightweightData.updatedAt = entity.updatedAt;
1906
+ return {
1907
+ entityType,
1908
+ entityId,
1909
+ score: Math.min(score, 1), // Normalize to 0-1
1910
+ matches,
1911
+ data: lightweightData,
1912
+ };
1913
+ }
1914
+ /**
1915
+ * Get all text fields from an entity
1916
+ */
1917
+ getTextFields(entity) {
1918
+ const textFields = [];
1919
+ const textFieldNames = ["title", "name", "description", "content", "summary", "decision", "rationale"];
1920
+ for (const field of textFieldNames) {
1921
+ if (field in entity && typeof entity[field] === "string") {
1922
+ textFields.push({ field, value: entity[field] });
1923
+ }
1924
+ }
1925
+ // Also check tags
1926
+ if ("tags" in entity && Array.isArray(entity.tags)) {
1927
+ for (const tag of entity.tags) {
1928
+ textFields.push({ field: "tags", value: tag });
1929
+ }
1930
+ }
1931
+ return textFields;
1932
+ }
1933
+ /**
1934
+ * Get nested value from an object
1935
+ */
1936
+ getNestedValue(obj, path) {
1937
+ const parts = path.split(".");
1938
+ let current = obj;
1939
+ for (const part of parts) {
1940
+ if (current === null || current === undefined)
1941
+ return undefined;
1942
+ if (typeof current !== "object")
1943
+ return undefined;
1944
+ current = current[part];
1945
+ }
1946
+ return current;
1947
+ }
1948
+ /**
1949
+ * Compare two values for sorting
1950
+ */
1951
+ compareValues(a, b) {
1952
+ if (a === b)
1953
+ return 0;
1954
+ if (a === undefined || a === null)
1955
+ return 1;
1956
+ if (b === undefined || b === null)
1957
+ return -1;
1958
+ if (typeof a === "string" && typeof b === "string") {
1959
+ return a.localeCompare(b);
1960
+ }
1961
+ if (typeof a === "number" && typeof b === "number") {
1962
+ return a - b;
1963
+ }
1964
+ // For dates (ISO strings)
1965
+ if (typeof a === "string" && typeof b === "string") {
1966
+ const dateA = new Date(a);
1967
+ const dateB = new Date(b);
1968
+ if (!isNaN(dateA.getTime()) && !isNaN(dateB.getTime())) {
1969
+ return dateA.getTime() - dateB.getTime();
1970
+ }
1971
+ }
1972
+ return String(a).localeCompare(String(b));
1973
+ }
1974
+ /**
1975
+ * Match a field value against a filter
1976
+ */
1977
+ matchFilter(fieldValue, filter) {
1978
+ const { operator, value } = filter;
1979
+ switch (operator) {
1980
+ case "eq":
1981
+ return fieldValue === value;
1982
+ case "neq":
1983
+ return fieldValue !== value;
1984
+ case "contains":
1985
+ if (typeof fieldValue !== "string" || typeof value !== "string")
1986
+ return false;
1987
+ return fieldValue.toLowerCase().includes(value.toLowerCase());
1988
+ case "startsWith":
1989
+ if (typeof fieldValue !== "string" || typeof value !== "string")
1990
+ return false;
1991
+ return fieldValue.toLowerCase().startsWith(value.toLowerCase());
1992
+ case "endsWith":
1993
+ if (typeof fieldValue !== "string" || typeof value !== "string")
1994
+ return false;
1995
+ return fieldValue.toLowerCase().endsWith(value.toLowerCase());
1996
+ case "gt":
1997
+ return this.compareValues(fieldValue, value) > 0;
1998
+ case "gte":
1999
+ return this.compareValues(fieldValue, value) >= 0;
2000
+ case "lt":
2001
+ return this.compareValues(fieldValue, value) < 0;
2002
+ case "lte":
2003
+ return this.compareValues(fieldValue, value) <= 0;
2004
+ case "in":
2005
+ if (!Array.isArray(value))
2006
+ return false;
2007
+ return value.includes(fieldValue);
2008
+ case "notIn":
2009
+ if (!Array.isArray(value))
2010
+ return false;
2011
+ return !value.includes(fieldValue);
2012
+ case "exists":
2013
+ return fieldValue !== undefined && fieldValue !== null;
2014
+ case "regex":
2015
+ if (typeof fieldValue !== "string" || typeof value !== "string")
2016
+ return false;
2017
+ try {
2018
+ return new RegExp(value, "i").test(fieldValue);
2019
+ }
2020
+ catch {
2021
+ return false;
2022
+ }
2023
+ default:
2024
+ return true;
2025
+ }
2026
+ }
2027
+ // ============ Bulk Operations ============
2028
+ /**
2029
+ * Bulk update multiple tasks
2030
+ */
2031
+ async bulkUpdateTasks(updates) {
2032
+ const now = new Date().toISOString();
2033
+ const results = [];
2034
+ let successCount = 0;
2035
+ let failedCount = 0;
2036
+ for (const update of updates) {
2037
+ try {
2038
+ const task = this.getTask(update.taskId);
2039
+ if (!task) {
2040
+ results.push({ id: update.taskId, success: false, error: "Task not found" });
2041
+ failedCount++;
2042
+ continue;
2043
+ }
2044
+ // Validate status transition if status is being updated
2045
+ if (update.status) {
2046
+ const allowedTransitions = VALID_TASK_TRANSITIONS[task.status];
2047
+ if (!allowedTransitions.includes(update.status)) {
2048
+ results.push({
2049
+ id: update.taskId,
2050
+ success: false,
2051
+ error: `Invalid transition: ${task.status} -> ${update.status}`,
2052
+ });
2053
+ failedCount++;
2054
+ continue;
2055
+ }
2056
+ task.status = update.status;
2057
+ if (update.status === "in_progress")
2058
+ task.startedAt = now;
2059
+ if (update.status === "done")
2060
+ task.completedAt = now;
2061
+ }
2062
+ if (update.priority)
2063
+ task.priority = update.priority;
2064
+ if (update.notes)
2065
+ task.notes = update.notes;
2066
+ task.updatedAt = now;
2067
+ this.invalidatePlanCache(task.planId);
2068
+ results.push({ id: update.taskId, success: true, data: task });
2069
+ successCount++;
2070
+ }
2071
+ catch (error) {
2072
+ results.push({
2073
+ id: update.taskId,
2074
+ success: false,
2075
+ error: error instanceof Error ? error.message : "Unknown error",
2076
+ });
2077
+ failedCount++;
2078
+ }
2079
+ }
2080
+ await this.save();
2081
+ return { success: successCount, failed: failedCount, results };
2082
+ }
2083
+ /**
2084
+ * Bulk delete tasks
2085
+ */
2086
+ async bulkDeleteTasks(taskIds) {
2087
+ const state = this.ensureInitialized();
2088
+ let deletedCount = 0;
2089
+ let failedCount = 0;
2090
+ const errors = [];
2091
+ for (const taskId of taskIds) {
2092
+ try {
2093
+ const task = state.tasks[taskId];
2094
+ if (!task) {
2095
+ errors.push({ id: taskId, error: "Task not found" });
2096
+ failedCount++;
2097
+ continue;
2098
+ }
2099
+ if (task.status === "in_progress") {
2100
+ errors.push({ id: taskId, error: "Cannot delete in-progress task" });
2101
+ failedCount++;
2102
+ continue;
2103
+ }
2104
+ // Remove from staging
2105
+ const staging = state.stagings[task.stagingId];
2106
+ if (staging) {
2107
+ staging.tasks = staging.tasks.filter(id => id !== taskId);
2108
+ }
2109
+ // Remove from other tasks' dependencies
2110
+ for (const t of Object.values(state.tasks)) {
2111
+ t.depends_on = t.depends_on.filter(id => id !== taskId);
2112
+ }
2113
+ this.invalidatePlanCache(task.planId);
2114
+ delete state.tasks[taskId];
2115
+ deletedCount++;
2116
+ }
2117
+ catch (error) {
2118
+ errors.push({
2119
+ id: taskId,
2120
+ error: error instanceof Error ? error.message : "Unknown error",
2121
+ });
2122
+ failedCount++;
2123
+ }
2124
+ }
2125
+ await this.save();
2126
+ return { deleted: deletedCount, failed: failedCount, errors };
2127
+ }
2128
+ /**
2129
+ * Bulk update memories
2130
+ */
2131
+ async bulkUpdateMemories(updates) {
2132
+ const state = this.ensureInitialized();
2133
+ const now = new Date().toISOString();
2134
+ const results = [];
2135
+ let successCount = 0;
2136
+ let failedCount = 0;
2137
+ for (const update of updates) {
2138
+ const memory = state.context.memories.find(m => m.id === update.memoryId);
2139
+ if (!memory) {
2140
+ results.push({ id: update.memoryId, success: false, error: "Memory not found" });
2141
+ failedCount++;
2142
+ continue;
2143
+ }
2144
+ if (update.enabled !== undefined)
2145
+ memory.enabled = update.enabled;
2146
+ if (update.priority !== undefined)
2147
+ memory.priority = update.priority;
2148
+ memory.updatedAt = now;
2149
+ results.push({ id: update.memoryId, success: true, data: memory });
2150
+ successCount++;
2151
+ }
2152
+ await this.save();
2153
+ return { success: successCount, failed: failedCount, results };
2154
+ }
2155
+ // ============ Pagination Operations ============
2156
+ /**
2157
+ * Get plans with pagination
2158
+ */
2159
+ getPlansWithPagination(options) {
2160
+ const page = options.page ?? 1;
2161
+ const pageSize = options.pageSize ?? 20;
2162
+ let plans = this.getAllPlans();
2163
+ // Filter by status
2164
+ if (options.status) {
2165
+ const statuses = Array.isArray(options.status) ? options.status : [options.status];
2166
+ plans = plans.filter(p => statuses.includes(p.status));
2167
+ }
2168
+ // Sort
2169
+ const sortBy = options.sortBy ?? "updatedAt";
2170
+ const sortOrder = options.sortOrder ?? "desc";
2171
+ plans.sort((a, b) => {
2172
+ const aVal = this.getNestedValue(a, sortBy);
2173
+ const bVal = this.getNestedValue(b, sortBy);
2174
+ const cmp = this.compareValues(aVal, bVal);
2175
+ return sortOrder === "desc" ? -cmp : cmp;
2176
+ });
2177
+ // Paginate
2178
+ const totalItems = plans.length;
2179
+ const totalPages = Math.ceil(totalItems / pageSize);
2180
+ const startIndex = (page - 1) * pageSize;
2181
+ const endIndex = Math.min(startIndex + pageSize - 1, totalItems - 1);
2182
+ const items = plans.slice(startIndex, startIndex + pageSize);
2183
+ return {
2184
+ items,
2185
+ pagination: {
2186
+ page,
2187
+ pageSize,
2188
+ totalItems,
2189
+ totalPages,
2190
+ hasPrevious: page > 1,
2191
+ hasNext: page < totalPages,
2192
+ startIndex,
2193
+ endIndex: Math.max(endIndex, 0),
2194
+ },
2195
+ };
2196
+ }
2197
+ /**
2198
+ * Get tasks with pagination
2199
+ */
2200
+ getTasksWithPagination(options) {
2201
+ const page = options.page ?? 1;
2202
+ const pageSize = options.pageSize ?? 20;
2203
+ let tasks = [];
2204
+ if (options.stagingId) {
2205
+ tasks = this.getTasksByStaging(options.stagingId);
2206
+ }
2207
+ else if (options.planId) {
2208
+ const stagings = this.getStagingsByPlan(options.planId);
2209
+ for (const staging of stagings) {
2210
+ tasks.push(...this.getTasksByStaging(staging.id));
2211
+ }
2212
+ }
2213
+ else {
2214
+ tasks = this.state ? Object.values(this.state.tasks) : [];
2215
+ }
2216
+ // Filter by status
2217
+ if (options.status) {
2218
+ const statuses = Array.isArray(options.status) ? options.status : [options.status];
2219
+ tasks = tasks.filter(t => statuses.includes(t.status));
2220
+ }
2221
+ // Filter by priority
2222
+ if (options.priority) {
2223
+ tasks = tasks.filter(t => t.priority === options.priority);
2224
+ }
2225
+ // Sort
2226
+ const sortBy = options.sortBy ?? "order";
2227
+ const sortOrder = options.sortOrder ?? "asc";
2228
+ tasks.sort((a, b) => {
2229
+ const aVal = this.getNestedValue(a, sortBy);
2230
+ const bVal = this.getNestedValue(b, sortBy);
2231
+ const cmp = this.compareValues(aVal, bVal);
2232
+ return sortOrder === "desc" ? -cmp : cmp;
2233
+ });
2234
+ // Paginate
2235
+ const totalItems = tasks.length;
2236
+ const totalPages = Math.ceil(totalItems / pageSize);
2237
+ const startIndex = (page - 1) * pageSize;
2238
+ const endIndex = Math.min(startIndex + pageSize - 1, totalItems - 1);
2239
+ const items = tasks.slice(startIndex, startIndex + pageSize);
2240
+ return {
2241
+ items,
2242
+ pagination: {
2243
+ page,
2244
+ pageSize,
2245
+ totalItems,
2246
+ totalPages,
2247
+ hasPrevious: page > 1,
2248
+ hasNext: page < totalPages,
2249
+ startIndex,
2250
+ endIndex: Math.max(endIndex, 0),
2251
+ },
2252
+ };
2253
+ }
2254
+ /**
2255
+ * Get templates with pagination
2256
+ */
2257
+ getTemplatesWithPagination(options) {
2258
+ const page = options.page ?? 1;
2259
+ const pageSize = options.pageSize ?? 20;
2260
+ let templates = this.getAllTemplates();
2261
+ // Filter by category
2262
+ if (options.category) {
2263
+ templates = templates.filter(t => t.category === options.category);
2264
+ }
2265
+ // Sort
2266
+ const sortBy = options.sortBy ?? "usageCount";
2267
+ const sortOrder = options.sortOrder ?? "desc";
2268
+ templates.sort((a, b) => {
2269
+ const aVal = this.getNestedValue(a, sortBy);
2270
+ const bVal = this.getNestedValue(b, sortBy);
2271
+ const cmp = this.compareValues(aVal, bVal);
2272
+ return sortOrder === "desc" ? -cmp : cmp;
2273
+ });
2274
+ // Paginate
2275
+ const totalItems = templates.length;
2276
+ const totalPages = Math.ceil(totalItems / pageSize);
2277
+ const startIndex = (page - 1) * pageSize;
2278
+ const endIndex = Math.min(startIndex + pageSize - 1, totalItems - 1);
2279
+ const items = templates.slice(startIndex, startIndex + pageSize);
2280
+ return {
2281
+ items,
2282
+ pagination: {
2283
+ page,
2284
+ pageSize,
2285
+ totalItems,
2286
+ totalPages,
2287
+ hasPrevious: page > 1,
2288
+ hasNext: page < totalPages,
2289
+ startIndex,
2290
+ endIndex: Math.max(endIndex, 0),
2291
+ },
2292
+ };
2293
+ }
2294
+ // ============ Lazy Loading Operations ============
2295
+ /**
2296
+ * Get plan with optional lazy loading (without tasks details)
2297
+ */
2298
+ getPlanLazy(planId, options) {
2299
+ const plan = this.getPlan(planId);
2300
+ if (!plan)
2301
+ return null;
2302
+ const stagings = this.getStagingsByPlan(planId);
2303
+ let taskCount = 0;
2304
+ let tasks;
2305
+ for (const staging of stagings) {
2306
+ taskCount += staging.tasks.length;
2307
+ }
2308
+ if (options?.includeTasks) {
2309
+ tasks = [];
2310
+ for (const staging of stagings) {
2311
+ tasks.push(...this.getTasksByStaging(staging.id));
2312
+ }
2313
+ }
2314
+ return { plan, stagings, taskCount, tasks };
2315
+ }
2316
+ /**
2317
+ * Get staging with optional task loading
2318
+ */
2319
+ getStagingLazy(stagingId, options) {
2320
+ const staging = this.getStaging(stagingId);
2321
+ if (!staging)
2322
+ return null;
2323
+ const taskCount = staging.tasks.length;
2324
+ let tasks;
2325
+ if (options?.includeTasks) {
2326
+ const rawTasks = this.getTasksByStaging(stagingId);
2327
+ if (options?.includeOutputs) {
2328
+ tasks = rawTasks;
2329
+ }
2330
+ else {
2331
+ // Exclude outputs to reduce payload
2332
+ tasks = rawTasks.map(({ output, ...rest }) => rest);
2333
+ }
2334
+ }
2335
+ return { staging, taskCount, tasks };
2336
+ }
2337
+ /**
2338
+ * Load task outputs on demand (for lazy loading)
2339
+ */
2340
+ getTaskOutputs(taskIds) {
2341
+ const outputs = {};
2342
+ for (const taskId of taskIds) {
2343
+ const task = this.getTask(taskId);
2344
+ outputs[taskId] = task?.output ?? null;
2345
+ }
2346
+ return outputs;
2347
+ }
2348
+ /**
2349
+ * Get lightweight plan list (for dashboard/overview)
2350
+ */
2351
+ getLightweightPlanList() {
2352
+ const plans = this.getAllPlans();
2353
+ return plans.map(plan => {
2354
+ const cache = this.getCachedPlanStatus(plan.id);
2355
+ return {
2356
+ id: plan.id,
2357
+ title: plan.title,
2358
+ status: plan.status,
2359
+ stagingCount: cache?.stagingCount ?? plan.stagings.length,
2360
+ taskCount: cache?.totalTasks ?? 0,
2361
+ completedTaskCount: cache?.completedTasks ?? 0,
2362
+ progressPercent: cache && cache.totalTasks > 0
2363
+ ? Math.round((cache.completedTasks / cache.totalTasks) * 100)
2364
+ : 0,
2365
+ createdAt: plan.createdAt,
2366
+ updatedAt: plan.updatedAt,
2367
+ };
2368
+ });
2369
+ }
2370
+ // ============ Built-in Templates ============
2371
+ /**
2372
+ * Load built-in templates into state
2373
+ * @param overwrite - If true, overwrite existing built-in templates
2374
+ * @returns Number of templates loaded
2375
+ */
2376
+ async loadBuiltInTemplates(overwrite = false) {
2377
+ const state = this.ensureInitialized();
2378
+ // Dynamic import to avoid circular dependencies
2379
+ const { getBuiltInTemplates } = await import("../templates/index.js");
2380
+ const builtInTemplates = getBuiltInTemplates();
2381
+ let loadedCount = 0;
2382
+ for (const template of builtInTemplates) {
2383
+ // Check if template already exists by name
2384
+ const existing = this.getAllTemplates().find(t => t.isBuiltIn && t.name === template.name);
2385
+ if (existing && !overwrite) {
2386
+ continue;
2387
+ }
2388
+ // Remove existing if overwriting
2389
+ if (existing && overwrite) {
2390
+ delete state.templates[existing.id];
2391
+ }
2392
+ // Create the template
2393
+ const now = new Date().toISOString();
2394
+ const templateId = idGenerator.generateTemplateId();
2395
+ const newTemplate = {
2396
+ id: templateId,
2397
+ name: template.name,
2398
+ description: template.description,
2399
+ category: template.category,
2400
+ tags: template.tags,
2401
+ stagings: template.stagings,
2402
+ variables: template.variables,
2403
+ usageCount: 0,
2404
+ isBuiltIn: true,
2405
+ createdAt: now,
2406
+ updatedAt: now,
2407
+ };
2408
+ state.templates[templateId] = newTemplate;
2409
+ loadedCount++;
2410
+ }
2411
+ if (loadedCount > 0) {
2412
+ await this.save();
2413
+ }
2414
+ return loadedCount;
2415
+ }
2416
+ /**
2417
+ * Check if built-in templates are loaded
2418
+ */
2419
+ hasBuiltInTemplates() {
2420
+ const templates = this.getAllTemplates();
2421
+ return templates.some(t => t.isBuiltIn);
2422
+ }
1214
2423
  }
1215
2424
  export { idGenerator };
1216
2425
  //# sourceMappingURL=manager.js.map