overture-mcp 0.1.7 → 0.1.8

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.
@@ -209,6 +209,289 @@ var historyStorage = new HistoryStorage();
209
209
  // src/websocket/ws-server.ts
210
210
  import { WebSocketServer, WebSocket } from "ws";
211
211
 
212
+ // src/storage/project-storage.ts
213
+ import fs2 from "fs/promises";
214
+ import path2 from "path";
215
+ var MAX_PROJECT_HISTORY_ENTRIES = 50;
216
+ var ProjectStorage = class {
217
+ workspacePath;
218
+ filePath;
219
+ projectId;
220
+ cache = null;
221
+ writeDebounceTimer = null;
222
+ writePromise = null;
223
+ writePermissionDenied = false;
224
+ constructor(workspacePath, projectId) {
225
+ this.workspacePath = workspacePath;
226
+ this.projectId = projectId;
227
+ this.filePath = path2.join(workspacePath, ".overture.json");
228
+ }
229
+ /**
230
+ * Get the storage file path
231
+ */
232
+ getFilePath() {
233
+ return this.filePath;
234
+ }
235
+ /**
236
+ * Check if write permission was denied (should fall back to global storage)
237
+ */
238
+ isWritePermissionDenied() {
239
+ return this.writePermissionDenied;
240
+ }
241
+ /**
242
+ * Check if project storage file exists
243
+ */
244
+ async exists() {
245
+ try {
246
+ await fs2.access(this.filePath);
247
+ return true;
248
+ } catch {
249
+ return false;
250
+ }
251
+ }
252
+ /**
253
+ * Load project configuration from disk (with caching)
254
+ */
255
+ async load() {
256
+ if (this.cache) return this.cache;
257
+ try {
258
+ const data = await fs2.readFile(this.filePath, "utf-8");
259
+ const parsed = JSON.parse(data);
260
+ if (!parsed || typeof parsed.version !== "number") {
261
+ console.error("[Overture] Invalid project config format, creating new one");
262
+ this.cache = this.createEmptyConfig();
263
+ return this.cache;
264
+ }
265
+ this.cache = parsed;
266
+ console.error(`[Overture] Loaded project config: ${this.cache.history.length} history entries`);
267
+ return this.cache;
268
+ } catch (error) {
269
+ if (error.code === "ENOENT") {
270
+ console.error("[Overture] Project config does not exist, creating new one");
271
+ this.cache = this.createEmptyConfig();
272
+ return this.cache;
273
+ }
274
+ if (error.code === "EACCES") {
275
+ console.error("[Overture] Permission denied reading project config, will use global storage");
276
+ this.writePermissionDenied = true;
277
+ this.cache = this.createEmptyConfig();
278
+ return this.cache;
279
+ }
280
+ console.error("[Overture] Error loading project config:", error.message);
281
+ this.cache = this.createEmptyConfig();
282
+ return this.cache;
283
+ }
284
+ }
285
+ /**
286
+ * Create an empty project configuration
287
+ */
288
+ createEmptyConfig() {
289
+ return {
290
+ version: 1,
291
+ projectId: this.projectId,
292
+ history: []
293
+ };
294
+ }
295
+ /**
296
+ * Save project configuration to disk (debounced)
297
+ */
298
+ async save() {
299
+ if (this.writePermissionDenied) {
300
+ console.error("[Overture] Write permission denied, skipping project storage save");
301
+ return;
302
+ }
303
+ if (this.writeDebounceTimer) {
304
+ clearTimeout(this.writeDebounceTimer);
305
+ }
306
+ if (this.writePromise) {
307
+ return this.writePromise;
308
+ }
309
+ this.writePromise = new Promise((resolve, reject) => {
310
+ this.writeDebounceTimer = setTimeout(async () => {
311
+ try {
312
+ if (!this.cache) {
313
+ resolve();
314
+ return;
315
+ }
316
+ await fs2.writeFile(this.filePath, JSON.stringify(this.cache, null, 2));
317
+ console.error("[Overture] Project config saved to", this.filePath);
318
+ resolve();
319
+ } catch (error) {
320
+ if (error.code === "EACCES") {
321
+ console.error("[Overture] Permission denied writing project config, will use global storage");
322
+ this.writePermissionDenied = true;
323
+ resolve();
324
+ } else {
325
+ console.error("[Overture] Failed to save project config:", error);
326
+ reject(error);
327
+ }
328
+ } finally {
329
+ this.writePromise = null;
330
+ }
331
+ }, 1e3);
332
+ });
333
+ return this.writePromise;
334
+ }
335
+ /**
336
+ * Force immediate save (bypass debounce)
337
+ */
338
+ async saveNow() {
339
+ if (this.writePermissionDenied) {
340
+ console.error("[Overture] Write permission denied, skipping project storage save");
341
+ return;
342
+ }
343
+ if (this.writeDebounceTimer) {
344
+ clearTimeout(this.writeDebounceTimer);
345
+ this.writeDebounceTimer = null;
346
+ }
347
+ if (!this.cache) return;
348
+ try {
349
+ await fs2.writeFile(this.filePath, JSON.stringify(this.cache, null, 2));
350
+ console.error("[Overture] Project config saved (immediate) to", this.filePath);
351
+ } catch (error) {
352
+ if (error.code === "EACCES") {
353
+ console.error("[Overture] Permission denied writing project config");
354
+ this.writePermissionDenied = true;
355
+ } else {
356
+ console.error("[Overture] Failed to save project config:", error);
357
+ throw error;
358
+ }
359
+ }
360
+ }
361
+ /**
362
+ * Add or update a plan in history
363
+ */
364
+ async addPlanToHistory(plan) {
365
+ const config = await this.load();
366
+ const existingIndex = config.history.findIndex((p) => p.plan.id === plan.plan.id);
367
+ if (existingIndex >= 0) {
368
+ config.history[existingIndex] = plan;
369
+ } else {
370
+ config.history.unshift(plan);
371
+ }
372
+ if (config.history.length > MAX_PROJECT_HISTORY_ENTRIES) {
373
+ const removed = config.history.splice(MAX_PROJECT_HISTORY_ENTRIES);
374
+ console.error(`[Overture] Pruned ${removed.length} old project history entries`);
375
+ }
376
+ await this.save();
377
+ }
378
+ /**
379
+ * Get all history entries for this project
380
+ */
381
+ async getHistory() {
382
+ const config = await this.load();
383
+ return config.history;
384
+ }
385
+ /**
386
+ * Get history entries as lightweight HistoryEntry objects
387
+ */
388
+ async getHistoryEntries() {
389
+ const config = await this.load();
390
+ return config.history.map((persisted) => ({
391
+ id: persisted.plan.id,
392
+ projectId: persisted.plan.projectId,
393
+ workspacePath: persisted.plan.workspacePath,
394
+ projectName: path2.basename(persisted.plan.workspacePath),
395
+ title: persisted.plan.title,
396
+ agent: persisted.plan.agent,
397
+ status: persisted.plan.status,
398
+ createdAt: persisted.plan.createdAt,
399
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
400
+ // We don't store updatedAt per plan, so use now
401
+ nodeCount: persisted.nodes.length,
402
+ completedNodeCount: persisted.nodes.filter((n) => n.status === "completed").length
403
+ }));
404
+ }
405
+ /**
406
+ * Get a specific plan by ID
407
+ */
408
+ async getPlan(planId) {
409
+ const config = await this.load();
410
+ return config.history.find((p) => p.plan.id === planId) || null;
411
+ }
412
+ /**
413
+ * Delete a plan from history
414
+ */
415
+ async deletePlan(planId) {
416
+ const config = await this.load();
417
+ config.history = config.history.filter((p) => p.plan.id !== planId);
418
+ await this.save();
419
+ }
420
+ /**
421
+ * Get project settings
422
+ */
423
+ async getSettings() {
424
+ const config = await this.load();
425
+ return config.settings;
426
+ }
427
+ /**
428
+ * Update project settings
429
+ */
430
+ async updateSettings(settings) {
431
+ const config = await this.load();
432
+ config.settings = {
433
+ ...config.settings,
434
+ ...settings
435
+ };
436
+ await this.save();
437
+ }
438
+ /**
439
+ * Clear all history
440
+ */
441
+ async clearHistory() {
442
+ const config = await this.load();
443
+ config.history = [];
444
+ await this.saveNow();
445
+ }
446
+ /**
447
+ * Invalidate cache (force reload from disk on next access)
448
+ */
449
+ invalidateCache() {
450
+ this.cache = null;
451
+ }
452
+ };
453
+ var ProjectStorageRegistry = class {
454
+ storages = /* @__PURE__ */ new Map();
455
+ /**
456
+ * Get or create a ProjectStorage instance for a workspace path
457
+ */
458
+ getStorage(workspacePath, projectId) {
459
+ const key = workspacePath;
460
+ let storage = this.storages.get(key);
461
+ if (!storage) {
462
+ storage = new ProjectStorage(workspacePath, projectId);
463
+ this.storages.set(key, storage);
464
+ console.error(`[Overture] Created project storage for: ${workspacePath}`);
465
+ }
466
+ return storage;
467
+ }
468
+ /**
469
+ * Check if a project has local storage
470
+ */
471
+ hasStorage(workspacePath) {
472
+ return this.storages.has(workspacePath);
473
+ }
474
+ /**
475
+ * Remove storage instance from registry
476
+ */
477
+ removeStorage(workspacePath) {
478
+ this.storages.delete(workspacePath);
479
+ }
480
+ /**
481
+ * Get all active storage instances
482
+ */
483
+ getAllStorages() {
484
+ return Array.from(this.storages.values());
485
+ }
486
+ /**
487
+ * Clear all storage instances
488
+ */
489
+ clear() {
490
+ this.storages.clear();
491
+ }
492
+ };
493
+ var projectStorageRegistry = new ProjectStorageRegistry();
494
+
212
495
  // src/utils/plan-diff.ts
