@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.
- package/dist/errors/index.d.ts +28 -0
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +50 -0
- package/dist/errors/index.js.map +1 -1
- package/dist/state/manager.d.ts +33 -2
- package/dist/state/manager.d.ts.map +1 -1
- package/dist/state/manager.js +341 -138
- package/dist/state/manager.js.map +1 -1
- package/dist/state/manager.test.d.ts +2 -0
- package/dist/state/manager.test.d.ts.map +1 -0
- package/dist/state/manager.test.js +310 -0
- package/dist/state/manager.test.js.map +1 -0
- package/dist/state/schema.d.ts +190 -9
- package/dist/state/schema.d.ts.map +1 -1
- package/dist/state/schema.js +33 -1
- package/dist/state/schema.js.map +1 -1
- package/dist/state/schema.test.d.ts +2 -0
- package/dist/state/schema.test.d.ts.map +1 -0
- package/dist/state/schema.test.js +300 -0
- package/dist/state/schema.test.js.map +1 -0
- package/dist/state/types.d.ts +5 -1
- package/dist/state/types.d.ts.map +1 -1
- package/dist/state/types.js +3 -1
- package/dist/state/types.js.map +1 -1
- package/dist/tools/archive.d.ts.map +1 -1
- package/dist/tools/archive.js +57 -0
- package/dist/tools/archive.js.map +1 -1
- package/dist/tools/context.d.ts.map +1 -1
- package/dist/tools/context.js +14 -0
- package/dist/tools/context.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/memory.d.ts +7 -0
- package/dist/tools/memory.d.ts.map +1 -0
- package/dist/tools/memory.js +243 -0
- package/dist/tools/memory.js.map +1 -0
- package/dist/tools/modify.d.ts.map +1 -1
- package/dist/tools/modify.js +9 -6
- package/dist/tools/modify.js.map +1 -1
- package/dist/tools/plan.js +3 -3
- package/dist/tools/plan.js.map +1 -1
- package/dist/tools/staging.d.ts.map +1 -1
- package/dist/tools/staging.js +3 -3
- package/dist/tools/staging.js.map +1 -1
- package/dist/utils/paths.d.ts +34 -0
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +94 -2
- package/dist/utils/paths.js.map +1 -1
- package/package.json +10 -3
package/dist/state/manager.js
CHANGED
|
@@ -1,23 +1,29 @@
|
|
|
1
|
-
import { readFile,
|
|
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 {
|
|
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
|
-
|
|
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-${
|
|
17
|
-
generateStagingId: () => `staging-${
|
|
18
|
-
generateTaskId: () => `task-${
|
|
19
|
-
generateHistoryId: () => `hist-${Date.now()}-${
|
|
20
|
-
generateDecisionId: () => `dec-${Date.now()}-${
|
|
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
|
-
|
|
68
|
-
await
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
const
|
|
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
|
|
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
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
292
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
const
|
|
303
|
-
if (!
|
|
304
|
-
throw new
|
|
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.
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
348
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
|
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 (
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
408
|
-
|
|
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
|
|
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 (
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
532
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
572
|
-
|
|
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
|
|
738
|
+
throw new StagingInvalidStateError(stagingId, staging.status, ["pending", "completed", "cancelled"]);
|
|
578
739
|
}
|
|
579
|
-
const plan = this.
|
|
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
|
|
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
|
|
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
|
-
|
|
602
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
634
|
-
|
|
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
|
|
825
|
+
throw new TaskInvalidStateError(taskId, task.status, ["pending", "done", "blocked", "cancelled"]);
|
|
640
826
|
}
|
|
641
|
-
const staging = this.
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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();
|