@zeliper/zscode-mcp-server 1.0.1 → 1.0.2

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 (50) hide show
  1. package/dist/errors/index.d.ts +28 -0
  2. package/dist/errors/index.d.ts.map +1 -1
  3. package/dist/errors/index.js +50 -0
  4. package/dist/errors/index.js.map +1 -1
  5. package/dist/state/manager.d.ts +33 -2
  6. package/dist/state/manager.d.ts.map +1 -1
  7. package/dist/state/manager.js +341 -138
  8. package/dist/state/manager.js.map +1 -1
  9. package/dist/state/manager.test.d.ts +2 -0
  10. package/dist/state/manager.test.d.ts.map +1 -0
  11. package/dist/state/manager.test.js +310 -0
  12. package/dist/state/manager.test.js.map +1 -0
  13. package/dist/state/schema.d.ts +190 -9
  14. package/dist/state/schema.d.ts.map +1 -1
  15. package/dist/state/schema.js +33 -1
  16. package/dist/state/schema.js.map +1 -1
  17. package/dist/state/schema.test.d.ts +2 -0
  18. package/dist/state/schema.test.d.ts.map +1 -0
  19. package/dist/state/schema.test.js +300 -0
  20. package/dist/state/schema.test.js.map +1 -0
  21. package/dist/state/types.d.ts +5 -1
  22. package/dist/state/types.d.ts.map +1 -1
  23. package/dist/state/types.js +3 -1
  24. package/dist/state/types.js.map +1 -1
  25. package/dist/tools/archive.d.ts.map +1 -1
  26. package/dist/tools/archive.js +57 -0
  27. package/dist/tools/archive.js.map +1 -1
  28. package/dist/tools/context.d.ts.map +1 -1
  29. package/dist/tools/context.js +14 -0
  30. package/dist/tools/context.js.map +1 -1
  31. package/dist/tools/index.d.ts.map +1 -1
  32. package/dist/tools/index.js +3 -0
  33. package/dist/tools/index.js.map +1 -1
  34. package/dist/tools/memory.d.ts +7 -0
  35. package/dist/tools/memory.d.ts.map +1 -0
  36. package/dist/tools/memory.js +243 -0
  37. package/dist/tools/memory.js.map +1 -0
  38. package/dist/tools/modify.d.ts.map +1 -1
  39. package/dist/tools/modify.js +9 -6
  40. package/dist/tools/modify.js.map +1 -1
  41. package/dist/tools/plan.js +3 -3
  42. package/dist/tools/plan.js.map +1 -1
  43. package/dist/tools/staging.d.ts.map +1 -1
  44. package/dist/tools/staging.js +3 -3
  45. package/dist/tools/staging.js.map +1 -1
  46. package/dist/utils/paths.d.ts +34 -0
  47. package/dist/utils/paths.d.ts.map +1 -1
  48. package/dist/utils/paths.js +94 -2
  49. package/dist/utils/paths.js.map +1 -1
  50. package/package.json +10 -3
@@ -1,23 +1,29 @@
1
- import { readFile, writeFile, access, rm, cp } from "fs/promises";
1
+ import { readFile, access, rm, cp } from "fs/promises";
2
2
  import { join, dirname } from "path";
3
3
  import { constants } from "fs";
4
4
  import { StateSchema } from "./schema.js";
5
- import { normalizePath, toPosixPath, ensureDir } from "../utils/paths.js";
5
+ import { STATE_VERSION, } from "./types.js";
6
+ import { normalizePath, toPosixPath, ensureDir, isPathSafe, isValidId, atomicWriteFile, generateSecureId, } from "../utils/paths.js";
7
+ import { ProjectNotInitializedError, PlanNotFoundError, PlanInvalidStateError, StagingNotFoundError, StagingOrderError, StagingPlanMismatchError, StagingInvalidStateError, TaskNotFoundError, TaskInvalidStateError, TaskStateTransitionError, CircularDependencyError, MemoryNotFoundError, PathTraversalError, InvalidIdError, } from "../errors/index.js";
8
+ // ============ Constants ============
9
+ const MAX_HISTORY_ENTRIES = 1000;
10
+ // Valid task state transitions
11
+ const VALID_TASK_TRANSITIONS = {
12
+ pending: ["in_progress", "blocked", "cancelled"],
13
+ in_progress: ["done", "blocked", "cancelled"],
14
+ blocked: ["in_progress", "cancelled"],
15
+ done: [], // Terminal state - no transitions allowed
16
+ cancelled: [], // Terminal state - no transitions allowed
17
+ };
6
18
  // ============ ID Generator ============