213
496
  function calculatePlanDiff(oldPlan, newPlan) {
214
497
  const oldNodeMap = new Map(oldPlan.nodes.map((n) => [n.id, n]));
@@ -272,7 +555,7 @@ function nodesAreEqual(a, b) {
272
555
  }
273
556
 
274
557
  // src/store/plan-store.ts
275
- import path2 from "path";
558
+ import path3 from "path";
276
559
  var DEFAULT_PROJECT_ID = "default";
277
560
  var MultiProjectPlanStore = class {
278
561
  projects = /* @__PURE__ */ new Map();
@@ -292,6 +575,21 @@ var MultiProjectPlanStore = class {
292
575
  constructor() {
293
576
  this.startAutoSave();
294
577
  }
578
+ /**
579
+ * Get project storage for a project, or null if it should use global storage
580
+ * Uses project-local .overture.json if workspace path is available
581
+ */
582
+ getProjectStorage(projectId) {
583
+ const state = this.projects.get(projectId);
584
+ if (!state || !state.workspacePath || state.workspacePath === process.cwd()) {
585
+ return null;
586
+ }
587
+ const storage = projectStorageRegistry.getStorage(state.workspacePath, projectId);
588
+ if (storage.isWritePermissionDenied()) {
589
+ return null;
590
+ }
591
+ return storage;
592
+ }
295
593
  /**
296
594
  * Start auto-save interval to persist all active projects every 3 seconds
297
595
  */
@@ -311,9 +609,16 @@ var MultiProjectPlanStore = class {
311
609
  selectedBranches: state.selectedBranches,
312
610
  nodeConfigs: state.nodeConfigs
313
611
  };
314
- await historyStorage.savePlan(persisted);
315
- await historyStorage.saveNow();
316
- console.error(`[Overture] Auto-saved project ${projectId} (plan: ${state.plan.id})`);
612
+ const projectStorage = this.getProjectStorage(projectId);
613
+ if (projectStorage) {
614
+ await projectStorage.addPlanToHistory(persisted);
615
+ await projectStorage.saveNow();
616
+ console.error(`[Overture] Auto-saved project ${projectId} to project storage (plan: ${state.plan.id})`);
617
+ } else {
618
+ await historyStorage.savePlan(persisted);
619
+ await historyStorage.saveNow();
620
+ console.error(`[Overture] Auto-saved project ${projectId} to global storage (plan: ${state.plan.id})`);
621
+ }
317
622
  } catch (error) {
318
623
  console.error(`[Overture] Auto-save failed for project ${projectId}:`, error);
319
624
  }
@@ -354,7 +659,7 @@ var MultiProjectPlanStore = class {
354
659
  contexts.push({
355
660
  projectId,
356
661
  workspacePath: state.workspacePath,
357
- projectName: path2.basename(state.workspacePath),
662
+ projectName: path3.basename(state.workspacePath),
358
663
  agentType: state.plan?.agent || "unknown"
359
664
  });
360
665
  }
@@ -464,6 +769,19 @@ var MultiProjectPlanStore = class {
464
769
  this.persistToHistory(projectId);
465
770
  }
466
771
  }
772
+ updatePlanSettings(projectId, planId, settings) {
773
+ const state = this.projects.get(projectId);
774
+ if (!state?.plan || state.plan.id !== planId) return false;
775
+ if (settings.model !== void 0) {
776
+ state.plan.model = settings.model || void 0;
777
+ }
778
+ if (settings.provider !== void 0) {
779
+ state.plan.provider = settings.provider || void 0;
780
+ }
781
+ this.persistToHistory(projectId);
782
+ console.error(`[Overture] Plan settings updated for ${planId}: model=${settings.model}, provider=${settings.provider}`);
783
+ return true;
784
+ }
467
785
  updateNodeStatus(projectId, nodeId, status, output, structuredOutput) {
468
786
  const state = this.projects.get(projectId);
469
787
  if (!state) return;
@@ -479,6 +797,17 @@ var MultiProjectPlanStore = class {
479
797
  this.persistToHistory(projectId);
480
798
  }
481
799
  }
800
+ updateNodeDescription(projectId, nodeId, description) {
801
+ const state = this.projects.get(projectId);
802
+ if (!state) return false;
803
+ const node = state.nodes.find((n) => n.id === nodeId);
804
+ if (node) {
805
+ node.description = description;
806
+ this.persistToHistory(projectId);
807
+ return true;
808
+ }
809
+ return false;
810
+ }
482
811
  // === Approval ===
483
812
  async setApproval(projectId, fieldValues, selectedBranches, nodeConfigs = {}) {
484
813
  console.error(`[Overture] setApproval called for project: ${projectId}`);
@@ -679,6 +1008,45 @@ var MultiProjectPlanStore = class {
679
1008
  }
680
1009
  return { removedEdgeIds, reconnectionEdges };
681
1010
  }
1011
+ /**
1012
+ * Insert nodes BEFORE a reference node.
1013
+ * Used when inserting before the first node in a plan.
1014
+ */
1015
+ insertNodesBefore(projectId, beforeNodeId, newNodes, newEdges) {
1016
+ const state = this.projects.get(projectId);
1017
+ if (!state) return { removedEdgeIds: [], allEdges: [] };
1018
+ const incomingEdges = state.edges.filter((e) => e.to === beforeNodeId);
1019
+ const removedEdgeIds = incomingEdges.map((e) => e.id);
1020
+ state.edges = state.edges.filter((e) => e.to !== beforeNodeId);
1021
+ state.nodes.push(...newNodes);
1022
+ state.edges.push(...newEdges);
1023
+ const newNodeIds = new Set(newNodes.map((n) => n.id));
1024
+ const entryNodeIds = newNodes.filter((n) => !newEdges.some((e) => e.to === n.id && newNodeIds.has(e.from))).map((n) => n.id);
1025
+ const exitNodeIds = newNodes.filter((n) => !newEdges.some((e) => e.from === n.id && newNodeIds.has(e.to))).map((n) => n.id);
1026
+ const allNewEdges = [...newEdges];
1027
+ let edgeCounter = Date.now();
1028
+ for (const incomingEdge of incomingEdges) {
1029
+ for (const entryNodeId of entryNodeIds) {
1030
+ const reconnectEdge = {
1031
+ id: `e_inserted_${edgeCounter++}`,
1032
+ from: incomingEdge.from,
1033
+ to: entryNodeId
1034
+ };
1035
+ state.edges.push(reconnectEdge);
1036
+ allNewEdges.push(reconnectEdge);
1037
+ }
1038
+ }
1039
+ for (const exitNodeId of exitNodeIds) {
1040
+ const exitEdge = {
1041
+ id: `e_inserted_${edgeCounter++}`,
1042
+ from: exitNodeId,
1043
+ to: beforeNodeId
1044
+ };
1045
+ state.edges.push(exitEdge);
1046
+ allNewEdges.push(exitEdge);
1047
+ }
1048
+ return { removedEdgeIds, allEdges: allNewEdges };
1049
+ }
682
1050
  removeNode(projectId, nodeId) {
683
1051
  const state = this.projects.get(projectId);
684
1052
  if (!state) return { newEdges: [], removedEdgeIds: [] };
@@ -767,7 +1135,8 @@ var MultiProjectPlanStore = class {
767
1135
  // === History/Persistence ===
768
1136
  /**
769
1137
  * Persist current project state to history
770
- * Uses immediate write to ensure data is not lost
1138
+ * Uses project-local storage (.overture.json) if workspace path is available,
1139
+ * otherwise falls back to global storage (~/.overture/history.json)
771
1140
  */
772
1141
  async persistToHistory(projectId) {
773
1142
  const state = this.projects.get(projectId);
@@ -781,9 +1150,16 @@ var MultiProjectPlanStore = class {
781
1150
  selectedBranches: state.selectedBranches,
782
1151
  nodeConfigs: state.nodeConfigs
783
1152
  };
784
- await historyStorage.savePlan(persisted);
785
- await historyStorage.saveNow();
786
- console.error(`[Overture] Persisted to history: ${state.plan.id} (${state.nodes.length} nodes)`);
1153
+ const projectStorage = this.getProjectStorage(projectId);
1154
+ if (projectStorage) {
1155
+ await projectStorage.addPlanToHistory(persisted);
1156
+ await projectStorage.saveNow();
1157
+ console.error(`[Overture] Persisted to project storage: ${state.plan.id} (${state.nodes.length} nodes)`);
1158
+ } else {
1159
+ await historyStorage.savePlan(persisted);
1160
+ await historyStorage.saveNow();
1161
+ console.error(`[Overture] Persisted to global storage: ${state.plan.id} (${state.nodes.length} nodes)`);
1162
+ }
787
1163
  } catch (error) {
788
1164
  console.error("[Overture] Failed to persist plan to history:", error);
789
1165
  }
@@ -808,20 +1184,62 @@ var MultiProjectPlanStore = class {
808
1184
  selectedBranches: state.selectedBranches,
809
1185
  nodeConfigs: state.nodeConfigs
810
1186
  };
811
- await historyStorage.savePlan(persisted);
812
- await historyStorage.saveNow();
813
- console.error(`[Overture] Plan ${state.plan.id} force-persisted to history`);
814
- return { success: true, planId: state.plan.id };
1187
+ const projectStorage = this.getProjectStorage(projectId);
1188
+ if (projectStorage) {
1189
+ await projectStorage.addPlanToHistory(persisted);
1190
+ await projectStorage.saveNow();
1191
+ console.error(`[Overture] Plan ${state.plan.id} force-persisted to project storage`);
1192
+ return { success: true, planId: state.plan.id, storageType: "project" };
1193
+ } else {
1194
+ await historyStorage.savePlan(persisted);
1195
+ await historyStorage.saveNow();
1196
+ console.error(`[Overture] Plan ${state.plan.id} force-persisted to global storage`);
1197
+ return { success: true, planId: state.plan.id, storageType: "global" };
1198
+ }
815
1199
  } catch (error) {
816
1200
  console.error("[Overture] Failed to force persist plan:", error);
817
1201
  return { success: false };
818
1202
  }
819
1203
  }
1204
+ /**
1205
+ * Load a plan from a PersistedPlan object directly into the store
1206
+ * Used when loading from project storage
1207
+ */
1208
+ async loadFromPersistedPlan(persisted) {
1209
+ const state = {
1210
+ projectId: persisted.plan.projectId,
1211
+ workspacePath: persisted.plan.workspacePath,
1212
+ plan: persisted.plan,
1213
+ nodes: persisted.nodes,
1214
+ edges: persisted.edges,
1215
+ fieldValues: persisted.fieldValues,
1216
+ selectedBranches: persisted.selectedBranches,
1217
+ nodeConfigs: persisted.nodeConfigs
1218
+ };
1219
+ this.projects.set(state.projectId, state);
1220
+ console.error(`[Overture] Loaded plan from PersistedPlan: ${persisted.plan.id}`);
1221
+ return state;
1222
+ }
820
1223
  /**
821
1224
  * Load a plan from history into a project
1225
+ * Checks both project-local storage and global storage
822
1226
  */
823
- async loadFromHistory(planId) {
824
- const persisted = await historyStorage.getPlan(planId);
1227
+ async loadFromHistory(planId, workspacePath) {
1228
+ let persisted = null;
1229
+ if (workspacePath) {
1230
+ const tempProjectId = "temp_lookup";
1231
+ const projectStorage = projectStorageRegistry.getStorage(workspacePath, tempProjectId);
1232
+ persisted = await projectStorage.getPlan(planId);
1233
+ if (persisted) {
1234
+ console.error(`[Overture] Loaded plan ${planId} from project storage`);
1235
+ }
1236
+ }
1237
+ if (!persisted) {
1238
+ persisted = await historyStorage.getPlan(planId);
1239
+ if (persisted) {
1240
+ console.error(`[Overture] Loaded plan ${planId} from global storage`);
1241
+ }
1242
+ }
825
1243
  if (!persisted) return null;
826
1244
  const state = {
827
1245
  projectId: persisted.plan.projectId,
@@ -839,19 +1257,33 @@ var MultiProjectPlanStore = class {
839
1257
  /**
840
1258
  * Restore a project from history by projectId
841
1259
  * Finds the most recent plan for this project and loads it
1260
+ * Checks both project-local storage and global storage
842
1261
  */
843
- async restoreProjectFromHistory(projectId) {
1262
+ async restoreProjectFromHistory(projectId, workspacePath) {
844
1263
  try {
845
- const entries = await historyStorage.getEntriesByProject(projectId);
846
- if (entries.length === 0) {
847
- console.error(`[Overture] No history entries found for project ${projectId}`);
848
- return false;
1264
+ let entries = [];
1265
+ let persisted = null;
1266
+ if (workspacePath) {
1267
+ const projectStorage = projectStorageRegistry.getStorage(workspacePath, projectId);
1268
+ entries = await projectStorage.getHistoryEntries();
1269
+ if (entries.length > 0) {
1270
+ const mostRecent = entries[0];
1271
+ console.error(`[Overture] Found project storage entry: ${mostRecent.title} (${mostRecent.id})`);
1272
+ persisted = await projectStorage.getPlan(mostRecent.id);
1273
+ }
1274
+ }
1275
+ if (!persisted) {
1276
+ entries = await historyStorage.getEntriesByProject(projectId);
1277
+ if (entries.length === 0) {
1278
+ console.error(`[Overture] No history entries found for project ${projectId}`);
1279
+ return false;
1280
+ }
1281
+ const mostRecent = entries[0];
1282
+ console.error(`[Overture] Found global storage entry: ${mostRecent.title} (${mostRecent.id})`);
1283
+ persisted = await historyStorage.getPlan(mostRecent.id);
849
1284
  }
850
- const mostRecent = entries[0];
851
- console.error(`[Overture] Found history entry: ${mostRecent.title} (${mostRecent.id})`);
852
- const persisted = await historyStorage.getPlan(mostRecent.id);
853
1285
  if (!persisted) {
854
- console.error(`[Overture] Could not load plan data for ${mostRecent.id}`);
1286
+ console.error(`[Overture] Could not load plan data for project ${projectId}`);
855
1287
  return false;
856
1288
  }
857
1289
  const state = {
@@ -877,6 +1309,89 @@ var MultiProjectPlanStore = class {
877
1309
  return false;
878
1310
  }
879
1311
  }
1312
+ /**
1313
+ * Get all history entries for a project
1314
+ * Combines entries from project-local storage and global storage
1315
+ */
1316
+ async getProjectHistory(projectId, workspacePath) {
1317
+ const allEntries = [];
1318
+ const seenIds = /* @__PURE__ */ new Set();
1319
+ if (workspacePath) {
1320
+ try {
1321
+ const projectStorage = projectStorageRegistry.getStorage(workspacePath, projectId);
1322
+ const projectEntries = await projectStorage.getHistoryEntries();
1323
+ for (const entry of projectEntries) {
1324
+ if (!seenIds.has(entry.id)) {
1325
+ seenIds.add(entry.id);
1326
+ allEntries.push(entry);
1327
+ }
1328
+ }
1329
+ console.error(`[Overture] Found ${projectEntries.length} entries in project storage`);
1330
+ } catch (error) {
1331
+ console.error("[Overture] Failed to get project storage history:", error);
1332
+ }
1333
+ }
1334
+ try {
1335
+ const globalEntries = await historyStorage.getEntriesByProject(projectId);
1336
+ for (const entry of globalEntries) {
1337
+ if (!seenIds.has(entry.id)) {
1338
+ seenIds.add(entry.id);
1339
+ allEntries.push(entry);
1340
+ }
1341
+ }
1342
+ console.error(`[Overture] Found ${globalEntries.length} entries in global storage`);
1343
+ } catch (error) {
1344
+ console.error("[Overture] Failed to get global storage history:", error);
1345
+ }
1346
+ allEntries.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
1347
+ return allEntries;
1348
+ }
1349
+ /**
1350
+ * Get all history entries grouped by project
1351
+ * Combines entries from all project storages and global storage
1352
+ */
1353
+ async getAllHistoryGroupedByProject() {
1354
+ const grouped = /* @__PURE__ */ new Map();
1355
+ for (const storage of projectStorageRegistry.getAllStorages()) {
1356
+ try {
1357
+ const entries = await storage.getHistoryEntries();
1358
+ for (const entry of entries) {
1359
+ if (!grouped.has(entry.projectId)) {
1360
+ grouped.set(entry.projectId, {
1361
+ projectName: entry.projectName,
1362
+ workspacePath: entry.workspacePath,
1363
+ entries: []
1364
+ });
1365
+ }
1366
+ grouped.get(entry.projectId).entries.push(entry);
1367
+ }
1368
+ } catch (error) {
1369
+ console.error("[Overture] Failed to get project storage history:", error);
1370
+ }
1371
+ }
1372
+ try {
1373
+ const globalEntries = await historyStorage.getAllEntries();
1374
+ for (const entry of globalEntries) {
1375
+ if (!grouped.has(entry.projectId)) {
1376
+ grouped.set(entry.projectId, {
1377
+ projectName: entry.projectName,
1378
+ workspacePath: entry.workspacePath,
1379
+ entries: []
1380
+ });
1381
+ }
1382
+ const group = grouped.get(entry.projectId);
1383
+ if (!group.entries.some((e) => e.id === entry.id)) {
1384
+ group.entries.push(entry);
1385
+ }
1386
+ }
1387
+ } catch (error) {
1388
+ console.error("[Overture] Failed to get global storage history:", error);
1389
+ }
1390
+ for (const group of grouped.values()) {
1391
+ group.entries.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
1392
+ }
1393
+ return grouped;
1394
+ }
880
1395
  // === Resume Info ===
881
1396
  /**
882
1397
  * Generate resume info for a paused/failed plan
@@ -1062,6 +1577,49 @@ var LegacyPlanStore = class {
1062
1577
  };
1063
1578
  var planStore = new LegacyPlanStore();
1064
1579
 
1580
+ // src/store/settings-store.ts
1581
+ var DEFAULT_SETTINGS = {
1582
+ minNodesPerPlan: 1
1583
+ };
1584
+ var MIN_NODES_MIN = 1;
1585
+ var MIN_NODES_MAX = 20;
1586
+ var SettingsStore = class {
1587
+ settings = { ...DEFAULT_SETTINGS };
1588
+ /**
1589
+ * Get current settings
1590
+ */
1591
+ getSettings() {
1592
+ return { ...this.settings };
1593
+ }
1594
+ /**
1595
+ * Get minimum nodes per plan setting
1596
+ */
1597
+ getMinNodesPerPlan() {
1598
+ return this.settings.minNodesPerPlan;
1599
+ }
1600
+ /**
1601
+ * Update settings from UI
1602
+ */
1603
+ updateSettings(newSettings) {
1604
+ if (newSettings.minNodesPerPlan !== void 0) {
1605
+ const value = Math.min(
1606
+ Math.max(newSettings.minNodesPerPlan, MIN_NODES_MIN),
1607
+ MIN_NODES_MAX
1608
+ );
1609
+ this.settings.minNodesPerPlan = value;
1610
+ console.error(`[Overture] Settings updated: minNodesPerPlan = ${value}`);
1611
+ }
1612
+ }
1613
+ /**
1614
+ * Reset settings to defaults
1615
+ */
1616
+ reset() {
1617
+ this.settings = { ...DEFAULT_SETTINGS };
1618
+ console.error("[Overture] Settings reset to defaults");
1619
+ }
1620
+ };
1621
+ var settingsStore = new SettingsStore();
1622
+
1065
1623
  // src/websocket/ws-server.ts
1066
1624
  var WebSocketManager = class {
1067
1625
  wss = null;
@@ -1215,7 +1773,25 @@ var WebSocketManager = class {
1215
1773
  case "get_history": {
1216
1774
  console.error("[Overture] History requested");
1217
1775
  let entries;
1218
- if (message.projectId) {
1776
+ if (message.workspacePath && message.projectId) {
1777
+ const projectStorage = projectStorageRegistry.getStorage(message.workspacePath, message.projectId);
1778
+ if (projectStorage.isWritePermissionDenied()) {
1779
+ console.error("[Overture] Project storage permission denied, using global storage");
1780
+ entries = await historyStorage.getEntriesByProject(message.projectId);
1781
+ } else {
1782
+ const projectEntries = await projectStorage.getHistoryEntries();
1783
+ const globalEntries = await historyStorage.getEntriesByProject(message.projectId);
1784
+ const entryMap = /* @__PURE__ */ new Map();
1785
+ for (const entry of globalEntries) {
1786
+ entryMap.set(entry.id, entry);
1787
+ }
1788
+ for (const entry of projectEntries) {
1789
+ entryMap.set(entry.id, entry);
1790
+ }
1791
+ entries = Array.from(entryMap.values());
1792
+ entries.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
1793
+ }
1794
+ } else if (message.projectId) {
1219
1795
  entries = await historyStorage.getEntriesByProject(message.projectId);
1220
1796
  } else {
1221
1797
  entries = await historyStorage.getAllEntries();
@@ -1225,7 +1801,19 @@ var WebSocketManager = class {
1225
1801
  }
1226
1802
  case "load_plan": {
1227
1803
  console.error(`[Overture] Loading plan from history: ${message.planId}`);
1228
- const state = await multiProjectPlanStore.loadFromHistory(message.planId);
1804
+ let state = null;
1805
+ if (message.workspacePath && message.projectId) {
1806
+ const projectStorage = projectStorageRegistry.getStorage(message.workspacePath, message.projectId);
1807
+ if (!projectStorage.isWritePermissionDenied()) {
1808
+ const persistedPlan = await projectStorage.getPlan(message.planId);
1809
+ if (persistedPlan) {
1810
+ state = await multiProjectPlanStore.loadFromPersistedPlan(persistedPlan);
1811
+ }
1812
+ }
1813
+ }
1814
+ if (!state) {
1815
+ state = await multiProjectPlanStore.loadFromHistory(message.planId);
1816
+ }
1229
1817
  if (state?.plan) {
1230
1818
  const client = this.clients.get(ws);
1231
1819
  if (client) {
@@ -1341,16 +1929,30 @@ var WebSocketManager = class {
1341
1929
  this.broadcastToProject(projectId, { type: "plan_resumed", projectId });
1342
1930
  break;
1343
1931
  case "insert_nodes": {
1344
- console.error(`[Overture] Inserting ${message.nodes.length} node(s) after ${message.afterNodeId} (project: ${projectId})`);
1345
- const insertResult = multiProjectPlanStore.insertNodes(projectId, message.afterNodeId, message.nodes, message.edges);
1346
- const allEdges = [...message.edges, ...insertResult.reconnectionEdges];
1347
- this.broadcastToProject(projectId, {
1348
- type: "nodes_inserted",
1349
- nodes: message.nodes,
1350
- edges: allEdges,
1351
- removedEdgeIds: insertResult.removedEdgeIds,
1352
- projectId
1353
- });
1932
+ if (message.afterNodeId) {
1933
+ console.error(`[Overture] Inserting ${message.nodes.length} node(s) after ${message.afterNodeId} (project: ${projectId})`);
1934
+ const insertResult = multiProjectPlanStore.insertNodes(projectId, message.afterNodeId, message.nodes, message.edges);
1935
+ const allEdges = [...message.edges, ...insertResult.reconnectionEdges];
1936
+ this.broadcastToProject(projectId, {
1937
+ type: "nodes_inserted",
1938
+ nodes: message.nodes,
1939
+ edges: allEdges,
1940
+ removedEdgeIds: insertResult.removedEdgeIds,
1941
+ projectId
1942
+ });
1943
+ } else if (message.beforeNodeId) {
1944
+ console.error(`[Overture] Inserting ${message.nodes.length} node(s) before ${message.beforeNodeId} (project: ${projectId})`);
1945
+ const insertResult = multiProjectPlanStore.insertNodesBefore(projectId, message.beforeNodeId, message.nodes, message.edges);
1946
+ this.broadcastToProject(projectId, {
1947
+ type: "nodes_inserted",
1948
+ nodes: message.nodes,
1949
+ edges: insertResult.allEdges,
1950
+ removedEdgeIds: insertResult.removedEdgeIds,
1951
+ projectId
1952
+ });
1953
+ } else {
1954
+ console.error(`[Overture] insert_nodes called without afterNodeId or beforeNodeId`);
1955
+ }
1354
1956
  break;
1355
1957
  }
1356
1958
  case "remove_node": {
@@ -1388,6 +1990,48 @@ var WebSocketManager = class {
1388
1990
  });
1389
1991
  break;
1390
1992
  }
1993
+ case "update_node_description": {
1994
+ const effectiveProjectId = message.projectId || projectId;
1995
+ console.error(`[Overture] Updating node description: ${message.nodeId} (project: ${effectiveProjectId})`);
1996
+ const success = multiProjectPlanStore.updateNodeDescription(
1997
+ effectiveProjectId,
1998
+ message.nodeId,
1999
+ message.description
2000
+ );
2001
+ if (success) {
2002
+ this.broadcastToProject(effectiveProjectId, {
2003
+ type: "node_description_updated",
2004
+ nodeId: message.nodeId,
2005
+ description: message.description,
2006
+ projectId: effectiveProjectId
2007
+ });
2008
+ }
2009
+ break;
2010
+ }
2011
+ case "sync_settings": {
2012
+ console.error("[Overture] Received settings sync:", message.settings);
2013
+ settingsStore.updateSettings(message.settings);
2014
+ break;
2015
+ }
2016
+ case "update_plan_settings": {
2017
+ const effectiveProjectId = message.projectId || projectId;
2018
+ console.error(`[Overture] Updating plan settings for plan: ${message.planId} (project: ${effectiveProjectId})`);
2019
+ const success = multiProjectPlanStore.updatePlanSettings(
2020
+ effectiveProjectId,
2021
+ message.planId,
2022
+ { model: message.model, provider: message.provider }
2023
+ );
2024
+ if (success) {
2025
+ this.broadcastToProject(effectiveProjectId, {
2026
+ type: "plan_settings_updated",
2027
+ planId: message.planId,
2028
+ model: message.model,
2029
+ provider: message.provider,
2030
+ projectId: effectiveProjectId
2031
+ });
2032
+ }
2033
+ break;
2034
+ }
1391
2035
  }
1392
2036
  }
1393
2037
  /**
@@ -1564,8 +2208,8 @@ var wsManager = new WebSocketManager();
1564
2208
 
1565
2209
  // src/tools/handlers.ts
1566
2210
  import { createHash } from "crypto";
1567
- import path3 from "path";
1568
- import fs2 from "fs/promises";
2211
+ import path4 from "path";
2212
+ import fs3 from "fs/promises";
1569
2213
  import { fileURLToPath } from "url";
1570
2214
 
1571
2215
  // src/parser/xml-parser.ts
@@ -1609,7 +2253,9 @@ var StreamingXMLParser = class {
1609
2253
  title: tag.attributes.title || "Untitled Plan",
1610
2254
  agent: tag.attributes.agent || "unknown",
1611
2255
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1612
- status: "streaming"
2256
+ status: "streaming",
2257
+ model: tag.attributes.model,
2258
+ provider: tag.attributes.provider
1613
2259
  };
1614
2260
  this.callback({ type: "plan", plan: this.state.plan });
1615
2261
  break;
@@ -1778,6 +2424,38 @@ var StreamingXMLParser = class {
1778
2424
 
1779
2425
  // src/parser/output-parser.ts
1780
2426
  import sax2 from "sax";
2427
+ function normalizeDiffContent(content) {
2428
+ if (!content) return "";
2429
+ let normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
2430
+ let lines = normalized.split("\n");
2431
+ while (lines.length > 0 && lines[0].trim() === "") {
2432
+ lines.shift();
2433
+ }
2434
+ while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
2435
+ lines.pop();
2436
+ }
2437
+ if (lines.length === 0) return "";
2438
+ const nonDiffLines = lines.filter(
2439
+ (line) => line.trim() !== "" && !/^[+\-@]/.test(line.trimStart())
2440
+ );
2441
+ let commonIndent = Infinity;
2442
+ for (const line of nonDiffLines) {
2443
+ const match = line.match(/^(\s*)/);
2444
+ if (match) {
2445
+ commonIndent = Math.min(commonIndent, match[1].length);
2446
+ }
2447
+ }
2448
+ if (commonIndent > 0 && commonIndent !== Infinity && commonIndent <= 8) {
2449
+ lines = lines.map((line) => {
2450
+ if (line.trim() === "") return "";
2451
+ if (line.length >= commonIndent) {
2452
+ return line.slice(commonIndent);
2453
+ }
2454
+ return line;
2455
+ });
2456
+ }
2457
+ return lines.join("\n");
2458
+ }
1781
2459
  function parseStructuredOutput(output) {
1782
2460
  const startTag = "<execution_output>";
1783
2461
  const endTag = "</execution_output>";
@@ -1905,7 +2583,12 @@ function parseStructuredOutput(output) {
1905
2583
  break;
1906
2584
  case "diff":
1907
2585
  if (state.currentFile) {
1908
- state.currentFile.diff = text;
2586
+ state.currentFile.diff = normalizeDiffContent(state.textBuffer);
2587
+ }
2588
+ break;
2589
+ case "content":
2590
+ if (state.currentFileCreated) {
2591
+ state.currentFileCreated.content = normalizeDiffContent(state.textBuffer);
1909
2592
  }
1910
2593
  break;
1911
2594
  case "config":
@@ -2348,7 +3031,7 @@ function handleSubmitPlan(planXml, workspacePath, agentType) {
2348
3031
  const projectContext = {
2349
3032
  projectId,
2350
3033
  workspacePath: effectivePath,
2351
- projectName: path3.basename(effectivePath),
3034
+ projectName: path4.basename(effectivePath),
2352
3035
  agentType: plan.agent
2353
3036
  };
2354
3037
  multiProjectPlanStore.initializeProject(projectContext);
@@ -2404,6 +3087,16 @@ function handleSubmitPlan(planXml, workspacePath, agentType) {
2404
3087
  console.error("[Overture] Plan parsing result. Nodes:", nodes.length, "Edges:", edges.length);
2405
3088
  console.error("[Overture] Project stored with ID:", projectId);
2406
3089
  console.error("[Overture] All projects after submit:", Array.from(multiProjectPlanStore.getAllProjects().map((p) => p.projectId)));
3090
+ const minNodesRequired = settingsStore.getMinNodesPerPlan();
3091
+ if (nodes.length > 0 && nodes.length < minNodesRequired) {
3092
+ console.error(`[Overture] Plan rejected: ${nodes.length} nodes < minimum ${minNodesRequired}`);
3093
+ multiProjectPlanStore.clearProjectPlan(projectId);
3094
+ return {
3095
+ success: false,
3096
+ message: `Plan rejected: Only ${nodes.length} node(s) provided, but minimum ${minNodesRequired} node(s) required. Please create a more detailed plan.`,
3097
+ projectId
3098
+ };
3099
+ }
2407
3100
  if (nodes.length > 0) {
2408
3101
  return {
2409
3102
  success: true,
@@ -2419,8 +3112,9 @@ function handleSubmitPlan(planXml, workspacePath, agentType) {
2419
3112
  projectId
2420
3113
  };
2421
3114
  }
2422
- async function handleGetApproval(projectId) {
3115
+ async function handleGetApproval(projectId, workspacePath) {
2423
3116
  const effectiveProjectId = projectId || currentProjectId;
3117
+ const effectiveWorkspacePath = workspacePath;
2424
3118
  console.error(`[Overture] get_approval called for project: ${effectiveProjectId}`);
2425
3119
  console.error(`[Overture] Provided projectId: ${projectId}, currentProjectId: ${currentProjectId}`);
2426
3120
  console.error(`[Overture] All projects in store:`, multiProjectPlanStore.getAllProjects().map((p) => p.projectId));
@@ -2454,23 +3148,26 @@ async function handleGetApproval(projectId) {
2454
3148
  status: "approved",
2455
3149
  firstNode: firstNodeInfo,
2456
3150
  message: "Plan approved by user. Execute firstNode, then call update_node_status to get the next node.",
2457
- projectId: effectiveProjectId
3151
+ projectId: effectiveProjectId,
3152
+ workspacePath: effectiveWorkspacePath
2458
3153
  };
2459
3154
  }
2460
3155
  if (result === "cancelled") {
2461
3156
  return {
2462
3157
  status: "cancelled",
2463
3158
  message: "Plan cancelled by user",
2464
- projectId: effectiveProjectId
3159
+ projectId: effectiveProjectId,
3160
+ workspacePath: effectiveWorkspacePath
2465
3161
  };
2466
3162
  }
2467
3163
  return {
2468
3164
  status: "pending",
2469
3165
  message: "Waiting for user approval. Call get_approval again to continue waiting.",
2470
- projectId: effectiveProjectId
3166
+ projectId: effectiveProjectId,
3167
+ workspacePath: effectiveWorkspacePath
2471
3168
  };
2472
3169
  }
2473
- async function handleCheckPause(wait = false, projectId) {
3170
+ async function handleCheckPause(wait = false, projectId, workspacePath) {
2474
3171
  const effectiveProjectId = projectId || currentProjectId;
2475
3172
  const isPaused = multiProjectPlanStore.getIsPaused(effectiveProjectId);
2476
3173
  if (!isPaused) {
@@ -2478,7 +3175,8 @@ async function handleCheckPause(wait = false, projectId) {
2478
3175
  isPaused: false,
2479
3176
  wasResumed: false,
2480
3177
  message: "Execution is not paused",
2481
- projectId: effectiveProjectId
3178
+ projectId: effectiveProjectId,
3179
+ workspacePath
2482
3180
  };
2483
3181
  }
2484
3182
  if (!wait) {
@@ -2486,7 +3184,8 @@ async function handleCheckPause(wait = false, projectId) {
2486
3184
  isPaused: true,
2487
3185
  wasResumed: false,
2488
3186
  message: "Execution is paused. Call with wait=true to block until resumed.",
2489
- projectId: effectiveProjectId
3187
+ projectId: effectiveProjectId,
3188
+ workspacePath
2490
3189
  };
2491
3190
  }
2492
3191
  await multiProjectPlanStore.waitIfPaused(effectiveProjectId);
@@ -2494,10 +3193,11 @@ async function handleCheckPause(wait = false, projectId) {
2494
3193
  isPaused: false,
2495
3194
  wasResumed: true,
2496
3195
  message: "Execution was paused and has now been resumed",
2497
- projectId: effectiveProjectId
3196
+ projectId: effectiveProjectId,
3197
+ workspacePath
2498
3198
  };
2499
3199
  }
2500
- function handleUpdateNodeStatus(nodeId, status, output, projectId) {
3200
+ function handleUpdateNodeStatus(nodeId, status, output, projectId, workspacePath) {
2501
3201
  const effectiveProjectId = projectId || currentProjectId;
2502
3202
  const plan = multiProjectPlanStore.getPlan(effectiveProjectId);
2503
3203
  const provider = plan?.agent || "unknown";
@@ -2506,7 +3206,7 @@ function handleUpdateNodeStatus(nodeId, status, output, projectId) {
2506
3206
  const nodeConfigs = multiProjectPlanStore.getNodeConfigs(effectiveProjectId);
2507
3207
  const node = nodes.find((n) => n.id === nodeId);
2508
3208
  if (!node) {
2509
- return { success: false, message: `Node ${nodeId} not found`, projectId: effectiveProjectId };
3209
+ return { success: false, message: `Node ${nodeId} not found`, projectId: effectiveProjectId, workspacePath };
2510
3210
  }
2511
3211
  if (plan && (plan.status === "ready" || plan.status === "streaming")) {
2512
3212
  console.error(`[Overture] Auto-approving plan - agent called update_node_status before get_approval (manual approval detected)`);
@@ -2542,7 +3242,8 @@ function handleUpdateNodeStatus(nodeId, status, output, projectId) {
2542
3242
  message: `Node ${nodeId} status updated to ${status}. Execute this node now.`,
2543
3243
  currentNode: currentNodeInfo,
2544
3244
  isPaused,
2545
- projectId: effectiveProjectId
3245
+ projectId: effectiveProjectId,
3246
+ workspacePath
2546
3247
  };
2547
3248
  }
2548
3249
  if (status === "completed") {
@@ -2553,7 +3254,8 @@ function handleUpdateNodeStatus(nodeId, status, output, projectId) {
2553
3254
  message: `Node ${nodeId} status updated to ${status}`,
2554
3255
  nextNode: nextNodeInfo,
2555
3256
  isPaused,
2556
- projectId: effectiveProjectId
3257
+ projectId: effectiveProjectId,
3258
+ workspacePath
2557
3259
  };
2558
3260
  } else {
2559
3261
  return {
@@ -2561,7 +3263,8 @@ function handleUpdateNodeStatus(nodeId, status, output, projectId) {
2561
3263
  message: `Node ${nodeId} status updated to ${status}. This was the last node.`,
2562
3264
  isLastNode: true,
2563
3265
  isPaused,
2564
- projectId: effectiveProjectId
3266
+ projectId: effectiveProjectId,
3267
+ workspacePath
2565
3268
  };
2566
3269
  }
2567
3270
  }
@@ -2569,7 +3272,8 @@ function handleUpdateNodeStatus(nodeId, status, output, projectId) {
2569
3272
  success: true,
2570
3273
  message: `Node ${nodeId} status updated to ${status}`,
2571
3274
  isPaused,
2572
- projectId: effectiveProjectId
3275
+ projectId: effectiveProjectId,
3276
+ workspacePath
2573
3277
  };
2574
3278
  }
2575
3279
  function findNextNode(projectId, currentNodeId, nodes, edges, provider) {
@@ -2646,26 +3350,27 @@ function findNextNode(projectId, currentNodeId, nodes, edges, provider) {
2646
3350
  }
2647
3351
  return null;
2648
3352
  }
2649
- function handlePlanCompleted(projectId) {
3353
+ function handlePlanCompleted(projectId, workspacePath) {
2650
3354
  const effectiveProjectId = projectId || currentProjectId;
2651
3355
  multiProjectPlanStore.updatePlanStatus(effectiveProjectId, "completed");
2652
3356
  wsManager.broadcastToProject(effectiveProjectId, { type: "plan_completed", projectId: effectiveProjectId });
2653
- return { success: true, message: "Plan completed", projectId: effectiveProjectId };
3357
+ return { success: true, message: "Plan completed", projectId: effectiveProjectId, workspacePath };
2654
3358
  }
2655
- function handlePlanFailed(error, projectId) {
3359
+ function handlePlanFailed(error, projectId, workspacePath) {
2656
3360
  const effectiveProjectId = projectId || currentProjectId;
2657
3361
  multiProjectPlanStore.updatePlanStatus(effectiveProjectId, "failed");
2658
3362
  wsManager.broadcastToProject(effectiveProjectId, { type: "plan_failed", error, projectId: effectiveProjectId });
2659
- return { success: true, message: "Plan failed", projectId: effectiveProjectId };
3363
+ return { success: true, message: "Plan failed", projectId: effectiveProjectId, workspacePath };
2660
3364
  }
2661
- async function handleCheckRerun(timeoutMs = 5e3, projectId) {
3365
+ async function handleCheckRerun(timeoutMs = 5e3, projectId, workspacePath) {
2662
3366
  const effectiveProjectId = projectId || currentProjectId;
2663
3367
  const rerunRequest = await multiProjectPlanStore.waitForRerun(effectiveProjectId, timeoutMs);
2664
3368
  if (!rerunRequest) {
2665
3369
  return {
2666
3370
  hasRerun: false,
2667
3371
  message: "No rerun request pending",
2668
- projectId: effectiveProjectId
3372
+ projectId: effectiveProjectId,
3373
+ workspacePath
2669
3374
  };
2670
3375
  }
2671
3376
  const resetNodeIds = multiProjectPlanStore.resetNodesForRerun(effectiveProjectId, rerunRequest.nodeId, rerunRequest.mode);
@@ -2699,27 +3404,30 @@ async function handleCheckRerun(timeoutMs = 5e3, projectId) {
2699
3404
  mode: rerunRequest.mode,
2700
3405
  nodeInfo,
2701
3406
  message: `Rerun requested from node ${rerunRequest.nodeId} (${rerunRequest.mode})`,
2702
- projectId: effectiveProjectId
3407
+ projectId: effectiveProjectId,
3408
+ workspacePath
2703
3409
  };
2704
3410
  }
2705
- function handleGetResumeInfo(projectId) {
3411
+ function handleGetResumeInfo(projectId, workspacePath) {
2706
3412
  const effectiveProjectId = projectId || currentProjectId;
2707
3413
  const resumeInfo = multiProjectPlanStore.getResumeInfo(effectiveProjectId);
2708
3414
  if (!resumeInfo) {
2709
3415
  return {
2710
3416
  success: false,
2711
3417
  message: `No active plan found for project: ${effectiveProjectId}`,
2712
- projectId: effectiveProjectId
3418
+ projectId: effectiveProjectId,
3419
+ workspacePath
2713
3420
  };
2714
3421
  }
2715
3422
  return {
2716
3423
  success: true,
2717
3424
  resumeInfo,
2718
3425
  message: `Resume info retrieved. Plan is at status '${resumeInfo.status}'. ${resumeInfo.currentNodeId ? `Current node: ${resumeInfo.currentNodeTitle} (${resumeInfo.currentNodeStatus})` : "No current node."} Completed: ${resumeInfo.completedNodes.length}, Pending: ${resumeInfo.pendingNodes.length}, Failed: ${resumeInfo.failedNodes.length}`,
2719
- projectId: effectiveProjectId
3426
+ projectId: effectiveProjectId,
3427
+ workspacePath
2720
3428
  };
2721
3429
  }
2722
- function handleRequestPlanUpdate(operations, projectId) {
3430
+ function handleRequestPlanUpdate(operations, projectId, workspacePath) {
2723
3431
  const effectiveProjectId = projectId || currentProjectId;
2724
3432
  const currentPlan = multiProjectPlanStore.getPlan(effectiveProjectId);
2725
3433
  if (!currentPlan) {
@@ -2727,7 +3435,8 @@ function handleRequestPlanUpdate(operations, projectId) {
2727
3435
  success: false,
2728
3436
  message: `No active plan found for project: ${effectiveProjectId}. Submit a new plan instead.`,
2729
3437
  results: [],
2730
- projectId: effectiveProjectId
3438
+ projectId: effectiveProjectId,
3439
+ workspacePath
2731
3440
  };
2732
3441
  }
2733
3442
  multiProjectPlanStore.storePreviousPlanState(effectiveProjectId);
@@ -2780,7 +3489,8 @@ function handleRequestPlanUpdate(operations, projectId) {
2780
3489
  success: failCount === 0,
2781
3490
  message: `Applied ${successCount}/${operations.length} operations. ${failCount > 0 ? "Some operations failed." : "All operations succeeded."} Call get_approval to confirm changes with user.`,
2782
3491
  results,
2783
- projectId: effectiveProjectId
3492
+ projectId: effectiveProjectId,
3493
+ workspacePath
2784
3494
  };
2785
3495
  }
2786
3496
  function applyInsertOperation(projectId, referenceNodeId, position, nodeData) {
@@ -2907,7 +3617,7 @@ function applyReplaceOperation(projectId, nodeId, newNodeData) {
2907
3617
  console.error(`[Overture] Node ${nodeId} replaced with new content`);
2908
3618
  return { success: true, message: `Node "${oldNode.title}" replaced with "${updatedNode.title}"` };
2909
3619
  }
2910
- function handleCreateNewPlan(projectId) {
3620
+ function handleCreateNewPlan(projectId, workspacePath) {
2911
3621
  const effectiveProjectId = projectId || currentProjectId;
2912
3622
  wsManager.broadcastToProject(effectiveProjectId, {
2913
3623
  type: "new_plan_created",
@@ -2919,7 +3629,8 @@ function handleCreateNewPlan(projectId) {
2919
3629
  return {
2920
3630
  success: true,
2921
3631
  message: `Ready to receive new plan. Submit the new plan using submit_plan or stream_plan_chunk, then call get_approval to wait for user approval. Note: Existing plans will be preserved on the canvas.`,
2922
- projectId: effectiveProjectId
3632
+ projectId: effectiveProjectId,
3633
+ workspacePath
2923
3634
  };
2924
3635
  }
2925
3636
  async function handleGetUsageInstructions(agentType) {
@@ -2950,25 +3661,25 @@ async function handleGetUsageInstructions(agentType) {
2950
3661
  };
2951
3662
  }
2952
3663
  const __filename2 = fileURLToPath(import.meta.url);
2953
- const __dirname2 = path3.dirname(__filename2);
3664
+ const __dirname2 = path4.dirname(__filename2);
2954
3665
  const possiblePaths = [
2955
- path3.resolve(__dirname2, "../../prompts"),
3666
+ path4.resolve(__dirname2, "../../prompts"),
2956
3667
  // npm installed (dist/tools -> prompts)
2957
- path3.resolve(__dirname2, "../prompts"),
3668
+ path4.resolve(__dirname2, "../prompts"),
2958
3669
  // Alternative npm location
2959
- path3.resolve(__dirname2, "../../../../prompts"),
3670
+ path4.resolve(__dirname2, "../../../../prompts"),
2960
3671
  // Development (monorepo root)
2961
- path3.resolve(__dirname2, "../../../prompts"),
3672
+ path4.resolve(__dirname2, "../../../prompts"),
2962
3673
  // Alternative dev location
2963
- path3.resolve(process.cwd(), "prompts")
3674
+ path4.resolve(process.cwd(), "prompts")
2964
3675
  // Relative to cwd
2965
3676
  ];
2966
3677
  let promptFile = null;
2967
3678
  let instructions = null;
2968
3679
  for (const promptsDir of possiblePaths) {
2969
- const candidatePath = path3.join(promptsDir, `${mappedType}.md`);
3680
+ const candidatePath = path4.join(promptsDir, `${mappedType}.md`);
2970
3681
  try {
2971
- instructions = await fs2.readFile(candidatePath, "utf-8");
3682
+ instructions = await fs3.readFile(candidatePath, "utf-8");
2972
3683
  promptFile = candidatePath;
2973
3684
  console.error(`[Overture] Found instructions at: ${candidatePath}`);
2974
3685
  break;
@@ -2987,7 +3698,7 @@ async function handleGetUsageInstructions(agentType) {
2987
3698
  };
2988
3699
  }
2989
3700
  console.error(`[Overture] Failed to find instructions for ${mappedType}. Searched paths:`);
2990
- possiblePaths.forEach((p) => console.error(` - ${path3.join(p, `${mappedType}.md`)}`));
3701
+ possiblePaths.forEach((p) => console.error(` - ${path4.join(p, `${mappedType}.md`)}`));
2991
3702
  return {
2992
3703
  success: false,
2993
3704
  agentType: mappedType,
@@ -2995,34 +3706,210 @@ async function handleGetUsageInstructions(agentType) {
2995
3706
  availableAgents
2996
3707
  };
2997
3708
  }
3709
+ function handleGetNodeInfo(nodeId, projectId, workspacePath) {
3710
+ const effectiveProjectId = projectId || currentProjectId;
3711
+ const nodes = multiProjectPlanStore.getNodes(effectiveProjectId);
3712
+ const nodeConfigs = multiProjectPlanStore.getNodeConfigs(effectiveProjectId);
3713
+ const selectedBranches = multiProjectPlanStore.getSelectedBranches(effectiveProjectId);
3714
+ const node = nodes.find((n) => n.id === nodeId);
3715
+ if (!node) {
3716
+ return {
3717
+ success: false,
3718
+ error: `Node ${nodeId} not found in project ${effectiveProjectId}`,
3719
+ projectId: effectiveProjectId,
3720
+ workspacePath
3721
+ };
3722
+ }
3723
+ const config = nodeConfigs[nodeId] || { fieldValues: {}, attachments: [] };
3724
+ let selectedBranchId;
3725
+ if (node.isBranchPoint) {
3726
+ selectedBranchId = selectedBranches[nodeId];
3727
+ }
3728
+ return {
3729
+ success: true,
3730
+ node: {
3731
+ id: node.id,
3732
+ title: node.title,
3733
+ type: node.type,
3734
+ status: node.status,
3735
+ description: node.description,
3736
+ complexity: node.complexity,
3737
+ expectedOutput: node.expectedOutput,
3738
+ risks: node.risks,
3739
+ fieldValues: config.fieldValues || {},
3740
+ attachments: config.attachments || [],
3741
+ mcpServers: config.mcpServers || [],
3742
+ metaInstructions: config.metaInstructions,
3743
+ isBranchPoint: node.isBranchPoint,
3744
+ branchTargetIds: node.branchTargetIds,
3745
+ selectedBranchId,
3746
+ branchSourceId: node.branchSourceId,
3747
+ output: node.output
3748
+ },
3749
+ projectId: effectiveProjectId,
3750
+ workspacePath
3751
+ };
3752
+ }
3753
+ function handleUpdateNodesDetail(updates, projectId, workspacePath) {
3754
+ const effectiveProjectId = projectId || currentProjectId;
3755
+ const state = multiProjectPlanStore.getState(effectiveProjectId);
3756
+ if (!state) {
3757
+ return {
3758
+ success: false,
3759
+ updatedCount: 0,
3760
+ errors: [`No active plan found for project: ${effectiveProjectId}`],
3761
+ projectId: effectiveProjectId,
3762
+ workspacePath
3763
+ };
3764
+ }
3765
+ const errors = [];
3766
+ let updatedCount = 0;
3767
+ const appliedUpdates = [];
3768
+ for (const update of updates) {
3769
+ const nodeIndex = state.nodes.findIndex((n) => n.id === update.node_id);
3770
+ if (nodeIndex < 0) {
3771
+ errors.push(`Node ${update.node_id} not found`);
3772
+ continue;
3773
+ }
3774
+ const node = state.nodes[nodeIndex];
3775
+ const nodeUpdates = {};
3776
+ if (update.title !== void 0) {
3777
+ node.title = update.title;
3778
+ nodeUpdates.title = update.title;
3779
+ }
3780
+ if (update.description !== void 0) {
3781
+ node.description = update.description;
3782
+ nodeUpdates.description = update.description;
3783
+ }
3784
+ if (update.complexity !== void 0) {
3785
+ node.complexity = update.complexity;
3786
+ nodeUpdates.complexity = update.complexity;
3787
+ }
3788
+ if (update.expectedOutput !== void 0) {
3789
+ node.expectedOutput = update.expectedOutput;
3790
+ nodeUpdates.expectedOutput = update.expectedOutput;
3791
+ }
3792
+ if (update.risks !== void 0) {
3793
+ node.risks = update.risks;
3794
+ nodeUpdates.risks = update.risks;
3795
+ }
3796
+ if (Object.keys(nodeUpdates).length > 0) {
3797
+ appliedUpdates.push({ nodeId: update.node_id, updates: nodeUpdates });
3798
+ updatedCount++;
3799
+ }
3800
+ }
3801
+ if (appliedUpdates.length > 0) {
3802
+ wsManager.broadcastToProject(effectiveProjectId, {
3803
+ type: "nodes_detail_updated",
3804
+ updates: appliedUpdates,
3805
+ projectId: effectiveProjectId
3806
+ });
3807
+ console.error(`[Overture] Updated details for ${updatedCount} node(s) in project ${effectiveProjectId}`);
3808
+ }
3809
+ return {
3810
+ success: errors.length === 0,
3811
+ updatedCount,
3812
+ errors: errors.length > 0 ? errors : void 0,
3813
+ projectId: effectiveProjectId,
3814
+ workspacePath
3815
+ };
3816
+ }
3817
+ function handleUpdateNodeDetail(nodeId, updates, projectId, workspacePath) {
3818
+ const effectiveProjectId = projectId || currentProjectId;
3819
+ const state = multiProjectPlanStore.getState(effectiveProjectId);
3820
+ if (!state) {
3821
+ return {
3822
+ success: false,
3823
+ message: `No active plan found for project: ${effectiveProjectId}`,
3824
+ projectId: effectiveProjectId,
3825
+ workspacePath
3826
+ };
3827
+ }
3828
+ const nodeIndex = state.nodes.findIndex((n) => n.id === nodeId);
3829
+ if (nodeIndex < 0) {
3830
+ return {
3831
+ success: false,
3832
+ message: `Node ${nodeId} not found in project ${effectiveProjectId}`,
3833
+ projectId: effectiveProjectId,
3834
+ workspacePath
3835
+ };
3836
+ }
3837
+ const node = state.nodes[nodeIndex];
3838
+ const nodeUpdates = {};
3839
+ if (updates.title !== void 0) {
3840
+ node.title = updates.title;
3841
+ nodeUpdates.title = updates.title;
3842
+ }
3843
+ if (updates.description !== void 0) {
3844
+ node.description = updates.description;
3845
+ nodeUpdates.description = updates.description;
3846
+ }
3847
+ if (updates.complexity !== void 0) {
3848
+ node.complexity = updates.complexity;
3849
+ nodeUpdates.complexity = updates.complexity;
3850
+ }
3851
+ if (updates.expectedOutput !== void 0) {
3852
+ node.expectedOutput = updates.expectedOutput;
3853
+ nodeUpdates.expectedOutput = updates.expectedOutput;
3854
+ }
3855
+ if (updates.risks !== void 0) {
3856
+ node.risks = updates.risks;
3857
+ nodeUpdates.risks = updates.risks;
3858
+ }
3859
+ if (Object.keys(nodeUpdates).length > 0) {
3860
+ wsManager.broadcastToProject(effectiveProjectId, {
3861
+ type: "node_detail_updated",
3862
+ nodeId,
3863
+ updates: nodeUpdates,
3864
+ projectId: effectiveProjectId
3865
+ });
3866
+ console.error(`[Overture] Updated details for node ${nodeId} in project ${effectiveProjectId}`);
3867
+ }
3868
+ return {
3869
+ success: true,
3870
+ message: `Node ${nodeId} updated successfully`,
3871
+ node: {
3872
+ id: node.id,
3873
+ title: node.title,
3874
+ type: node.type,
3875
+ status: node.status,
3876
+ description: node.description,
3877
+ complexity: node.complexity,
3878
+ expectedOutput: node.expectedOutput,
3879
+ risks: node.risks
3880
+ },
3881
+ projectId: effectiveProjectId,
3882
+ workspacePath
3883
+ };
3884
+ }
2998
3885
 
2999
3886
  // src/http/server.ts
3000
3887
  import express from "express";
3001
- import path4 from "path";
3888
+ import path5 from "path";
3002
3889
  import { createServer } from "http";
3003
- import fs3 from "fs";
3890
+ import fs4 from "fs";
3004
3891
  import fsp from "fs/promises";
3005
3892
  import os2 from "os";
3006
3893
  import { fileURLToPath as fileURLToPath2 } from "url";
3007
3894
  var __filename = fileURLToPath2(import.meta.url);
3008
- var __dirname = path4.dirname(__filename);
3895
+ var __dirname = path5.dirname(__filename);
3009
3896
  function startHttpServer(port) {
3010
3897
  const app = express();
3011
3898
  app.use(express.json({ limit: "25mb" }));
3012
3899
  const possiblePaths = [
3013
- path4.resolve(__dirname, "../ui-dist"),
3900
+ path5.resolve(__dirname, "../ui-dist"),
3014
3901
  // packages/mcp-server/dist/../ui-dist
3015
- path4.resolve(__dirname, "../../ui-dist"),
3902
+ path5.resolve(__dirname, "../../ui-dist"),
3016
3903
  // packages/mcp-server/ui-dist
3017
- path4.resolve(__dirname, "../../../ui/dist"),
3904
+ path5.resolve(__dirname, "../../../ui/dist"),
3018
3905
  // packages/ui/dist
3019
- path4.resolve(process.cwd(), "ui-dist"),
3906
+ path5.resolve(process.cwd(), "ui-dist"),
3020
3907
  // fallback to cwd
3021
- path4.resolve(process.cwd(), "packages/mcp-server/ui-dist")
3908
+ path5.resolve(process.cwd(), "packages/mcp-server/ui-dist")
3022
3909
  ];
3023
3910
  let staticPath = possiblePaths[0];
3024
3911
  for (const p of possiblePaths) {
3025
- if (fs3.existsSync(path4.join(p, "index.html"))) {
3912
+ if (fs4.existsSync(path5.join(p, "index.html"))) {
3026
3913
  staticPath = p;
3027
3914
  break;
3028
3915
  }
@@ -3049,17 +3936,46 @@ function startHttpServer(port) {
3049
3936
  res.status(500).json({ error: "Failed to fetch MCP marketplace" });
3050
3937
  }
3051
3938
  });
3939
+ app.post("/api/read-file", async (req, res) => {
3940
+ try {
3941
+ const { filePath } = req.body;
3942
+ if (!filePath) {
3943
+ return res.status(400).json({ error: "filePath is required" });
3944
+ }
3945
+ const normalizedPath = path5.normalize(filePath);
3946
+ if (normalizedPath.includes("..") && !path5.isAbsolute(normalizedPath)) {
3947
+ return res.status(400).json({ error: "Invalid file path" });
3948
+ }
3949
+ try {
3950
+ await fsp.access(normalizedPath, fs4.constants.R_OK);
3951
+ } catch {
3952
+ return res.status(404).json({ error: "File not found or not readable" });
3953
+ }
3954
+ const content = await fsp.readFile(normalizedPath, "utf-8");
3955
+ const stats = await fsp.stat(normalizedPath);
3956
+ const lineCount = content.split("\n").length;
3957
+ res.json({
3958
+ content,
3959
+ lineCount,
3960
+ size: stats.size,
3961
+ lastModified: stats.mtime.toISOString()
3962
+ });
3963
+ } catch (error) {
3964
+ console.error("[Overture] Failed to read file:", error);
3965
+ res.status(500).json({ error: "Failed to read file" });
3966
+ }
3967
+ });
3052
3968
  app.post("/api/attachments/save", async (req, res) => {
3053
3969
  try {
3054
3970
  const { fileName, contentBase64 } = req.body;
3055
3971
  if (!fileName || !contentBase64) {
3056
3972
  return res.status(400).json({ error: "fileName and contentBase64 are required" });
3057
3973
  }
3058
- const safeFileName = path4.basename(fileName).replace(/[^\w.-]/g, "_");
3059
- const attachmentDir = path4.join(os2.homedir(), ".overture", "attachments");
3974
+ const safeFileName = path5.basename(fileName).replace(/[^\w.-]/g, "_");
3975
+ const attachmentDir = path5.join(os2.homedir(), ".overture", "attachments");
3060
3976
  await fsp.mkdir(attachmentDir, { recursive: true });
3061
3977
  const timestamp = Date.now();
3062
- const absolutePath = path4.join(attachmentDir, `${timestamp}_${safeFileName}`);
3978
+ const absolutePath = path5.join(attachmentDir, `${timestamp}_${safeFileName}`);
3063
3979
  const fileBuffer = Buffer.from(contentBase64, "base64");
3064
3980
  await fsp.writeFile(absolutePath, fileBuffer);
3065
3981
  res.json({
@@ -3073,7 +3989,7 @@ function startHttpServer(port) {
3073
3989
  });
3074
3990
  app.use(express.static(staticPath));
3075
3991
  app.get("*", (_req, res) => {
3076
- res.sendFile(path4.join(staticPath, "index.html"));
3992
+ res.sendFile(path5.join(staticPath, "index.html"));
3077
3993
  });
3078
3994
  const server = createServer(app);
3079
3995
  server.on("error", (err) => {
@@ -3102,5 +4018,8 @@ export {
3102
4018
  handleRequestPlanUpdate,
3103
4019
  handleCreateNewPlan,
3104
4020
  handleGetUsageInstructions,
4021
+ handleGetNodeInfo,
4022
+ handleUpdateNodesDetail,
4023
+ handleUpdateNodeDetail,
3105
4024
  startHttpServer
3106
4025
  };