roadmap-skill 0.2.3 → 0.2.5

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/index.js CHANGED
@@ -287,6 +287,58 @@ var ProjectStorage = class {
287
287
  }
288
288
  return results;
289
289
  }
290
+ async exportAllData() {
291
+ await this.ensureDirectory();
292
+ const fs3 = await import("fs/promises");
293
+ const files = await fs3.readdir(this.storageDir);
294
+ const jsonFiles = files.filter((f) => f.endsWith(".json"));
295
+ const projects = [];
296
+ for (const file of jsonFiles) {
297
+ try {
298
+ const filePath = path3.join(this.storageDir, file);
299
+ const data = await readJsonFile(filePath);
300
+ projects.push(data);
301
+ } catch {
302
+ continue;
303
+ }
304
+ }
305
+ return {
306
+ version: 1,
307
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
308
+ projects
309
+ };
310
+ }
311
+ async importAllData(data) {
312
+ await this.ensureDirectory();
313
+ let imported = 0;
314
+ let errors = 0;
315
+ const errorDetails = [];
316
+ if (!data.projects || !Array.isArray(data.projects)) {
317
+ throw new Error("Invalid backup data: projects array is required");
318
+ }
319
+ for (const projectData of data.projects) {
320
+ try {
321
+ if (!projectData.project || !projectData.project.id) {
322
+ errors++;
323
+ errorDetails.push("Skipping invalid project: missing project or id");
324
+ continue;
325
+ }
326
+ const filePath = this.getFilePath(projectData.project.id);
327
+ await writeJsonFile(filePath, projectData);
328
+ imported++;
329
+ } catch (error) {
330
+ errors++;
331
+ const errorMessage = error instanceof Error ? error.message : String(error);
332
+ errorDetails.push(`Failed to import project ${projectData.project?.id || "unknown"}: ${errorMessage}`);
333
+ }
334
+ }
335
+ return {
336
+ success: errors === 0,
337
+ imported,
338
+ errors,
339
+ errorDetails
340
+ };
341
+ }
290
342
  };
291
343
  var storage = new ProjectStorage();
292
344
 
