@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.
- package/dist/state/manager.d.ts +274 -3
- package/dist/state/manager.d.ts.map +1 -1
- package/dist/state/manager.js +1211 -2
- package/dist/state/manager.js.map +1 -1
- package/dist/state/schema.d.ts +6535 -545
- package/dist/state/schema.d.ts.map +1 -1
- package/dist/state/schema.js +341 -0
- package/dist/state/schema.js.map +1 -1
- package/dist/state/types.d.ts +62 -1
- package/dist/state/types.d.ts.map +1 -1
- package/dist/state/types.js.map +1 -1
- package/dist/templates/index.d.ts +79 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +472 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/tools/bulk.d.ts +6 -0
- package/dist/tools/bulk.d.ts.map +1 -0
- package/dist/tools/bulk.js +273 -0
- package/dist/tools/bulk.js.map +1 -0
- package/dist/tools/context.d.ts.map +1 -1
- package/dist/tools/context.js +59 -0
- package/dist/tools/context.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +15 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/navigate.d.ts +6 -0
- package/dist/tools/navigate.d.ts.map +1 -0
- package/dist/tools/navigate.js +334 -0
- package/dist/tools/navigate.js.map +1 -0
- package/dist/tools/plan.d.ts.map +1 -1
- package/dist/tools/plan.js +134 -2
- package/dist/tools/plan.js.map +1 -1
- package/dist/tools/rollback.d.ts +6 -0
- package/dist/tools/rollback.d.ts.map +1 -0
- package/dist/tools/rollback.js +278 -0
- package/dist/tools/rollback.js.map +1 -0
- package/dist/tools/search.d.ts +6 -0
- package/dist/tools/search.d.ts.map +1 -0
- package/dist/tools/search.js +252 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/staging.d.ts.map +1 -1
- package/dist/tools/staging.js +74 -22
- package/dist/tools/staging.js.map +1 -1
- package/dist/tools/template.d.ts +6 -0
- package/dist/tools/template.d.ts.map +1 -0
- package/dist/tools/template.js +348 -0
- package/dist/tools/template.js.map +1 -0
- package/dist/utils/format.d.ts +173 -0
- package/dist/utils/format.d.ts.map +1 -1
- package/dist/utils/format.js +336 -0
- package/dist/utils/format.js.map +1 -1
- package/package.json +1 -1
package/dist/state/manager.js
CHANGED
|
@@ -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
|