7
- function generateRandomId(length) {
8
- const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
9
- let result = "";
10
- for (let i = 0; i < length; i++) {
11
- result += chars.charAt(Math.floor(Math.random() * chars.length));
12
- }
13
- return result;
14
- }
19
+ // Uses cryptographically secure random generation
15
20
  const idGenerator = {
16
- generatePlanId: () => `plan-${generateRandomId(8)}`,
17
- generateStagingId: () => `staging-${generateRandomId(4)}`,
18
- generateTaskId: () => `task-${generateRandomId(8)}`,
19
- generateHistoryId: () => `hist-${Date.now()}-${generateRandomId(4)}`,
20
- generateDecisionId: () => `dec-${Date.now()}-${generateRandomId(4)}`,
21
+ generatePlanId: () => `plan-${generateSecureId(8)}`,
22
+ generateStagingId: () => `staging-${generateSecureId(4)}`,
23
+ generateTaskId: () => `task-${generateSecureId(8)}`,
24
+ generateHistoryId: () => `hist-${Date.now()}-${generateSecureId(4)}`,
25
+ generateDecisionId: () => `dec-${Date.now()}-${generateSecureId(4)}`,
26
+ generateMemoryId: () => `mem-${generateSecureId(8)}`,
21
27
  };
22
28
  // ============ StateManager Class ============