@@ -443,266 +495,534 @@ var deleteProjectTool = {
443
495
  // src/tools/task-tools.ts
444
496
  init_esm_shims();
445
497
  import { z as z2 } from "zod";
446
- var TaskStatusEnum = z2.enum(["todo", "in-progress", "review", "done"]);
447
- var TaskPriorityEnum = z2.enum(["low", "medium", "high", "critical"]);
448
- function generateTaskId() {
449
- return `task_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
498
+
499
+ // src/services/index.ts
500
+ init_esm_shims();
501
+
502
+ // src/services/tag-service.ts
503
+ init_esm_shims();
504
+ function generateTagId() {
505
+ return `tag_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
450
506
  }
451
- var createTaskTool = {
452
- name: "create_task",
453
- description: "Create a new task in a project",
454
- inputSchema: z2.object({
455
- projectId: z2.string().min(1, "Project ID is required"),
456
- title: z2.string().min(1, "Task title is required"),
457
- description: z2.string(),
458
- priority: TaskPriorityEnum.default("medium"),
459
- tags: z2.array(z2.string()).default([]),
460
- dueDate: z2.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
461
- assignee: z2.string().optional()
462
- }),
463
- async execute(input) {
507
+ var TagService = class {
508
+ storage;
509
+ constructor(storage2) {
510
+ this.storage = storage2;
511
+ }
512
+ /**
513
+ * Create a new tag in a project
514
+ * @param projectId - The project ID
515
+ * @param data - Tag creation data
516
+ * @returns The created tag or error
517
+ */
518
+ async create(projectId, data) {
464
519
  try {
465
- const projectData = await storage.readProject(input.projectId);
520
+ const projectData = await this.storage.readProject(projectId);
466
521
  if (!projectData) {
467
522
  return {
468
523
  success: false,
469
- error: `Project with ID '${input.projectId}' not found`
524
+ error: `Project with ID '${projectId}' not found`,
525
+ code: "NOT_FOUND"
526
+ };
527
+ }
528
+ const existingTag = projectData.tags.find(
529
+ (t) => t.name.toLowerCase() === data.name.toLowerCase()
530
+ );
531
+ if (existingTag) {
532
+ return {
533
+ success: false,
534
+ error: `Tag with name '${data.name}' already exists in this project`,
535
+ code: "DUPLICATE_ERROR"
470
536
  };
471
537
  }
472
538
  const now = (/* @__PURE__ */ new Date()).toISOString();
473
- const task = {
474
- id: generateTaskId(),
475
- projectId: input.projectId,
476
- title: input.title,
477
- description: input.description,
478
- status: "todo",
479
- priority: input.priority,
480
- tags: input.tags,
481
- dueDate: input.dueDate ?? null,
482
- assignee: input.assignee ?? null,
483
- createdAt: now,
484
- updatedAt: now,
485
- completedAt: null
539
+ const tag = {
540
+ id: generateTagId(),
541
+ name: data.name,
542
+ color: data.color,
543
+ description: data.description || "",
544
+ createdAt: now
486
545
  };
487
- projectData.tasks.push(task);
546
+ projectData.tags.push(tag);
488
547
  projectData.project.updatedAt = now;
489
- const filePath = storage.getFilePath(input.projectId);
490
- const { writeJsonFile: writeJsonFile2 } = await Promise.resolve().then(() => (init_file_helpers(), file_helpers_exports));
491
- await writeJsonFile2(filePath, projectData);
548
+ await this.saveProjectData(projectId, projectData);
492
549
  return {
493
550
  success: true,
494
- data: task
551
+ data: tag
495
552
  };
496
553
  } catch (error) {
497
554
  return {
498
555
  success: false,
499
- error: error instanceof Error ? error.message : "Failed to create task"
556
+ error: error instanceof Error ? error.message : "Failed to create tag",
557
+ code: "INTERNAL_ERROR"
500
558
  };
501
559
  }
502
560
  }
503
- };
504
- var listTasksTool = {
505
- name: "list_tasks",
506
- description: "List tasks with optional filters",
507
- inputSchema: z2.object({
508
- projectId: z2.string().optional(),
509
- status: TaskStatusEnum.optional(),
510
- priority: TaskPriorityEnum.optional(),
511
- tags: z2.array(z2.string()).optional(),
512
- assignee: z2.string().optional(),
513
- dueBefore: z2.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
514
- dueAfter: z2.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
515
- includeCompleted: z2.boolean().optional()
516
- }),
517
- async execute(input) {
561
+ /**
562
+ * List all tags in a project
563
+ * @param projectId - The project ID
564
+ * @returns Array of tags or error
565
+ */
566
+ async list(projectId) {
518
567
  try {
519
- const results = await storage.searchTasks({
520
- projectId: input.projectId,
521
- status: input.status,
522
- priority: input.priority,
523
- tags: input.tags,
524
- assignee: input.assignee,
525
- dueBefore: input.dueBefore,
526
- dueAfter: input.dueAfter,
527
- includeCompleted: input.includeCompleted
528
- });
568
+ const projectData = await this.storage.readProject(projectId);
569
+ if (!projectData) {
570
+ return {
571
+ success: false,
572
+ error: `Project with ID '${projectId}' not found`,
573
+ code: "NOT_FOUND"
574
+ };
575
+ }
529
576
  return {
530
577
  success: true,
531
- data: results
578
+ data: projectData.tags
532
579
  };
533
580
  } catch (error) {
534
581
  return {
535
582
  success: false,
536
- error: error instanceof Error ? error.message : "Failed to list tasks"
583
+ error: error instanceof Error ? error.message : "Failed to list tags",
584
+ code: "INTERNAL_ERROR"
537
585
  };
538
586
  }
539
587
  }
540
- };
541
- var getTaskTool = {
542
- name: "get_task",
543
- description: "Get a specific task by project ID and task ID",
544
- inputSchema: z2.object({
545
- projectId: z2.string().min(1, "Project ID is required"),
546
- taskId: z2.string().min(1, "Task ID is required")
547
- }),
548
- async execute(input) {
588
+ /**
589
+ * Update an existing tag
590
+ * @param projectId - The project ID
591
+ * @param tagId - The tag ID
592
+ * @param data - Tag update data
593
+ * @returns The updated tag or error
594
+ */
595
+ async update(projectId, tagId, data) {
549
596
  try {
550
- const projectData = await storage.readProject(input.projectId);
597
+ const projectData = await this.storage.readProject(projectId);
551
598
  if (!projectData) {
552
599
  return {
553
600
  success: false,
554
- error: `Project with ID '${input.projectId}' not found`
601
+ error: `Project with ID '${projectId}' not found`,
602
+ code: "NOT_FOUND"
555
603
  };
556
604
  }
557
- const task = projectData.tasks.find((t) => t.id === input.taskId);
558
- if (!task) {
605
+ const tagIndex = projectData.tags.findIndex((t) => t.id === tagId);
606
+ if (tagIndex === -1) {
607
+ return {
608
+ success: false,
609
+ error: `Tag with ID '${tagId}' not found in project '${projectId}'`,
610
+ code: "NOT_FOUND"
611
+ };
612
+ }
613
+ if (Object.keys(data).length === 0) {
559
614
  return {
560
615
  success: false,
561
- error: `Task with ID '${input.taskId}' not found in project '${input.projectId}'`
616
+ error: "At least one field to update is required",
617
+ code: "VALIDATION_ERROR"
562
618
  };
563
619
  }
620
+ if (data.name) {
621
+ const existingTag2 = projectData.tags.find(
622
+ (t) => t.name.toLowerCase() === data.name.toLowerCase() && t.id !== tagId
623
+ );
624
+ if (existingTag2) {
625
+ return {
626
+ success: false,
627
+ error: `Tag with name '${data.name}' already exists in this project`,
628
+ code: "DUPLICATE_ERROR"
629
+ };
630
+ }
631
+ }
632
+ const now = (/* @__PURE__ */ new Date()).toISOString();
633
+ const existingTag = projectData.tags[tagIndex];
634
+ const updatedTag = {
635
+ ...existingTag,
636
+ ...data,
637
+ id: existingTag.id,
638
+ createdAt: existingTag.createdAt
639
+ };
640
+ projectData.tags[tagIndex] = updatedTag;
641
+ projectData.project.updatedAt = now;
642
+ await this.saveProjectData(projectId, projectData);
564
643
  return {
565
644
  success: true,
566
- data: task
645
+ data: updatedTag
567
646
  };
568
647
  } catch (error) {
569
648
  return {
570
649
  success: false,
571
- error: error instanceof Error ? error.message : "Failed to get task"
650
+ error: error instanceof Error ? error.message : "Failed to update tag",
651
+ code: "INTERNAL_ERROR"
572
652
  };
573
653
  }
574
654
  }
575
- };
576
- var updateTaskTool = {
577
- name: "update_task",
578
- description: "Update an existing task",
579
- inputSchema: z2.object({
580
- projectId: z2.string().min(1, "Project ID is required"),
581
- taskId: z2.string().min(1, "Task ID is required"),
582
- title: z2.string().min(1).optional(),
583
- description: z2.string().optional(),
584
- status: TaskStatusEnum.optional(),
585
- priority: TaskPriorityEnum.optional(),
586
- tags: z2.array(z2.string()).optional(),
587
- dueDate: z2.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().nullable(),
588
- assignee: z2.string().optional().nullable()
589
- }),
590
- async execute(input) {
655
+ /**
656
+ * Delete a tag by ID
657
+ * Also removes the tag from all tasks that use it
658
+ * @param projectId - The project ID
659
+ * @param tagId - The tag ID
660
+ * @returns Delete result with tag info and count of updated tasks
661
+ */
662
+ async delete(projectId, tagId) {
591
663
  try {
592
- const projectData = await storage.readProject(input.projectId);
664
+ const projectData = await this.storage.readProject(projectId);
593
665
  if (!projectData) {
594
666
  return {
595
667
  success: false,
596
- error: `Project with ID '${input.projectId}' not found`
597
- };
598
- }
599
- const taskIndex = projectData.tasks.findIndex((t) => t.id === input.taskId);
600
- if (taskIndex === -1) {
601
- return {
602
- success: false,
603
- error: `Task with ID '${input.taskId}' not found in project '${input.projectId}'`
668
+ error: `Project with ID '${projectId}' not found`,
669
+ code: "NOT_FOUND"
604
670
  };
605
671
  }
606
- const { projectId, taskId, ...updateData } = input;
607
- if (Object.keys(updateData).length === 0) {
672
+ const tagIndex = projectData.tags.findIndex((t) => t.id === tagId);
673
+ if (tagIndex === -1) {
608
674
  return {
609
675
  success: false,
610
- error: "At least one field to update is required"
676
+ error: `Tag with ID '${tagId}' not found in project '${projectId}'`,
677
+ code: "NOT_FOUND"
611
678
  };
612
679
  }
680
+ const tag = projectData.tags[tagIndex];
613
681
  const now = (/* @__PURE__ */ new Date()).toISOString();
614
- const existingTask = projectData.tasks[taskIndex];
615
- const updatedTask = {
616
- ...existingTask,
617
- ...updateData,
618
- id: existingTask.id,
619
- projectId: existingTask.projectId,
620
- createdAt: existingTask.createdAt,
621
- updatedAt: now,
622
- completedAt: updateData.status === "done" && existingTask.status !== "done" ? now : updateData.status && updateData.status !== "done" ? null : existingTask.completedAt
623
- };
624
- projectData.tasks[taskIndex] = updatedTask;
682
+ let tasksUpdated = 0;
683
+ for (const task of projectData.tasks) {
684
+ const tagIndexInTask = task.tags.indexOf(tagId);
685
+ if (tagIndexInTask !== -1) {
686
+ task.tags.splice(tagIndexInTask, 1);
687
+ task.updatedAt = now;
688
+ tasksUpdated++;
689
+ }
690
+ }
691
+ projectData.tags.splice(tagIndex, 1);
625
692
  projectData.project.updatedAt = now;
626
- const filePath = storage.getFilePath(input.projectId);
627
- const { writeJsonFile: writeJsonFile2 } = await Promise.resolve().then(() => (init_file_helpers(), file_helpers_exports));
628
- await writeJsonFile2(filePath, projectData);
693
+ await this.saveProjectData(projectId, projectData);
629
694
  return {
630
695
  success: true,
631
- data: updatedTask
696
+ data: {
697
+ deleted: true,
698
+ tag,
699
+ tasksUpdated
700
+ }
632
701
  };
633
702
  } catch (error) {
634
703
  return {
635
704
  success: false,
636
- error: error instanceof Error ? error.message : "Failed to update task"
705
+ error: error instanceof Error ? error.message : "Failed to delete tag",
706
+ code: "INTERNAL_ERROR"
637
707
  };
638
708
  }
639
709
  }
640
- };
641
- var deleteTaskTool = {
642
- name: "delete_task",
643
- description: "Delete a task by project ID and task ID",
644
- inputSchema: z2.object({
645
- projectId: z2.string().min(1, "Project ID is required"),
646
- taskId: z2.string().min(1, "Task ID is required")
647
- }),
648
- async execute(input) {
710
+ /**
711
+ * Get all tasks that have a specific tag by tag name
712
+ * @param projectId - The project ID
713
+ * @param tagName - The tag name
714
+ * @returns Tag info and matching tasks
715
+ */
716
+ async getTasksByTag(projectId, tagName) {
649
717
  try {
650
- const projectData = await storage.readProject(input.projectId);
718
+ const projectData = await this.storage.readProject(projectId);
651
719
  if (!projectData) {
652
720
  return {
653
721
  success: false,
654
- error: `Project with ID '${input.projectId}' not found`
722
+ error: `Project with ID '${projectId}' not found`,
723
+ code: "NOT_FOUND"
655
724
  };
656
725
  }
657
- const taskIndex = projectData.tasks.findIndex((t) => t.id === input.taskId);
658
- if (taskIndex === -1) {
726
+ const tag = projectData.tags.find(
727
+ (t) => t.name.toLowerCase() === tagName.toLowerCase()
728
+ );
729
+ if (!tag) {
659
730
  return {
660
731
  success: false,
661
- error: `Task with ID '${input.taskId}' not found in project '${input.projectId}'`
732
+ error: `Tag with name '${tagName}' not found in project '${projectId}'`,
733
+ code: "NOT_FOUND"
662
734
  };
663
735
  }
664
- const now = (/* @__PURE__ */ new Date()).toISOString();
665
- projectData.tasks.splice(taskIndex, 1);
666
- projectData.project.updatedAt = now;
667
- const filePath = storage.getFilePath(input.projectId);
668
- const { writeJsonFile: writeJsonFile2 } = await Promise.resolve().then(() => (init_file_helpers(), file_helpers_exports));
669
- await writeJsonFile2(filePath, projectData);
736
+ const tasks = projectData.tasks.filter((t) => t.tags.includes(tag.id));
670
737
  return {
671
738
  success: true,
672
- data: { deleted: true }
739
+ data: {
740
+ tag,
741
+ tasks,
742
+ count: tasks.length
743
+ }
673
744
  };
674
745
  } catch (error) {
675
746
  return {
676
747
  success: false,
677
- error: error instanceof Error ? error.message : "Failed to delete task"
748
+ error: error instanceof Error ? error.message : "Failed to get tasks by tag",
749
+ code: "INTERNAL_ERROR"
678
750
  };
679
751
  }
680
752
  }
753
+ /**
754
+ * Helper method to save project data
755
+ * @param projectId - The project ID
756
+ * @param projectData - The project data to save
757
+ */
758
+ async saveProjectData(projectId, projectData) {
759
+ const filePath = this.storage.getFilePath(projectId);
760
+ const { writeJsonFile: writeJsonFile2 } = await Promise.resolve().then(() => (init_file_helpers(), file_helpers_exports));
761
+ await writeJsonFile2(filePath, projectData);
762
+ }
681
763
  };
682
- var batchUpdateTasksTool = {
683
- name: "batch_update_tasks",
684
- description: "Update multiple tasks at once",
685
- inputSchema: z2.object({
686
- projectId: z2.string().min(1, "Project ID is required"),
687
- taskIds: z2.array(z2.string()).min(1, "At least one task ID is required"),
688
- status: TaskStatusEnum.optional(),
689
- priority: TaskPriorityEnum.optional(),
690
- tags: z2.array(z2.string()).optional(),
691
- tagOperation: z2.enum(["add", "remove", "replace"]).default("replace")
692
- }),
693
- async execute(input) {
764
+
765
+ // src/services/task-service.ts
766
+ init_esm_shims();
767
+ init_file_helpers();
768
+ function generateTaskId() {
769
+ return `task_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
770
+ }
771
+ function calculateCompletedAt(currentStatus, newStatus, existingCompletedAt, now) {
772
+ if (!newStatus) {
773
+ return existingCompletedAt;
774
+ }
775
+ if (newStatus === "done" && currentStatus !== "done") {
776
+ return now;
777
+ }
778
+ if (currentStatus === "done" && newStatus !== "done") {
779
+ return null;
780
+ }
781
+ return existingCompletedAt;
782
+ }
783
+ var TaskService = {
784
+ /**
785
+ * Create a new task in a project
786
+ * @param projectId - The project ID
787
+ * @param data - Task creation data
788
+ * @returns The created task or error
789
+ */
790
+ async create(projectId, data) {
694
791
  try {
695
- const projectData = await storage.readProject(input.projectId);
792
+ const projectData = await storage.readProject(projectId);
696
793
  if (!projectData) {
697
794
  return {
698
795
  success: false,
699
- error: `Project with ID '${input.projectId}' not found`
796
+ error: `Project with ID '${projectId}' not found`,
797
+ code: "NOT_FOUND"
798
+ };
799
+ }
800
+ const now = (/* @__PURE__ */ new Date()).toISOString();
801
+ const task = {
802
+ id: generateTaskId(),
803
+ projectId,
804
+ title: data.title,
805
+ description: data.description,
806
+ status: "todo",
807
+ priority: data.priority ?? "medium",
808
+ tags: data.tags ?? [],
809
+ dueDate: data.dueDate ?? null,
810
+ assignee: data.assignee ?? null,
811
+ createdAt: now,
812
+ updatedAt: now,
813
+ completedAt: null
814
+ };
815
+ projectData.tasks.push(task);
816
+ projectData.project.updatedAt = now;
817
+ const filePath = storage.getFilePath(projectId);
818
+ await writeJsonFile(filePath, projectData);
819
+ return {
820
+ success: true,
821
+ data: task
822
+ };
823
+ } catch (error) {
824
+ return {
825
+ success: false,
826
+ error: error instanceof Error ? error.message : "Failed to create task",
827
+ code: "INTERNAL_ERROR"
828
+ };
829
+ }
830
+ },
831
+ /**
832
+ * Get a specific task by project ID and task ID
833
+ * @param projectId - The project ID
834
+ * @param taskId - The task ID
835
+ * @returns The task or error
836
+ */
837
+ async get(projectId, taskId) {
838
+ try {
839
+ const projectData = await storage.readProject(projectId);
840
+ if (!projectData) {
841
+ return {
842
+ success: false,
843
+ error: `Project with ID '${projectId}' not found`,
844
+ code: "NOT_FOUND"
845
+ };
846
+ }
847
+ const task = projectData.tasks.find((t) => t.id === taskId);
848
+ if (!task) {
849
+ return {
850
+ success: false,
851
+ error: `Task with ID '${taskId}' not found in project '${projectId}'`,
852
+ code: "NOT_FOUND"
853
+ };
854
+ }
855
+ return {
856
+ success: true,
857
+ data: task
858
+ };
859
+ } catch (error) {
860
+ return {
861
+ success: false,
862
+ error: error instanceof Error ? error.message : "Failed to get task",
863
+ code: "INTERNAL_ERROR"
864
+ };
865
+ }
866
+ },
867
+ /**
868
+ * Update an existing task
869
+ * Handles completedAt automatically based on status changes
870
+ * @param projectId - The project ID
871
+ * @param taskId - The task ID
872
+ * @param data - Task update data
873
+ * @returns The updated task or error
874
+ */
875
+ async update(projectId, taskId, data) {
876
+ try {
877
+ const projectData = await storage.readProject(projectId);
878
+ if (!projectData) {
879
+ return {
880
+ success: false,
881
+ error: `Project with ID '${projectId}' not found`,
882
+ code: "NOT_FOUND"
883
+ };
884
+ }
885
+ const taskIndex = projectData.tasks.findIndex((t) => t.id === taskId);
886
+ if (taskIndex === -1) {
887
+ return {
888
+ success: false,
889
+ error: `Task with ID '${taskId}' not found in project '${projectId}'`,
890
+ code: "NOT_FOUND"
891
+ };
892
+ }
893
+ const updateKeys = Object.keys(data);
894
+ if (updateKeys.length === 0) {
895
+ return {
896
+ success: false,
897
+ error: "At least one field to update is required",
898
+ code: "VALIDATION_ERROR"
899
+ };
900
+ }
901
+ const now = (/* @__PURE__ */ new Date()).toISOString();
902
+ const existingTask = projectData.tasks[taskIndex];
903
+ const completedAt = calculateCompletedAt(
904
+ existingTask.status,
905
+ data.status,
906
+ existingTask.completedAt,
907
+ now
908
+ );
909
+ const updatedTask = {
910
+ ...existingTask,
911
+ ...data,
912
+ id: existingTask.id,
913
+ projectId: existingTask.projectId,
914
+ createdAt: existingTask.createdAt,
915
+ updatedAt: now,
916
+ completedAt
917
+ };
918
+ projectData.tasks[taskIndex] = updatedTask;
919
+ projectData.project.updatedAt = now;
920
+ const filePath = storage.getFilePath(projectId);
921
+ await writeJsonFile(filePath, projectData);
922
+ return {
923
+ success: true,
924
+ data: updatedTask
925
+ };
926
+ } catch (error) {
927
+ return {
928
+ success: false,
929
+ error: error instanceof Error ? error.message : "Failed to update task",
930
+ code: "INTERNAL_ERROR"
931
+ };
932
+ }
933
+ },
934
+ /**
935
+ * Delete a task by project ID and task ID
936
+ * @param projectId - The project ID
937
+ * @param taskId - The task ID
938
+ * @returns Void or error
939
+ */
940
+ async delete(projectId, taskId) {
941
+ try {
942
+ const projectData = await storage.readProject(projectId);
943
+ if (!projectData) {
944
+ return {
945
+ success: false,
946
+ error: `Project with ID '${projectId}' not found`,
947
+ code: "NOT_FOUND"
948
+ };
949
+ }
950
+ const taskIndex = projectData.tasks.findIndex((t) => t.id === taskId);
951
+ if (taskIndex === -1) {
952
+ return {
953
+ success: false,
954
+ error: `Task with ID '${taskId}' not found in project '${projectId}'`,
955
+ code: "NOT_FOUND"
956
+ };
957
+ }
958
+ const now = (/* @__PURE__ */ new Date()).toISOString();
959
+ projectData.tasks.splice(taskIndex, 1);
960
+ projectData.project.updatedAt = now;
961
+ const filePath = storage.getFilePath(projectId);
962
+ await writeJsonFile(filePath, projectData);
963
+ return {
964
+ success: true,
965
+ data: void 0
966
+ };
967
+ } catch (error) {
968
+ return {
969
+ success: false,
970
+ error: error instanceof Error ? error.message : "Failed to delete task",
971
+ code: "INTERNAL_ERROR"
972
+ };
973
+ }
974
+ },
975
+ /**
976
+ * List tasks with optional filters
977
+ * @param filters - Optional filters for the search
978
+ * @returns Array of tasks or error
979
+ */
980
+ async list(filters) {
981
+ try {
982
+ const results = await storage.searchTasks({
983
+ projectId: filters?.projectId,
984
+ status: filters?.status,
985
+ priority: filters?.priority,
986
+ tags: filters?.tags,
987
+ assignee: filters?.assignee,
988
+ dueBefore: filters?.dueBefore,
989
+ dueAfter: filters?.dueAfter,
990
+ includeCompleted: filters?.includeCompleted
991
+ });
992
+ const tasks = results.map((r) => r.task);
993
+ return {
994
+ success: true,
995
+ data: tasks
996
+ };
997
+ } catch (error) {
998
+ return {
999
+ success: false,
1000
+ error: error instanceof Error ? error.message : "Failed to list tasks",
1001
+ code: "INTERNAL_ERROR"
1002
+ };
1003
+ }
1004
+ },
1005
+ /**
1006
+ * Update multiple tasks at once
1007
+ * @param projectId - The project ID
1008
+ * @param taskIds - Array of task IDs to update
1009
+ * @param data - Batch update data
1010
+ * @returns Batch update result or error
1011
+ */
1012
+ async batchUpdate(projectId, taskIds, data) {
1013
+ try {
1014
+ const projectData = await storage.readProject(projectId);
1015
+ if (!projectData) {
1016
+ return {
1017
+ success: false,
1018
+ error: `Project with ID '${projectId}' not found`,
1019
+ code: "NOT_FOUND"
700
1020
  };
701
1021
  }
702
1022
  const now = (/* @__PURE__ */ new Date()).toISOString();
703
1023
  const updatedTasks = [];
704
1024
  const notFoundIds = [];
705
- for (const taskId of input.taskIds) {
1025
+ for (const taskId of taskIds) {
706
1026
  const taskIndex = projectData.tasks.findIndex((t) => t.id === taskId);
707
1027
  if (taskIndex === -1) {
708
1028
  notFoundIds.push(taskId);
@@ -710,29 +1030,34 @@ var batchUpdateTasksTool = {
710
1030
  }
711
1031
  const existingTask = projectData.tasks[taskIndex];
712
1032
  let updatedTags = existingTask.tags;
713
- if (input.tags && input.tags.length > 0) {
1033
+ if (data.tags && data.tags.length > 0) {
714
1034
  const existingTags = existingTask.tags || [];
715
- switch (input.tagOperation) {
1035
+ switch (data.tagOperation) {
716
1036
  case "add":
717
- updatedTags = [.../* @__PURE__ */ new Set([...existingTags, ...input.tags])];
1037
+ updatedTags = [.../* @__PURE__ */ new Set([...existingTags, ...data.tags])];
718
1038
  break;
719
1039
  case "remove":
720
- updatedTags = existingTags.filter((tag) => !input.tags.includes(tag));
1040
+ updatedTags = existingTags.filter((tag) => !data.tags.includes(tag));
721
1041
  break;
722
1042
  case "replace":
723
1043
  default:
724
- updatedTags = input.tags;
1044
+ updatedTags = data.tags;
725
1045
  break;
726
1046
  }
727
1047
  }
1048
+ const completedAt = calculateCompletedAt(
1049
+ existingTask.status,
1050
+ data.status,
1051
+ existingTask.completedAt,
1052
+ now
1053
+ );
728
1054
  const updatedTask = {
729
1055
  ...existingTask,
730
- ...input.status && { status: input.status },
731
- ...input.priority && { priority: input.priority },
1056
+ ...data.status && { status: data.status },
1057
+ ...data.priority && { priority: data.priority },
732
1058
  tags: updatedTags,
733
1059
  updatedAt: now,
734
- ...input.status === "done" && existingTask.status !== "done" && { completedAt: now },
735
- ...input.status && input.status !== "done" && { completedAt: null }
1060
+ completedAt
736
1061
  };
737
1062
  projectData.tasks[taskIndex] = updatedTask;
738
1063
  updatedTasks.push(updatedTask);
@@ -741,13 +1066,12 @@ var batchUpdateTasksTool = {
741
1066
  return {
742
1067
  success: false,
743
1068
  error: "No tasks were found to update",
744
- notFoundIds
1069
+ code: "NOT_FOUND"
745
1070
  };
746
1071
  }
747
1072
  projectData.project.updatedAt = now;
748
- const filePath = storage.getFilePath(input.projectId);
749
- const { writeJsonFile: writeJsonFile2 } = await Promise.resolve().then(() => (init_file_helpers(), file_helpers_exports));
750
- await writeJsonFile2(filePath, projectData);
1073
+ const filePath = storage.getFilePath(projectId);
1074
+ await writeJsonFile(filePath, projectData);
751
1075
  return {
752
1076
  success: true,
753
1077
  data: {
@@ -759,18 +1083,155 @@ var batchUpdateTasksTool = {
759
1083
  } catch (error) {
760
1084
  return {
761
1085
  success: false,
762
- error: error instanceof Error ? error.message : "Failed to batch update tasks"
1086
+ error: error instanceof Error ? error.message : "Failed to batch update tasks",
1087
+ code: "INTERNAL_ERROR"
763
1088
  };
764
1089
  }
765
1090
  }
766
1091
  };
767
1092
 
1093
+ // src/services/types.ts
1094
+ init_esm_shims();
1095
+
1096
+ // src/tools/task-tools.ts
1097
+ var TaskStatusEnum = z2.enum(["todo", "in-progress", "review", "done"]);
1098
+ var TaskPriorityEnum = z2.enum(["low", "medium", "high", "critical"]);
1099
+ var createTaskTool = {
1100
+ name: "create_task",
1101
+ description: "Create a new task in a project",
1102
+ inputSchema: z2.object({
1103
+ projectId: z2.string().min(1, "Project ID is required"),
1104
+ title: z2.string().min(1, "Task title is required"),
1105
+ description: z2.string(),
1106
+ priority: TaskPriorityEnum.default("medium"),
1107
+ tags: z2.array(z2.string()).default([]),
1108
+ dueDate: z2.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
1109
+ assignee: z2.string().optional()
1110
+ }),
1111
+ async execute(input) {
1112
+ const result = await TaskService.create(input.projectId, {
1113
+ title: input.title,
1114
+ description: input.description,
1115
+ priority: input.priority,
1116
+ tags: input.tags,
1117
+ dueDate: input.dueDate,
1118
+ assignee: input.assignee
1119
+ });
1120
+ return result;
1121
+ }
1122
+ };
1123
+ var listTasksTool = {
1124
+ name: "list_tasks",
1125
+ description: "List tasks with optional filters",
1126
+ inputSchema: z2.object({
1127
+ projectId: z2.string().optional(),
1128
+ status: TaskStatusEnum.optional(),
1129
+ priority: TaskPriorityEnum.optional(),
1130
+ tags: z2.array(z2.string()).optional(),
1131
+ assignee: z2.string().optional(),
1132
+ dueBefore: z2.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
1133
+ dueAfter: z2.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
1134
+ includeCompleted: z2.boolean().optional()
1135
+ }),
1136
+ async execute(input) {
1137
+ try {
1138
+ const results = await storage.searchTasks({
1139
+ projectId: input.projectId,
1140
+ status: input.status,
1141
+ priority: input.priority,
1142
+ tags: input.tags,
1143
+ assignee: input.assignee,
1144
+ dueBefore: input.dueBefore,
1145
+ dueAfter: input.dueAfter,
1146
+ includeCompleted: input.includeCompleted
1147
+ });
1148
+ return {
1149
+ success: true,
1150
+ data: results
1151
+ };
1152
+ } catch (error) {
1153
+ return {
1154
+ success: false,
1155
+ error: error instanceof Error ? error.message : "Failed to list tasks"
1156
+ };
1157
+ }
1158
+ }
1159
+ };
1160
+ var getTaskTool = {
1161
+ name: "get_task",
1162
+ description: "Get a specific task by project ID and task ID",
1163
+ inputSchema: z2.object({
1164
+ projectId: z2.string().min(1, "Project ID is required"),
1165
+ taskId: z2.string().min(1, "Task ID is required")
1166
+ }),
1167
+ async execute(input) {
1168
+ const result = await TaskService.get(input.projectId, input.taskId);
1169
+ return result;
1170
+ }
1171
+ };
1172
+ var updateTaskTool = {
1173
+ name: "update_task",
1174
+ description: "Update an existing task",
1175
+ inputSchema: z2.object({
1176
+ projectId: z2.string().min(1, "Project ID is required"),
1177
+ taskId: z2.string().min(1, "Task ID is required"),
1178
+ title: z2.string().min(1).optional(),
1179
+ description: z2.string().optional(),
1180
+ status: TaskStatusEnum.optional(),
1181
+ priority: TaskPriorityEnum.optional(),
1182
+ tags: z2.array(z2.string()).optional(),
1183
+ dueDate: z2.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().nullable(),
1184
+ assignee: z2.string().optional().nullable()
1185
+ }),
1186
+ async execute(input) {
1187
+ const { projectId, taskId, ...updateData } = input;
1188
+ const result = await TaskService.update(projectId, taskId, updateData);
1189
+ return result;
1190
+ }
1191
+ };
1192
+ var deleteTaskTool = {
1193
+ name: "delete_task",
1194
+ description: "Delete a task by project ID and task ID",
1195
+ inputSchema: z2.object({
1196
+ projectId: z2.string().min(1, "Project ID is required"),
1197
+ taskId: z2.string().min(1, "Task ID is required")
1198
+ }),
1199
+ async execute(input) {
1200
+ const result = await TaskService.delete(input.projectId, input.taskId);
1201
+ if (result.success) {
1202
+ return {
1203
+ success: true,
1204
+ data: { deleted: true }
1205
+ };
1206
+ }
1207
+ return result;
1208
+ }
1209
+ };
1210
+ var batchUpdateTasksTool = {
1211
+ name: "batch_update_tasks",
1212
+ description: "Update multiple tasks at once",
1213
+ inputSchema: z2.object({
1214
+ projectId: z2.string().min(1, "Project ID is required"),
1215
+ taskIds: z2.array(z2.string()).min(1, "At least one task ID is required"),
1216
+ status: TaskStatusEnum.optional(),
1217
+ priority: TaskPriorityEnum.optional(),
1218
+ tags: z2.array(z2.string()).optional(),
1219
+ tagOperation: z2.enum(["add", "remove", "replace"]).default("replace")
1220
+ }),
1221
+ async execute(input) {
1222
+ const { projectId, taskIds, tagOperation, ...restData } = input;
1223
+ const result = await TaskService.batchUpdate(projectId, taskIds, {
1224
+ ...restData,
1225
+ tagOperation
1226
+ });
1227
+ return result;
1228
+ }
1229
+ };
1230
+
768
1231
  // src/tools/tag-tools.ts
769
1232
  init_esm_shims();
770
1233
  import { z as z3 } from "zod";
771
- function generateTagId() {
772
- return `tag_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
773
- }
1234
+ var tagService = new TagService(storage);
774
1235
  var createTagTool = {
775
1236
  name: "create_tag",
776
1237
  description: "Create a new tag in a project",
@@ -781,46 +1242,12 @@ var createTagTool = {
781
1242
  description: z3.string().default("")
782
1243
  }),
783
1244
  async execute(input) {
784
- try {
785
- const projectData = await storage.readProject(input.projectId);
786
- if (!projectData) {
787
- return {
788
- success: false,
789
- error: `Project with ID '${input.projectId}' not found`
790
- };
791
- }
792
- const existingTag = projectData.tags.find(
793
- (t) => t.name.toLowerCase() === input.name.toLowerCase()
794
- );
795
- if (existingTag) {
796
- return {
797
- success: false,
798
- error: `Tag with name '${input.name}' already exists in this project`
799
- };
800
- }
801
- const now = (/* @__PURE__ */ new Date()).toISOString();
802
- const tag = {
803
- id: generateTagId(),
804
- name: input.name,
805
- color: input.color,
806
- description: input.description,
807
- createdAt: now
808
- };
809
- projectData.tags.push(tag);
810
- projectData.project.updatedAt = now;
811
- const filePath = storage.getFilePath(input.projectId);
812
- const { writeJsonFile: writeJsonFile2 } = await Promise.resolve().then(() => (init_file_helpers(), file_helpers_exports));
813
- await writeJsonFile2(filePath, projectData);
814
- return {
815
- success: true,
816
- data: tag
817
- };
818
- } catch (error) {
819
- return {
820
- success: false,
821
- error: error instanceof Error ? error.message : "Failed to create tag"
822
- };
1245
+ const { projectId, ...data } = input;
1246
+ const result = await tagService.create(projectId, data);
1247
+ if (!result.success) {
1248
+ return { success: false, error: result.error };
823
1249
  }
1250
+ return { success: true, data: result.data };
824
1251
  }
825
1252
  };
826
1253
  var listTagsTool = {
@@ -830,24 +1257,11 @@ var listTagsTool = {
830
1257
  projectId: z3.string().min(1, "Project ID is required")
831
1258
  }),
832
1259
  async execute(input) {
833
- try {
834
- const projectData = await storage.readProject(input.projectId);
835
- if (!projectData) {
836
- return {
837
- success: false,
838
- error: `Project with ID '${input.projectId}' not found`
839
- };
840
- }
841
- return {
842
- success: true,
843
- data: projectData.tags
844
- };
845
- } catch (error) {
846
- return {
847
- success: false,
848
- error: error instanceof Error ? error.message : "Failed to list tags"
849
- };
1260
+ const result = await tagService.list(input.projectId);
1261
+ if (!result.success) {
1262
+ return { success: false, error: result.error };
850
1263
  }
1264
+ return { success: true, data: result.data };
851
1265
  }
852
1266
  };
853
1267
  var updateTagTool = {
@@ -861,62 +1275,12 @@ var updateTagTool = {
861
1275
  description: z3.string().optional()
862
1276
  }),
863
1277
  async execute(input) {
864
- try {
865
- const projectData = await storage.readProject(input.projectId);
866
- if (!projectData) {
867
- return {
868
- success: false,
869
- error: `Project with ID '${input.projectId}' not found`
870
- };
871
- }
872
- const tagIndex = projectData.tags.findIndex((t) => t.id === input.tagId);
873
- if (tagIndex === -1) {
874
- return {
875
- success: false,
876
- error: `Tag with ID '${input.tagId}' not found in project '${input.projectId}'`
877
- };
878
- }
879
- const { projectId, tagId, ...updateData } = input;
880
- if (Object.keys(updateData).length === 0) {
881
- return {
882
- success: false,
883
- error: "At least one field to update is required"
884
- };
885
- }
886
- if (updateData.name) {
887
- const existingTag2 = projectData.tags.find(
888
- (t) => t.name.toLowerCase() === updateData.name.toLowerCase() && t.id !== input.tagId
889
- );
890
- if (existingTag2) {
891
- return {
892
- success: false,
893
- error: `Tag with name '${updateData.name}' already exists in this project`
894
- };
895
- }
896
- }
897
- const now = (/* @__PURE__ */ new Date()).toISOString();
898
- const existingTag = projectData.tags[tagIndex];
899
- const updatedTag = {
900
- ...existingTag,
901
- ...updateData,
902
- id: existingTag.id,
903
- createdAt: existingTag.createdAt
904
- };
905
- projectData.tags[tagIndex] = updatedTag;
906
- projectData.project.updatedAt = now;
907
- const filePath = storage.getFilePath(input.projectId);
908
- const { writeJsonFile: writeJsonFile2 } = await Promise.resolve().then(() => (init_file_helpers(), file_helpers_exports));
909
- await writeJsonFile2(filePath, projectData);
910
- return {
911
- success: true,
912
- data: updatedTag
913
- };
914
- } catch (error) {
915
- return {
916
- success: false,
917
- error: error instanceof Error ? error.message : "Failed to update tag"
918
- };
1278
+ const { projectId, tagId, ...data } = input;
1279
+ const result = await tagService.update(projectId, tagId, data);
1280
+ if (!result.success) {
1281
+ return { success: false, error: result.error };
919
1282
  }
1283
+ return { success: true, data: result.data };
920
1284
  }
921
1285
  };
922
1286
  var deleteTagTool = {
@@ -927,51 +1291,11 @@ var deleteTagTool = {
927
1291
  tagId: z3.string().min(1, "Tag ID is required")
928
1292
  }),
929
1293
  async execute(input) {
930
- try {
931
- const projectData = await storage.readProject(input.projectId);
932
- if (!projectData) {
933
- return {
934
- success: false,
935
- error: `Project with ID '${input.projectId}' not found`
936
- };
937
- }
938
- const tagIndex = projectData.tags.findIndex((t) => t.id === input.tagId);
939
- if (tagIndex === -1) {
940
- return {
941
- success: false,
942
- error: `Tag with ID '${input.tagId}' not found in project '${input.projectId}'`
943
- };
944
- }
945
- const tag = projectData.tags[tagIndex];
946
- const now = (/* @__PURE__ */ new Date()).toISOString();
947
- let tasksUpdated = 0;
948
- for (const task of projectData.tasks) {
949
- const tagIndexInTask = task.tags.indexOf(input.tagId);
950
- if (tagIndexInTask !== -1) {
951
- task.tags.splice(tagIndexInTask, 1);
952
- task.updatedAt = now;
953
- tasksUpdated++;
954
- }
955
- }
956
- projectData.tags.splice(tagIndex, 1);
957
- projectData.project.updatedAt = now;
958
- const filePath = storage.getFilePath(input.projectId);
959
- const { writeJsonFile: writeJsonFile2 } = await Promise.resolve().then(() => (init_file_helpers(), file_helpers_exports));
960
- await writeJsonFile2(filePath, projectData);
961
- return {
962
- success: true,
963
- data: {
964
- deleted: true,
965
- tag,
966
- tasksUpdated
967
- }
968
- };
969
- } catch (error) {
970
- return {
971
- success: false,
972
- error: error instanceof Error ? error.message : "Failed to delete tag"
973
- };
1294
+ const result = await tagService.delete(input.projectId, input.tagId);
1295
+ if (!result.success) {
1296
+ return { success: false, error: result.error };
974
1297
  }
1298
+ return { success: true, data: result.data };
975
1299
  }
976
1300
  };
977
1301
  var getTasksByTagTool = {
@@ -982,38 +1306,11 @@ var getTasksByTagTool = {
982
1306
  tagName: z3.string().min(1, "Tag name is required")
983
1307
  }),
984
1308
  async execute(input) {
985
- try {
986
- const projectData = await storage.readProject(input.projectId);
987
- if (!projectData) {
988
- return {
989
- success: false,
990
- error: `Project with ID '${input.projectId}' not found`
991
- };
992
- }
993
- const tag = projectData.tags.find(
994
- (t) => t.name.toLowerCase() === input.tagName.toLowerCase()
995
- );
996
- if (!tag) {
997
- return {
998
- success: false,
999
- error: `Tag with name '${input.tagName}' not found in project '${input.projectId}'`
1000
- };
1001
- }
1002
- const tasks = projectData.tasks.filter((t) => t.tags.includes(tag.id));
1003
- return {
1004
- success: true,
1005
- data: {
1006
- tag,
1007
- tasks,
1008
- count: tasks.length
1009
- }
1010
- };
1011
- } catch (error) {
1012
- return {
1013
- success: false,
1014
- error: error instanceof Error ? error.message : "Failed to get tasks by tag"
1015
- };
1309
+ const result = await tagService.getTasksByTag(input.projectId, input.tagName);
1310
+ if (!result.success) {
1311
+ return { success: false, error: result.error };
1016
1312
  }
1313
+ return { success: true, data: result.data };
1017
1314
  }
1018
1315
  };
1019
1316
 
@@ -1024,6 +1321,10 @@ init_esm_shims();
1024
1321
  init_esm_shims();
1025
1322
  import express from "express";
1026
1323
  import * as path4 from "path";
1324
+ import { fileURLToPath as fileURLToPath2 } from "url";
1325
+ var __filename2 = fileURLToPath2(import.meta.url);
1326
+ var __dirname2 = path4.dirname(__filename2);
1327
+ var tagService2 = new TagService(storage);
1027
1328
  function createServer(port = 7860) {
1028
1329
  return new Promise((resolve, reject) => {
1029
1330
  const app = express();
@@ -1089,31 +1390,13 @@ function createServer(port = 7860) {
1089
1390
  app.post("/api/tasks", async (req, res) => {
1090
1391
  try {
1091
1392
  const { projectId, ...taskData } = req.body;
1092
- const projectData = await storage.readProject(projectId);
1093
- if (!projectData) {
1094
- res.status(404).json({ error: "Project not found" });
1393
+ const result = await TaskService.create(projectId, taskData);
1394
+ if (!result.success) {
1395
+ const statusCode = result.code === "NOT_FOUND" ? 404 : 400;
1396
+ res.status(statusCode).json({ error: result.error });
1095
1397
  return;
1096
1398
  }
1097
- const now = (/* @__PURE__ */ new Date()).toISOString();
1098
- const task = {
1099
- id: `task_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
1100
- projectId,
1101
- ...taskData,
1102
- status: taskData.status || "todo",
1103
- priority: taskData.priority || "medium",
1104
- tags: taskData.tags || [],
1105
- dueDate: taskData.dueDate || null,
1106
- assignee: taskData.assignee || null,
1107
- createdAt: now,
1108
- updatedAt: now,
1109
- completedAt: null
1110
- };
1111
- projectData.tasks.push(task);
1112
- projectData.project.updatedAt = now;
1113
- const filePath = storage.getFilePath(projectId);
1114
- const { writeJsonFile: writeJsonFile2 } = await Promise.resolve().then(() => (init_file_helpers(), file_helpers_exports));
1115
- await writeJsonFile2(filePath, projectData);
1116
- res.json({ success: true, data: task });
1399
+ res.json({ success: true, data: result.data });
1117
1400
  } catch (error) {
1118
1401
  res.status(500).json({ error: error.message });
1119
1402
  }
@@ -1121,33 +1404,13 @@ function createServer(port = 7860) {
1121
1404
  app.put("/api/tasks", async (req, res) => {
1122
1405
  try {
1123
1406
  const { projectId, taskId, ...updateData } = req.body;
1124
- const projectData = await storage.readProject(projectId);
1125
- if (!projectData) {
1126
- res.status(404).json({ error: "Project not found" });
1407
+ const result = await TaskService.update(projectId, taskId, updateData);
1408
+ if (!result.success) {
1409
+ const statusCode = result.code === "NOT_FOUND" ? 404 : 400;
1410
+ res.status(statusCode).json({ error: result.error });
1127
1411
  return;
1128
1412
  }
1129
- const taskIndex = projectData.tasks.findIndex((t) => t.id === taskId);
1130
- if (taskIndex === -1) {
1131
- res.status(404).json({ error: "Task not found" });
1132
- return;
1133
- }
1134
- const now = (/* @__PURE__ */ new Date()).toISOString();
1135
- const existingTask = projectData.tasks[taskIndex];
1136
- const updatedTask = {
1137
- ...existingTask,
1138
- ...updateData,
1139
- id: existingTask.id,
1140
- projectId: existingTask.projectId,
1141
- createdAt: existingTask.createdAt,
1142
- updatedAt: now,
1143
- completedAt: updateData.status === "done" && existingTask.status !== "done" ? now : updateData.status && updateData.status !== "done" ? null : existingTask.completedAt
1144
- };
1145
- projectData.tasks[taskIndex] = updatedTask;
1146
- projectData.project.updatedAt = now;
1147
- const filePath = storage.getFilePath(projectId);
1148
- const { writeJsonFile: writeJsonFile2 } = await Promise.resolve().then(() => (init_file_helpers(), file_helpers_exports));
1149
- await writeJsonFile2(filePath, projectData);
1150
- res.json({ success: true, data: updatedTask });
1413
+ res.json({ success: true, data: result.data });
1151
1414
  } catch (error) {
1152
1415
  res.status(500).json({ error: error.message });
1153
1416
  }
@@ -1155,27 +1418,96 @@ function createServer(port = 7860) {
1155
1418
  app.delete("/api/tasks", async (req, res) => {
1156
1419
  try {
1157
1420
  const { projectId, taskId } = req.query;
1158
- const projectData = await storage.readProject(projectId);
1159
- if (!projectData) {
1160
- res.status(404).json({ error: "Project not found" });
1421
+ const result = await TaskService.delete(projectId, taskId);
1422
+ if (!result.success) {
1423
+ const statusCode = result.code === "NOT_FOUND" ? 404 : 400;
1424
+ res.status(statusCode).json({ error: result.error });
1161
1425
  return;
1162
1426
  }
1163
- const taskIndex = projectData.tasks.findIndex((t) => t.id === taskId);
1164
- if (taskIndex === -1) {
1165
- res.status(404).json({ error: "Task not found" });
1427
+ res.json({ success: true });
1428
+ } catch (error) {
1429
+ res.status(500).json({ error: error.message });
1430
+ }
1431
+ });
1432
+ app.post("/api/projects/:projectId/tags", async (req, res) => {
1433
+ try {
1434
+ const { projectId } = req.params;
1435
+ const result = await tagService2.create(projectId, req.body);
1436
+ if (!result.success) {
1437
+ const statusCode = result.code === "NOT_FOUND" ? 404 : 400;
1438
+ res.status(statusCode).json({ error: result.error });
1166
1439
  return;
1167
1440
  }
1168
- projectData.tasks.splice(taskIndex, 1);
1169
- projectData.project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1170
- const filePath = storage.getFilePath(projectId);
1171
- const { writeJsonFile: writeJsonFile2 } = await Promise.resolve().then(() => (init_file_helpers(), file_helpers_exports));
1172
- await writeJsonFile2(filePath, projectData);
1173
- res.json({ success: true });
1441
+ res.json({ success: true, data: result.data });
1174
1442
  } catch (error) {
1175
1443
  res.status(500).json({ error: error.message });
1176
1444
  }
1177
1445
  });
1178
- const distPath = path4.join(process.cwd(), "dist", "web", "app");
1446
+ app.get("/api/projects/:projectId/tags", async (req, res) => {
1447
+ try {
1448
+ const { projectId } = req.params;
1449
+ const result = await tagService2.list(projectId);
1450
+ if (!result.success) {
1451
+ const statusCode = result.code === "NOT_FOUND" ? 404 : 400;
1452
+ res.status(statusCode).json({ error: result.error });
1453
+ return;
1454
+ }
1455
+ res.json({ success: true, data: result.data });
1456
+ } catch (error) {
1457
+ res.status(500).json({ error: error.message });
1458
+ }
1459
+ });
1460
+ app.put("/api/projects/:projectId/tags/:tagId", async (req, res) => {
1461
+ try {
1462
+ const { projectId, tagId } = req.params;
1463
+ const result = await tagService2.update(projectId, tagId, req.body);
1464
+ if (!result.success) {
1465
+ const statusCode = result.code === "NOT_FOUND" ? 404 : 400;
1466
+ res.status(statusCode).json({ error: result.error });
1467
+ return;
1468
+ }
1469
+ res.json({ success: true, data: result.data });
1470
+ } catch (error) {
1471
+ res.status(500).json({ error: error.message });
1472
+ }
1473
+ });
1474
+ app.delete("/api/projects/:projectId/tags/:tagId", async (req, res) => {
1475
+ try {
1476
+ const { projectId, tagId } = req.params;
1477
+ const result = await tagService2.delete(projectId, tagId);
1478
+ if (!result.success) {
1479
+ const statusCode = result.code === "NOT_FOUND" ? 404 : 400;
1480
+ res.status(statusCode).json({ error: result.error });
1481
+ return;
1482
+ }
1483
+ res.json({ success: true, data: result.data });
1484
+ } catch (error) {
1485
+ res.status(500).json({ error: error.message });
1486
+ }
1487
+ });
1488
+ app.get("/api/backup", async (_req, res) => {
1489
+ try {
1490
+ const backup = await storage.exportAllData();
1491
+ const filename = `roadmap-skill-backup-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.json`;
1492
+ res.setHeader("Content-Type", "application/json");
1493
+ res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
1494
+ res.json(backup);
1495
+ } catch (error) {
1496
+ res.status(500).json({ error: error.message });
1497
+ }
1498
+ });
1499
+ app.post("/api/backup", async (req, res) => {
1500
+ try {
1501
+ const result = await storage.importAllData(req.body);
1502
+ res.json(result);
1503
+ } catch (error) {
1504
+ res.status(400).json({
1505
+ success: false,
1506
+ error: error.message
1507
+ });
1508
+ }
1509
+ });
1510
+ const distPath = path4.join(__dirname2, "app");
1179
1511
  app.use(express.static(distPath));
1180
1512
  app.get("*", (req, res) => {
1181
1513
  if (req.path.startsWith("/api")) {
@@ -1198,6 +1530,13 @@ function createServer(port = 7860) {
1198
1530
  });
1199
1531
  });
1200
1532
  }
1533
+ if (process.argv[1]?.endsWith("server.js")) {
1534
+ const port = parseInt(process.argv[2] || "7860", 10);
1535
+ createServer(port).catch((err) => {
1536
+ console.error("Failed to start server:", err);
1537
+ process.exit(1);
1538
+ });
1539
+ }
1201
1540
 
1202
1541
  // src/tools/web-tools.ts
1203
1542
  import open from "open";