23
29
  export class StateManager {
@@ -64,8 +70,47 @@ export class StateManager {
64
70
  if (!this.state) {
65
71
  throw new Error("No state to save");
66
72
  }
67
- await ensureDir(dirname(this.stateFilePath));
68
- await writeFile(this.stateFilePath, JSON.stringify(this.state, null, 2), "utf-8");
73
+ // Use atomic write to prevent data corruption
74
+ await atomicWriteFile(this.stateFilePath, JSON.stringify(this.state, null, 2));
75
+ }
76
+ // ============ Validation Helpers ============
77
+ ensureInitialized() {
78
+ if (!this.state) {
79
+ throw new ProjectNotInitializedError();
80
+ }
81
+ return this.state;
82
+ }
83
+ requirePlan(planId) {
84
+ const state = this.ensureInitialized();
85
+ const plan = state.plans[planId];
86
+ if (!plan) {
87
+ throw new PlanNotFoundError(planId);
88
+ }
89
+ return plan;
90
+ }
91
+ requireStaging(stagingId) {
92
+ const state = this.ensureInitialized();
93
+ const staging = state.stagings[stagingId];
94
+ if (!staging) {
95
+ throw new StagingNotFoundError(stagingId);
96
+ }
97
+ return staging;
98
+ }
99
+ requireTask(taskId) {
100
+ const state = this.ensureInitialized();
101
+ const task = state.tasks[taskId];
102
+ if (!task) {
103
+ throw new TaskNotFoundError(taskId);
104
+ }
105
+ return task;
106
+ }
107
+ requireMemory(memoryId) {
108
+ const state = this.ensureInitialized();
109
+ const memory = state.context.memories.find(m => m.id === memoryId);
110
+ if (!memory) {
111
+ throw new MemoryNotFoundError(memoryId);
112
+ }
113
+ return memory;
69
114
  }
70
115
  // ============ Getters ============
71
116
  getState() {
@@ -120,7 +165,7 @@ export class StateManager {
120
165
  updatedAt: now,
121
166
  };
122
167
  this.state = {
123
- version: "2.0.0",
168
+ version: STATE_VERSION,
124
169
  project,
125
170
  plans: {},
126
171
  stagings: {},
@@ -130,6 +175,7 @@ export class StateManager {
130
175
  lastUpdated: now,
131
176
  activeFiles: [],
132
177
  decisions: [],
178
+ memories: [],
133
179
  },
134
180
  };
135
181
  await this.addHistory("project_initialized", { projectName: name });
@@ -138,9 +184,7 @@ export class StateManager {
138
184
  }
139
185
  // ============ Plan Operations ============
140
186
  async createPlan(title, description, stagingConfigs) {
141
- if (!this.state) {
142
- throw new Error("Project not initialized");
143
- }
187
+ const state = this.ensureInitialized();
144
188
  const now = new Date().toISOString();
145
189
  const planId = idGenerator.generatePlanId();
146
190
  const artifactsRoot = toPosixPath(join(".claude", "plans", planId, "artifacts"));
@@ -173,7 +217,7 @@ export class StateManager {
173
217
  };
174
218
  tasks.push(task);
175
219
  taskIds.push(taskId);
176
- this.state.tasks[taskId] = task;
220
+ state.tasks[taskId] = task;
177
221
  }
178
222
  // Resolve task dependencies within the same staging
179
223
  for (let i = 0; i < tasks.length; i++) {
@@ -197,7 +241,7 @@ export class StateManager {
197
241
  artifacts_path: artifactsPath,
198
242
  createdAt: now,
199
243
  };
200
- this.state.stagings[stagingId] = staging;
244
+ state.stagings[stagingId] = staging;
201
245
  stagingIds.push(stagingId);
202
246
  }
203
247
  // Create plan
@@ -211,17 +255,13 @@ export class StateManager {
211
255
  createdAt: now,
212
256
  updatedAt: now,
213
257
  };
214
- this.state.plans[planId] = plan;
258
+ state.plans[planId] = plan;
215
259
  await this.addHistory("plan_created", { planId, title, stagingCount: stagingIds.length });
216
260
  await this.save();
217
261
  return plan;
218
262
  }
219
263
  async updatePlanStatus(planId, status) {
220
- if (!this.state)
221
- throw new Error("Project not initialized");
222
- const plan = this.getPlan(planId);
223
- if (!plan)
224
- throw new Error(`Plan not found: ${planId}`);
264
+ const plan = this.requirePlan(planId);
225
265
  plan.status = status;
226
266
  plan.updatedAt = new Date().toISOString();
227
267
  if (status === "completed") {
@@ -234,22 +274,17 @@ export class StateManager {
234
274
  }
235
275
  // ============ Staging Operations ============
236
276
  async startStaging(planId, stagingId) {
237
- if (!this.state)
238
- throw new Error("Project not initialized");
239
- const plan = this.getPlan(planId);
240
- if (!plan)
241
- throw new Error(`Plan not found: ${planId}`);
242
- const staging = this.getStaging(stagingId);
243
- if (!staging)
244
- throw new Error(`Staging not found: ${stagingId}`);
277
+ const state = this.ensureInitialized();
278
+ const plan = this.requirePlan(planId);
279
+ const staging = this.requireStaging(stagingId);
245
280
  if (staging.planId !== planId) {
246
- throw new Error(`Staging ${stagingId} does not belong to plan ${planId}`);
281
+ throw new StagingPlanMismatchError(stagingId, planId, staging.planId);
247
282
  }
248
283
  // Check if previous staging is completed
249
284
  if (staging.order > 0) {
250
285
  const prevStaging = this.getStagingByOrder(planId, staging.order - 1);
251
286
  if (prevStaging && prevStaging.status !== "completed") {
252
- throw new Error(`Cannot start staging: previous staging ${prevStaging.id} is not completed`);
287
+ throw new StagingOrderError(stagingId, prevStaging.id);
253
288
  }
254
289
  }
255
290
  const now = new Date().toISOString();
@@ -258,9 +293,9 @@ export class StateManager {
258
293
  plan.currentStagingId = stagingId;
259
294
  plan.status = "active";
260
295
  plan.updatedAt = now;
261
- this.state.context.currentPlanId = planId;
262
- this.state.context.currentStagingId = stagingId;
263
- this.state.context.lastUpdated = now;
296
+ state.context.currentPlanId = planId;
297
+ state.context.currentStagingId = stagingId;
298
+ state.context.lastUpdated = now;
264
299
  // Create artifacts directory
265
300
  const artifactsDir = normalizePath(join(this.projectRoot, staging.artifacts_path));
266
301
  await ensureDir(artifactsDir);
@@ -268,12 +303,9 @@ export class StateManager {
268
303
  await this.save();
269
304
  return staging;
270
305
  }
271
- async completestaging(stagingId) {
272
- if (!this.state)
273
- throw new Error("Project not initialized");
274
- const staging = this.getStaging(stagingId);
275
- if (!staging)
276
- throw new Error(`Staging not found: ${stagingId}`);
306
+ async completeStaging(stagingId) {
307
+ const state = this.ensureInitialized();
308
+ const staging = this.requireStaging(stagingId);
277
309
  const now = new Date().toISOString();
278
310
  staging.status = "completed";
279
311
  staging.completedAt = now;
@@ -288,8 +320,8 @@ export class StateManager {
288
320
  if (allCompleted) {
289
321
  plan.status = "completed";
290
322
  plan.completedAt = now;
291
- this.state.context.currentPlanId = undefined;
292
- this.state.context.currentStagingId = undefined;
323
+ state.context.currentPlanId = undefined;
324
+ state.context.currentStagingId = undefined;
293
325
  }
294
326
  }
295
327
  await this.addHistory("staging_completed", { stagingId, stagingName: staging.name });
@@ -297,11 +329,12 @@ export class StateManager {
297
329
  }
298
330
  // ============ Task Operations ============
299
331
  async updateTaskStatus(taskId, status, notes) {
300
- if (!this.state)
301
- throw new Error("Project not initialized");
302
- const task = this.getTask(taskId);
303
- if (!task)
304
- throw new Error(`Task not found: ${taskId}`);
332
+ const task = this.requireTask(taskId);
333
+ // Validate state transition
334
+ const allowedTransitions = VALID_TASK_TRANSITIONS[task.status];
335
+ if (!allowedTransitions.includes(status)) {
336
+ throw new TaskStateTransitionError(taskId, task.status, status, allowedTransitions);
337
+ }
305
338
  const now = new Date().toISOString();
306
339
  task.status = status;
307
340
  task.updatedAt = now;
@@ -323,7 +356,7 @@ export class StateManager {
323
356
  return t?.status === "done";
324
357
  });
325
358
  if (allTasksDone) {
326
- await this.completestaging(staging.id);
359
+ await this.completeStaging(staging.id);
327
360
  }
328
361
  }
329
362
  }
@@ -333,19 +366,24 @@ export class StateManager {
333
366
  await this.save();
334
367
  }
335
368
  async saveTaskOutput(taskId, output) {
336
- if (!this.state)
337
- throw new Error("Project not initialized");
338
- const task = this.getTask(taskId);
339
- if (!task)
340
- throw new Error(`Task not found: ${taskId}`);
369
+ // Validate taskId to prevent path traversal
370
+ if (!isValidId(taskId)) {
371
+ throw new InvalidIdError(taskId, "task");
372
+ }
373
+ const task = this.requireTask(taskId);
341
374
  task.output = output;
342
375
  task.updatedAt = new Date().toISOString();
343
376
  // Save output to artifacts file
344
377
  const staging = this.getStaging(task.stagingId);
345
378
  if (staging) {
379
+ const claudeDir = normalizePath(join(this.projectRoot, ".claude"));
346
380
  const outputPath = normalizePath(join(this.projectRoot, staging.artifacts_path, `${taskId}-output.json`));
347
- await ensureDir(dirname(outputPath));
348
- await writeFile(outputPath, JSON.stringify(output, null, 2), "utf-8");
381
+ // Verify path is within the .claude directory (path traversal protection)
382
+ if (!isPathSafe(claudeDir, outputPath)) {
383
+ throw new PathTraversalError(outputPath);
384
+ }
385
+ // Use atomic write to prevent data corruption
386
+ await atomicWriteFile(outputPath, JSON.stringify(output, null, 2));
349
387
  }
350
388
  await this.save();
351
389
  }
@@ -369,17 +407,23 @@ export class StateManager {
369
407
  }
370
408
  // ============ Archive Operations ============
371
409
  async archivePlan(planId, reason) {
372
- if (!this.state)
373
- throw new Error("Project not initialized");
374
- const plan = this.getPlan(planId);
375
- if (!plan)
376
- throw new Error(`Plan not found: ${planId}`);
410
+ const state = this.ensureInitialized();
411
+ // Validate planId to prevent path traversal
412
+ if (!isValidId(planId)) {
413
+ throw new InvalidIdError(planId, "plan");
414
+ }
415
+ const plan = this.requirePlan(planId);
377
416
  if (plan.status !== "completed" && plan.status !== "cancelled") {
378
- throw new Error(`Cannot archive plan in ${plan.status} status. Only completed or cancelled plans can be archived.`);
417
+ throw new PlanInvalidStateError(planId, plan.status, ["completed", "cancelled"]);
379
418
  }
380
419
  const now = new Date().toISOString();
420
+ const claudeDir = normalizePath(join(this.projectRoot, ".claude"));
381
421
  const sourcePath = normalizePath(join(this.projectRoot, ".claude", "plans", planId));
382
422
  const archivePath = normalizePath(join(this.projectRoot, ".claude", "archive", planId));
423
+ // Verify paths are within the .claude directory (path traversal protection)
424
+ if (!isPathSafe(claudeDir, sourcePath) || !isPathSafe(claudeDir, archivePath)) {
425
+ throw new PathTraversalError(planId);
426
+ }
383
427
  // Move plan directory to archive
384
428
  try {
385
429
  await ensureDir(dirname(archivePath));
@@ -394,23 +438,56 @@ export class StateManager {
394
438
  plan.archivedAt = now;
395
439
  plan.updatedAt = now;
396
440
  // Clear current context if this was the active plan
397
- if (this.state.context.currentPlanId === planId) {
398
- this.state.context.currentPlanId = undefined;
399
- this.state.context.currentStagingId = undefined;
441
+ if (state.context.currentPlanId === planId) {
442
+ state.context.currentPlanId = undefined;
443
+ state.context.currentStagingId = undefined;
400
444
  }
401
445
  await this.addHistory("plan_archived", { planId, title: plan.title, reason });
402
446
  await this.save();
403
447
  return toPosixPath(join(".claude", "archive", planId));
404
448
  }
449
+ async unarchivePlan(planId) {
450
+ this.ensureInitialized();
451
+ // Validate planId to prevent path traversal
452
+ if (!isValidId(planId)) {
453
+ throw new InvalidIdError(planId, "plan");
454
+ }
455
+ const plan = this.requirePlan(planId);
456
+ if (plan.status !== "archived") {
457
+ throw new PlanInvalidStateError(planId, plan.status, ["archived"]);
458
+ }
459
+ const now = new Date().toISOString();
460
+ const claudeDir = normalizePath(join(this.projectRoot, ".claude"));
461
+ const archivePath = normalizePath(join(this.projectRoot, ".claude", "archive", planId));
462
+ const restorePath = normalizePath(join(this.projectRoot, ".claude", "plans", planId));
463
+ // Verify paths are within the .claude directory (path traversal protection)
464
+ if (!isPathSafe(claudeDir, archivePath) || !isPathSafe(claudeDir, restorePath)) {
465
+ throw new PathTraversalError(planId);
466
+ }
467
+ // Move plan directory back from archive
468
+ try {
469
+ await ensureDir(dirname(restorePath));
470
+ await cp(archivePath, restorePath, { recursive: true });
471
+ await rm(archivePath, { recursive: true, force: true });
472
+ }
473
+ catch (error) {
474
+ // Directory might not exist if no artifacts were created
475
+ console.error(`Unarchive directory operation failed: ${error}`);
476
+ }
477
+ // Restore plan to completed status (since it was completed before archiving)
478
+ plan.status = "completed";
479
+ plan.archivedAt = undefined;
480
+ plan.updatedAt = now;
481
+ await this.addHistory("plan_unarchived", { planId, title: plan.title });
482
+ await this.save();
483
+ return { plan, restoredPath: toPosixPath(join(".claude", "plans", planId)) };
484
+ }
405
485
  // ============ Cancel Operations ============
406
486
  async cancelPlan(planId, reason) {
407
- if (!this.state)
408
- throw new Error("Project not initialized");
409
- const plan = this.getPlan(planId);
410
- if (!plan)
411
- throw new Error(`Plan not found: ${planId}`);
487
+ const state = this.ensureInitialized();
488
+ const plan = this.requirePlan(planId);
412
489
  if (plan.status === "archived" || plan.status === "cancelled") {
413
- throw new Error(`Plan is already ${plan.status}`);
490
+ throw new PlanInvalidStateError(planId, plan.status, ["draft", "active", "completed"]);
414
491
  }
415
492
  const now = new Date().toISOString();
416
493
  let affectedStagings = 0;
@@ -435,9 +512,9 @@ export class StateManager {
435
512
  plan.status = "cancelled";
436
513
  plan.updatedAt = now;
437
514
  // Clear current context if this was the active plan
438
- if (this.state.context.currentPlanId === planId) {
439
- this.state.context.currentPlanId = undefined;
440
- this.state.context.currentStagingId = undefined;
515
+ if (state.context.currentPlanId === planId) {
516
+ state.context.currentPlanId = undefined;
517
+ state.context.currentStagingId = undefined;
441
518
  }
442
519
  await this.addHistory("plan_cancelled", { planId, title: plan.title, reason, affectedStagings, affectedTasks });
443
520
  await this.save();
@@ -455,10 +532,14 @@ export class StateManager {
455
532
  };
456
533
  this.state.history.push(entry);
457
534
  this.state.context.lastUpdated = entry.timestamp;
535
+ // Enforce history size limit - remove oldest entries if exceeded
536
+ if (this.state.history.length > MAX_HISTORY_ENTRIES) {
537
+ const excess = this.state.history.length - MAX_HISTORY_ENTRIES;
538
+ this.state.history.splice(0, excess);
539
+ }
458
540
  }
459
541
  async addDecision(title, decision, rationale, relatedPlanId, relatedStagingId) {
460
- if (!this.state)
461
- throw new Error("Project not initialized");
542
+ const state = this.ensureInitialized();
462
543
  const now = new Date().toISOString();
463
544
  const decisionEntry = {
464
545
  id: idGenerator.generateDecisionId(),
@@ -469,11 +550,105 @@ export class StateManager {
469
550
  relatedStagingId,
470
551
  timestamp: now,
471
552
  };
472
- this.state.context.decisions.push(decisionEntry);
553
+ state.context.decisions.push(decisionEntry);
473
554
  await this.addHistory("decision_added", { decisionId: decisionEntry.id, title });
474
555
  await this.save();
475
556
  return decisionEntry;
476
557
  }
558
+ // ============ Memory Operations ============
559
+ async addMemory(category, title, content, tags, priority) {
560
+ const state = this.ensureInitialized();
561
+ const now = new Date().toISOString();
562
+ const memory = {
563
+ id: idGenerator.generateMemoryId(),
564
+ category,
565
+ title,
566
+ content,
567
+ tags: tags ?? [],
568
+ priority: priority ?? 50,
569
+ enabled: true,
570
+ createdAt: now,
571
+ updatedAt: now,
572
+ };
573
+ state.context.memories.push(memory);
574
+ await this.addHistory("memory_added", { memoryId: memory.id, title, category });
575
+ await this.save();
576
+ return memory;
577
+ }
578
+ listMemories(category, tags, enabledOnly = true) {
579
+ if (!this.state)
580
+ return [];
581
+ let memories = this.state.context.memories;
582
+ if (enabledOnly) {
583
+ memories = memories.filter(m => m.enabled);
584
+ }
585
+ if (category) {
586
+ memories = memories.filter(m => m.category === category);
587
+ }
588
+ if (tags && tags.length > 0) {
589
+ memories = memories.filter(m => tags.some(tag => m.tags.includes(tag)));
590
+ }
591
+ // Sort by priority (descending)
592
+ return memories.sort((a, b) => b.priority - a.priority);
593
+ }
594
+ getMemoriesForContext(context) {
595
+ if (!this.state)
596
+ return [];
597
+ const enabledMemories = this.state.context.memories.filter(m => m.enabled);
598
+ let result;
599
+ if (context === "all") {
600
+ result = enabledMemories;
601
+ }
602
+ else if (context === "general") {
603
+ result = enabledMemories.filter(m => m.category === "general");
604
+ }
605
+ else {
606
+ // Return general + context-specific memories
607
+ result = enabledMemories.filter(m => m.category === "general" || m.category === context);
608
+ }
609
+ // Sort by priority (descending)
610
+ return result.sort((a, b) => b.priority - a.priority);
611
+ }
612
+ async updateMemory(memoryId, updates) {
613
+ const memory = this.requireMemory(memoryId);
614
+ const now = new Date().toISOString();
615
+ if (updates.title !== undefined)
616
+ memory.title = updates.title;
617
+ if (updates.content !== undefined)
618
+ memory.content = updates.content;
619
+ if (updates.category !== undefined)
620
+ memory.category = updates.category;
621
+ if (updates.tags !== undefined)
622
+ memory.tags = updates.tags;
623
+ if (updates.priority !== undefined)
624
+ memory.priority = updates.priority;
625
+ if (updates.enabled !== undefined)
626
+ memory.enabled = updates.enabled;
627
+ memory.updatedAt = now;
628
+ await this.addHistory("memory_updated", { memoryId, updates });
629
+ await this.save();
630
+ return memory;
631
+ }
632
+ async removeMemory(memoryId) {
633
+ const state = this.ensureInitialized();
634
+ const memory = this.requireMemory(memoryId);
635
+ const index = state.context.memories.findIndex(m => m.id === memoryId);
636
+ state.context.memories.splice(index, 1);
637
+ await this.addHistory("memory_removed", { memoryId, title: memory.title });
638
+ await this.save();
639
+ }
640
+ getMemory(memoryId) {
641
+ return this.state?.context.memories.find(m => m.id === memoryId);
642
+ }
643
+ getCategories() {
644
+ if (!this.state)
645
+ return [];
646
+ const categories = new Set();
647
+ for (const memory of this.state.context.memories) {
648
+ categories.add(memory.category);
649
+ }
650
+ return Array.from(categories).sort();
651
+ }
477
652
  // ============ Session Operations ============
478
653
  async startSession() {
479
654
  await this.addHistory("session_started", {});
@@ -490,13 +665,9 @@ export class StateManager {
490
665
  }
491
666
  // ============ Modify Operations ============
492
667
  async updatePlan(planId, updates) {
493
- if (!this.state)
494
- throw new Error("Project not initialized");
495
- const plan = this.getPlan(planId);
496
- if (!plan)
497
- throw new Error(`Plan not found: ${planId}`);
668
+ const plan = this.requirePlan(planId);
498
669
  if (plan.status === "archived" || plan.status === "cancelled") {
499
- throw new Error(`Cannot modify ${plan.status} plan`);
670
+ throw new PlanInvalidStateError(planId, plan.status, ["draft", "active", "completed"]);
500
671
  }
501
672
  const now = new Date().toISOString();
502
673
  if (updates.title !== undefined)
@@ -509,13 +680,9 @@ export class StateManager {
509
680
  return plan;
510
681
  }
511
682
  async updateStaging(stagingId, updates) {
512
- if (!this.state)
513
- throw new Error("Project not initialized");
514
- const staging = this.getStaging(stagingId);
515
- if (!staging)
516
- throw new Error(`Staging not found: ${stagingId}`);
683
+ const staging = this.requireStaging(stagingId);
517
684
  if (staging.status === "completed" || staging.status === "cancelled") {
518
- throw new Error(`Cannot modify ${staging.status} staging`);
685
+ throw new StagingInvalidStateError(stagingId, staging.status, ["pending", "in_progress"]);
519
686
  }
520
687
  if (updates.name !== undefined)
521
688
  staging.name = updates.name;
@@ -528,13 +695,10 @@ export class StateManager {
528
695
  return staging;
529
696
  }
530
697
  async addStaging(planId, config) {
531
- if (!this.state)
532
- throw new Error("Project not initialized");
533
- const plan = this.getPlan(planId);
534
- if (!plan)
535
- throw new Error(`Plan not found: ${planId}`);
698
+ const state = this.ensureInitialized();
699
+ const plan = this.requirePlan(planId);
536
700
  if (plan.status === "archived" || plan.status === "cancelled" || plan.status === "completed") {
537
- throw new Error(`Cannot add staging to ${plan.status} plan`);
701
+ throw new PlanInvalidStateError(planId, plan.status, ["draft", "active"]);
538
702
  }
539
703
  const now = new Date().toISOString();
540
704
  const stagingId = idGenerator.generateStagingId();
@@ -559,7 +723,7 @@ export class StateManager {
559
723
  artifacts_path: artifactsPath,
560
724
  createdAt: now,
561
725
  };
562
- this.state.stagings[stagingId] = staging;
726
+ state.stagings[stagingId] = staging;
563
727
  // Insert into plan's staging list
564
728
  plan.stagings.splice(insertAt, 0, stagingId);
565
729
  plan.updatedAt = now;
@@ -568,20 +732,15 @@ export class StateManager {
568
732
  return staging;
569
733
  }
570
734
  async removeStaging(stagingId) {
571
- if (!this.state)
572
- throw new Error("Project not initialized");
573
- const staging = this.getStaging(stagingId);
574
- if (!staging)
575
- throw new Error(`Staging not found: ${stagingId}`);
735
+ const state = this.ensureInitialized();
736
+ const staging = this.requireStaging(stagingId);
576
737
  if (staging.status === "in_progress") {
577
- throw new Error("Cannot remove in_progress staging");
738
+ throw new StagingInvalidStateError(stagingId, staging.status, ["pending", "completed", "cancelled"]);
578
739
  }
579
- const plan = this.getPlan(staging.planId);
580
- if (!plan)
581
- throw new Error(`Plan not found: ${staging.planId}`);
740
+ const plan = this.requirePlan(staging.planId);
582
741
  // Remove all tasks in this staging
583
742
  for (const taskId of staging.tasks) {
584
- delete this.state.tasks[taskId];
743
+ delete state.tasks[taskId];
585
744
  }
586
745
  // Remove staging from plan
587
746
  plan.stagings = plan.stagings.filter(id => id !== stagingId);
@@ -593,22 +752,52 @@ export class StateManager {
593
752
  s.order = i;
594
753
  });
595
754
  // Remove staging
596
- delete this.state.stagings[stagingId];
755
+ delete state.stagings[stagingId];
597
756
  await this.addHistory("staging_removed", { stagingId, stagingName: staging.name });
598
757
  await this.save();
599
758
  }
759
+ // ============ Circular Dependency Detection ============
760
+ /**
761
+ * Check for circular dependencies in task dependencies
762
+ * @param taskId - The task being checked
763
+ * @param depends_on - Dependencies to validate
764
+ * @returns dependency chain if circular, null otherwise
765
+ */
766
+ detectCircularDependency(taskId, depends_on, visited = new Set()) {
767
+ if (visited.has(taskId)) {
768
+ return Array.from(visited);
769
+ }
770
+ visited.add(taskId);
771
+ for (const depId of depends_on) {
772
+ const depTask = this.getTask(depId);
773
+ if (!depTask)
774
+ continue;
775
+ if (depTask.depends_on.includes(taskId)) {
776
+ return [...Array.from(visited), depId, taskId];
777
+ }
778
+ const chain = this.detectCircularDependency(depId, depTask.depends_on, new Set(visited));
779
+ if (chain) {
780
+ return chain;
781
+ }
782
+ }
783
+ return null;
784
+ }
600
785
  async addTask(stagingId, config) {
601
- if (!this.state)
602
- throw new Error("Project not initialized");
603
- const staging = this.getStaging(stagingId);
604
- if (!staging)
605
- throw new Error(`Staging not found: ${stagingId}`);
786
+ const state = this.ensureInitialized();
787
+ const staging = this.requireStaging(stagingId);
606
788
  if (staging.status === "completed" || staging.status === "cancelled") {
607
- throw new Error(`Cannot add task to ${staging.status} staging`);
789
+ throw new StagingInvalidStateError(stagingId, staging.status, ["pending", "in_progress"]);
608
790
  }
609
791
  const now = new Date().toISOString();
610
792
  const taskId = idGenerator.generateTaskId();
611
793
  const existingTasks = this.getTasksByStaging(stagingId);
794
+ // Check for circular dependencies
795
+ if (config.depends_on && config.depends_on.length > 0) {
796
+ const circularChain = this.detectCircularDependency(taskId, config.depends_on);
797
+ if (circularChain) {
798
+ throw new CircularDependencyError(taskId, circularChain);
799
+ }
800
+ }
612
801
  const task = {
613
802
  id: taskId,
614
803
  planId: staging.planId,
@@ -623,28 +812,23 @@ export class StateManager {
623
812
  createdAt: now,
624
813
  updatedAt: now,
625
814
  };
626
- this.state.tasks[taskId] = task;
815
+ state.tasks[taskId] = task;
627
816
  staging.tasks.push(taskId);
628
817
  await this.addHistory("task_added", { stagingId, taskId, title: config.title });
629
818
  await this.save();
630
819
  return task;
631
820
  }
632
821
  async removeTask(taskId) {
633
- if (!this.state)
634
- throw new Error("Project not initialized");
635
- const task = this.getTask(taskId);
636
- if (!task)
637
- throw new Error(`Task not found: ${taskId}`);
822
+ const state = this.ensureInitialized();
823
+ const task = this.requireTask(taskId);
638
824
  if (task.status === "in_progress") {
639
- throw new Error("Cannot remove in_progress task");
825
+ throw new TaskInvalidStateError(taskId, task.status, ["pending", "done", "blocked", "cancelled"]);
640
826
  }
641
- const staging = this.getStaging(task.stagingId);
642
- if (!staging)
643
- throw new Error(`Staging not found: ${task.stagingId}`);
827
+ const staging = this.requireStaging(task.stagingId);
644
828
  // Remove task from staging
645
829
  staging.tasks = staging.tasks.filter(id => id !== taskId);
646
830
  // Remove this task from other tasks' dependencies
647
- for (const t of Object.values(this.state.tasks)) {
831
+ for (const t of Object.values(state.tasks)) {
648
832
  t.depends_on = t.depends_on.filter(id => id !== taskId);
649
833
  }
650
834
  // Reorder remaining tasks
@@ -654,18 +838,14 @@ export class StateManager {
654
838
  t.order = i;
655
839
  });
656
840
  // Remove task
657
- delete this.state.tasks[taskId];
841
+ delete state.tasks[taskId];
658
842
  await this.addHistory("task_removed", { taskId, taskTitle: task.title });
659
843
  await this.save();
660
844
  }
661
845
  async updateTaskDetails(taskId, updates) {
662
- if (!this.state)
663
- throw new Error("Project not initialized");
664
- const task = this.getTask(taskId);
665
- if (!task)
666
- throw new Error(`Task not found: ${taskId}`);
846
+ const task = this.requireTask(taskId);
667
847
  if (task.status === "done" || task.status === "cancelled") {
668
- throw new Error(`Cannot modify ${task.status} task`);
848
+ throw new TaskInvalidStateError(taskId, task.status, ["pending", "in_progress", "blocked"]);
669
849
  }
670
850
  const now = new Date().toISOString();
671
851
  if (updates.title !== undefined)
@@ -676,6 +856,29 @@ export class StateManager {
676
856
  task.priority = updates.priority;
677
857
  if (updates.execution_mode !== undefined)
678
858
  task.execution_mode = updates.execution_mode;
859
+ // Handle depends_on updates with circular dependency check
860
+ if (updates.depends_on !== undefined) {
861
+ // Validate all dependency IDs exist and are in the same staging
862
+ for (const depId of updates.depends_on) {
863
+ const depTask = this.getTask(depId);
864
+ if (!depTask) {
865
+ throw new TaskNotFoundError(depId);
866
+ }
867
+ if (depTask.stagingId !== task.stagingId) {
868
+ throw new TaskInvalidStateError(depId, "different staging", ["same staging as dependent task"]);
869
+ }
870
+ // Cannot depend on itself
871
+ if (depId === taskId) {
872
+ throw new CircularDependencyError(taskId, [taskId]);
873
+ }
874
+ }
875
+ // Check for circular dependencies
876
+ const circularChain = this.detectCircularDependency(taskId, updates.depends_on);
877
+ if (circularChain) {
878
+ throw new CircularDependencyError(taskId, circularChain);
879
+ }
880
+ task.depends_on = updates.depends_on;
881
+ }
679
882
  task.updatedAt = now;
680
883
  await this.addHistory("task_updated", { taskId, updates });
681
884
  await this.save();