@vibetasks/core 0.5.7 → 0.5.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.
package/dist/index.d.ts CHANGED
@@ -151,8 +151,38 @@ declare function createSupabaseClientFromEnv(): SupabaseClient<any, "public", "v
151
151
 
152
152
  type TaskFilter = 'all' | 'today' | 'upcoming' | 'completed' | 'archived';
153
153
  type TaskPriority = 'none' | 'low' | 'medium' | 'high';
154
- type TaskStatus = 'todo' | 'vibing' | 'done' | 'archived';
154
+ type TaskStatus = 'todo' | 'vibing' | 'done' | 'paused' | 'archived';
155
155
  type EnergyLevel = 'low' | 'medium' | 'high';
156
+ type ChangeType = 'created' | 'status_changed' | 'title_changed' | 'description_changed' | 'context_updated' | 'priority_changed' | 'due_date_changed' | 'subtask_added' | 'subtask_removed' | 'subtask_done' | 'subtask_undone' | 'tag_added' | 'tag_removed' | 'workspace_moved' | 'handoff' | 'revert' | 'archived' | 'restored';
157
+ interface TaskChange {
158
+ id: string;
159
+ task_id: string;
160
+ session_id?: string;
161
+ user_id?: string;
162
+ actor_type: 'ai' | 'human' | 'system';
163
+ change_type: ChangeType;
164
+ field_changed?: string;
165
+ old_value?: unknown;
166
+ new_value?: unknown;
167
+ message?: string;
168
+ reverted_change_id?: string;
169
+ created_at: string;
170
+ }
171
+ interface Workspace {
172
+ id: string;
173
+ user_id: string;
174
+ slug: string;
175
+ display_name: string;
176
+ full_path: string;
177
+ parent_id?: string;
178
+ depth: number;
179
+ color?: string;
180
+ description?: string;
181
+ settings: Record<string, unknown>;
182
+ archived_at?: string;
183
+ created_at: string;
184
+ updated_at: string;
185
+ }
156
186
  /**
157
187
  * Source type - where the task originated from
158
188
  * Matches TaskSourceType in @vibetasks/shared
@@ -265,15 +295,20 @@ interface Task {
265
295
  declare class TaskOperations {
266
296
  private supabase;
267
297
  constructor(supabase: SupabaseClient<any, any, any>);
298
+ private currentSessionId?;
268
299
  /**
269
- * Create TaskOperations from AuthManager
270
- * Automatically refreshes expired tokens
271
- */
300
+ * Set the current session ID for change tracking
301
+ */
302
+ setSessionId(sessionId: string): void;
303
+ /**
304
+ * Create TaskOperations from AuthManager
305
+ * Automatically refreshes expired tokens
306
+ */
272
307
  static fromAuthManager(authManager: AuthManager): Promise<TaskOperations>;
273
308
  getTasks(filter?: TaskFilter, includeArchived?: boolean): Promise<Task[]>;
274
309
  getTask(taskId: string): Promise<Task>;
275
310
  createTask(task: Partial<Task>): Promise<Task>;
276
- updateTask(taskId: string, updates: Partial<Task>): Promise<Task>;
311
+ updateTask(taskId: string, updates: Partial<Task>, logChanges?: boolean): Promise<Task>;
277
312
  deleteTask(taskId: string): Promise<void>;
278
313
  completeTask(taskId: string): Promise<Task>;
279
314
  uncompleteTask(taskId: string): Promise<Task>;
@@ -334,6 +369,127 @@ declare class TaskOperations {
334
369
  by_priority: Record<string, number>;
335
370
  needs_attention: boolean;
336
371
  }>;
372
+ /**
373
+ * Create a workspace document (research, domain doc, plan)
374
+ */
375
+ createWorkspaceDoc(input: {
376
+ doc_type: 'research' | 'domain' | 'plan';
377
+ title: string;
378
+ content: string;
379
+ project_tag?: string;
380
+ created_by?: 'ai' | 'human';
381
+ source_urls?: string[];
382
+ search_queries?: string[];
383
+ applies_to_children?: boolean;
384
+ tags?: string[];
385
+ globs?: string[];
386
+ }): Promise<any>;
387
+ /**
388
+ * Get workspace documents
389
+ */
390
+ getWorkspaceDocs(filters?: {
391
+ doc_type?: 'research' | 'domain' | 'plan';
392
+ project_tag?: string;
393
+ search?: string;
394
+ limit?: number;
395
+ }): Promise<any[]>;
396
+ /**
397
+ * Log a change to a task (like a git commit)
398
+ */
399
+ logChange(input: {
400
+ taskId: string;
401
+ changeType: ChangeType;
402
+ fieldChanged?: string;
403
+ oldValue?: unknown;
404
+ newValue?: unknown;
405
+ message?: string;
406
+ revertedChangeId?: string;
407
+ actorType?: 'ai' | 'human' | 'system';
408
+ }): Promise<TaskChange>;
409
+ /**
410
+ * Get change history for a task (like git log)
411
+ */
412
+ getTaskChanges(taskId: string, options?: {
413
+ limit?: number;
414
+ since?: string;
415
+ until?: string;
416
+ changeTypes?: ChangeType[];
417
+ }): Promise<TaskChange[]>;
418
+ /**
419
+ * Get a specific change (like git show)
420
+ */
421
+ getChange(changeId: string): Promise<TaskChange>;
422
+ /**
423
+ * Revert a change (non-destructive - creates a new "revert" change)
424
+ */
425
+ revertChange(changeId: string, message?: string): Promise<TaskChange>;
426
+ /**
427
+ * Get all tasks touched by a session
428
+ */
429
+ getSessionTasks(sessionId: string): Promise<{
430
+ task_id: string;
431
+ task_title: string;
432
+ change_count: number;
433
+ first_change: string;
434
+ last_change: string;
435
+ }[]>;
436
+ /**
437
+ * Search task changes by message
438
+ */
439
+ searchChanges(query: string, limit?: number): Promise<TaskChange[]>;
440
+ /**
441
+ * Create a workspace
442
+ */
443
+ createWorkspace(input: {
444
+ slug: string;
445
+ displayName: string;
446
+ parentId?: string;
447
+ color?: string;
448
+ description?: string;
449
+ }): Promise<Workspace>;
450
+ /**
451
+ * Get workspace tree for the current user
452
+ */
453
+ getWorkspaces(includeArchived?: boolean): Promise<Workspace[]>;
454
+ /**
455
+ * Get workspace by full path
456
+ */
457
+ getWorkspaceByPath(fullPath: string): Promise<Workspace | null>;
458
+ /**
459
+ * Get or create workspace from a project_tag (auto-creates hierarchy)
460
+ */
461
+ getOrCreateWorkspace(projectTag: string): Promise<Workspace>;
462
+ /**
463
+ * Archive a workspace (soft delete)
464
+ */
465
+ archiveWorkspace(workspaceId: string): Promise<Workspace>;
466
+ /**
467
+ * Restore a workspace from archive
468
+ */
469
+ restoreWorkspace(workspaceId: string): Promise<Workspace>;
470
+ /**
471
+ * Get workspace descendants (recursive)
472
+ */
473
+ getWorkspaceDescendants(workspaceId: string): Promise<{
474
+ id: string;
475
+ depth: number;
476
+ }[]>;
477
+ /**
478
+ * Generate consistent color for workspace
479
+ */
480
+ private generateWorkspaceColor;
481
+ /**
482
+ * Pause a task
483
+ */
484
+ pauseTask(taskId: string, reason?: string): Promise<Task>;
485
+ /**
486
+ * Resume a paused task
487
+ */
488
+ resumeTask(taskId: string): Promise<Task>;
489
+ /**
490
+ * Restore a task from archive
491
+ */
492
+ restoreTask(taskId: string, toStatus?: TaskStatus): Promise<Task>;
337
493
  }
338
494
 
339
495
  /**
@@ -441,4 +597,4 @@ declare class ClaudeSync {
441
597
  */
442
598
  declare function syncClaudeTodos(taskOps: TaskOperations, todos: ClaudeTodoItem[], options?: SyncOptions): Promise<SyncAllResult>;
443
599
 
444
- export { type Attachment, AuthManager, ClaudeSync, type ClaudeTodoItem, ConfigManager, type SourceType, type Subtask, type SupabaseConfig, type SyncAllResult, type SyncOptions, type SyncResult, type Tag, type Task, type TaskFilter, type TaskFlowConfig, TaskOperations, type TaskPriority, type TaskStatus, createSupabaseClient, createSupabaseClientFromEnv, syncClaudeTodos };
600
+ export { type Attachment, AuthManager, type ChangeType, ClaudeSync, type ClaudeTodoItem, ConfigManager, type SourceType, type Subtask, type SupabaseConfig, type SyncAllResult, type SyncOptions, type SyncResult, type Tag, type Task, type TaskChange, type TaskFilter, type TaskFlowConfig, TaskOperations, type TaskPriority, type TaskStatus, type Workspace, createSupabaseClient, createSupabaseClientFromEnv, syncClaudeTodos };
package/dist/index.js CHANGED
@@ -378,10 +378,18 @@ var TaskOperations = class _TaskOperations {
378
378
  constructor(supabase) {
379
379
  this.supabase = supabase;
380
380
  }
381
+ // Current session ID for change tracking
382
+ currentSessionId;
381
383
  /**
382
- * Create TaskOperations from AuthManager
383
- * Automatically refreshes expired tokens
384
- */
384
+ * Set the current session ID for change tracking
385
+ */
386
+ setSessionId(sessionId) {
387
+ this.currentSessionId = sessionId;
388
+ }
389
+ /**
390
+ * Create TaskOperations from AuthManager
391
+ * Automatically refreshes expired tokens
392
+ */
385
393
  static async fromAuthManager(authManager) {
386
394
  const supabaseUrl = await authManager.getConfig("supabase_url") || "https://ihmayqzxqyednchbezya.supabase.co";
387
395
  const supabaseKey = await authManager.getConfig("supabase_key") || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNia2t6dGJjb2l0cmZjbGVnaGZkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Njc3NTc0MjgsImV4cCI6MjA4MzMzMzQyOH0.G7ILx-nntP0NbxO1gKt5yASb7nt7OmpJ8qtykeGYbQA";
@@ -482,9 +490,16 @@ var TaskOperations = class _TaskOperations {
482
490
  if (task.tags && task.tags.length > 0) {
483
491
  await this.linkTaskTags(data.id, task.tags.map((t) => t.id));
484
492
  }
493
+ await this.logChange({
494
+ taskId: data.id,
495
+ changeType: "created",
496
+ message: `Created task: "${task.title}"`,
497
+ actorType: task.created_by === "human" ? "human" : "ai"
498
+ });
485
499
  return data;
486
500
  }
487
- async updateTask(taskId, updates) {
501
+ async updateTask(taskId, updates, logChanges = true) {
502
+ const oldTask = logChanges ? await this.getTask(taskId) : null;
488
503
  const updateData = {};
489
504
  if (updates.title !== void 0) updateData.title = updates.title;
490
505
  if (updates.description !== void 0) updateData.description = updates.description;
@@ -516,6 +531,85 @@ var TaskOperations = class _TaskOperations {
516
531
  await this.linkTaskTags(taskId, updates.tags.map((t) => t.id));
517
532
  }
518
533
  }
534
+ if (logChanges && oldTask) {
535
+ if (updates.status !== void 0 && updates.status !== oldTask.status) {
536
+ await this.logChange({
537
+ taskId,
538
+ changeType: "status_changed",
539
+ fieldChanged: "status",
540
+ oldValue: oldTask.status,
541
+ newValue: updates.status,
542
+ message: `Status: ${oldTask.status} \u2192 ${updates.status}`
543
+ });
544
+ }
545
+ if (updates.title !== void 0 && updates.title !== oldTask.title) {
546
+ await this.logChange({
547
+ taskId,
548
+ changeType: "title_changed",
549
+ fieldChanged: "title",
550
+ oldValue: oldTask.title,
551
+ newValue: updates.title,
552
+ message: `Title changed`
553
+ });
554
+ }
555
+ if (updates.context_notes !== void 0 && updates.context_notes !== oldTask.context_notes) {
556
+ await this.logChange({
557
+ taskId,
558
+ changeType: "context_updated",
559
+ fieldChanged: "context_notes",
560
+ oldValue: oldTask.context_notes,
561
+ newValue: updates.context_notes,
562
+ message: `Updated context notes`
563
+ });
564
+ }
565
+ if (updates.priority !== void 0 && updates.priority !== oldTask.priority) {
566
+ await this.logChange({
567
+ taskId,
568
+ changeType: "priority_changed",
569
+ fieldChanged: "priority",
570
+ oldValue: oldTask.priority,
571
+ newValue: updates.priority,
572
+ message: `Priority: ${oldTask.priority} \u2192 ${updates.priority}`
573
+ });
574
+ }
575
+ if (updates.subtasks_json !== void 0 && oldTask.subtasks_json) {
576
+ const oldSubtasks = oldTask.subtasks_json || [];
577
+ const newSubtasks = updates.subtasks_json || [];
578
+ for (const newSub of newSubtasks) {
579
+ const oldSub = oldSubtasks.find((s) => s.id === newSub.id);
580
+ if (oldSub && !oldSub.done && newSub.done) {
581
+ await this.logChange({
582
+ taskId,
583
+ changeType: "subtask_done",
584
+ fieldChanged: "subtasks",
585
+ oldValue: { id: newSub.id, done: false },
586
+ newValue: { id: newSub.id, done: true },
587
+ message: `Completed: ${newSub.title}`
588
+ });
589
+ } else if (oldSub && oldSub.done && !newSub.done) {
590
+ await this.logChange({
591
+ taskId,
592
+ changeType: "subtask_undone",
593
+ fieldChanged: "subtasks",
594
+ oldValue: { id: newSub.id, done: true },
595
+ newValue: { id: newSub.id, done: false },
596
+ message: `Unchecked: ${newSub.title}`
597
+ });
598
+ }
599
+ }
600
+ for (const newSub of newSubtasks) {
601
+ if (!oldSubtasks.find((s) => s.id === newSub.id)) {
602
+ await this.logChange({
603
+ taskId,
604
+ changeType: "subtask_added",
605
+ fieldChanged: "subtasks",
606
+ newValue: newSub,
607
+ message: `Added subtask: ${newSub.title}`
608
+ });
609
+ }
610
+ }
611
+ }
612
+ }
519
613
  return data;
520
614
  }
521
615
  async deleteTask(taskId) {
@@ -523,12 +617,29 @@ var TaskOperations = class _TaskOperations {
523
617
  if (error) throw error;
524
618
  }
525
619
  async completeTask(taskId) {
526
- const { data, error } = await this.supabase.from("tasks").update({
620
+ const { data: existingTask } = await this.supabase.from("tasks").select("subtasks, acceptance_criteria, status, title").eq("id", taskId).single();
621
+ const oldStatus = existingTask?.status || "todo";
622
+ const updatePayload = {
527
623
  completed: true,
528
624
  completed_at: (/* @__PURE__ */ new Date()).toISOString(),
529
625
  status: "done"
530
- }).eq("id", taskId).select().single();
626
+ };
627
+ if (existingTask?.subtasks && Array.isArray(existingTask.subtasks)) {
628
+ updatePayload.subtasks = existingTask.subtasks.map((s) => ({ ...s, done: true }));
629
+ }
630
+ if (existingTask?.acceptance_criteria && Array.isArray(existingTask.acceptance_criteria)) {
631
+ updatePayload.acceptance_criteria = existingTask.acceptance_criteria.map((c) => ({ ...c, done: true }));
632
+ }
633
+ const { data, error } = await this.supabase.from("tasks").update(updatePayload).eq("id", taskId).select().single();
531
634
  if (error) throw error;
635
+ await this.logChange({
636
+ taskId,
637
+ changeType: "status_changed",
638
+ fieldChanged: "status",
639
+ oldValue: oldStatus,
640
+ newValue: "done",
641
+ message: `Completed: ${existingTask?.title || "task"}`
642
+ });
532
643
  return data;
533
644
  }
534
645
  async uncompleteTask(taskId) {
@@ -547,11 +658,20 @@ var TaskOperations = class _TaskOperations {
547
658
  * Typically used for completed tasks you want to hide from views
548
659
  */
549
660
  async archiveTask(taskId) {
661
+ const oldTask = await this.getTask(taskId);
550
662
  const { data, error } = await this.supabase.from("tasks").update({
551
663
  status: "archived",
552
664
  archived_at: (/* @__PURE__ */ new Date()).toISOString()
553
665
  }).eq("id", taskId).select().single();
554
666
  if (error) throw error;
667
+ await this.logChange({
668
+ taskId,
669
+ changeType: "archived",
670
+ fieldChanged: "status",
671
+ oldValue: oldTask.status,
672
+ newValue: "archived",
673
+ message: `Archived: ${oldTask.title}`
674
+ });
555
675
  return data;
556
676
  }
557
677
  /**
@@ -757,6 +877,340 @@ var TaskOperations = class _TaskOperations {
757
877
  needs_attention: unacknowledged.length > 3 || byPriority.high > 0
758
878
  };
759
879
  }
880
+ // ============================================
881
+ // WORKSPACE DOC OPERATIONS
882
+ // ============================================
883
+ /**
884
+ * Create a workspace document (research, domain doc, plan)
885
+ */
886
+ async createWorkspaceDoc(input) {
887
+ const { data: { user } } = await this.supabase.auth.getUser();
888
+ if (!user) {
889
+ throw new Error("Not authenticated");
890
+ }
891
+ const { data, error } = await this.supabase.from("workspace_docs").insert({
892
+ user_id: user.id,
893
+ doc_type: input.doc_type,
894
+ title: input.title,
895
+ content: input.content,
896
+ project_tag: input.project_tag || "sparktory",
897
+ created_by: input.created_by || "ai",
898
+ source_urls: input.source_urls || [],
899
+ search_queries: input.search_queries || [],
900
+ applies_to_children: input.applies_to_children ?? true,
901
+ tags: input.tags || [],
902
+ globs: input.globs || []
903
+ }).select().single();
904
+ if (error) {
905
+ throw new Error(`Failed to create workspace doc: ${error.message}`);
906
+ }
907
+ return data;
908
+ }
909
+ /**
910
+ * Get workspace documents
911
+ */
912
+ async getWorkspaceDocs(filters) {
913
+ let query = this.supabase.from("workspace_docs").select("*").order("created_at", { ascending: false });
914
+ if (filters?.doc_type) {
915
+ query = query.eq("doc_type", filters.doc_type);
916
+ }
917
+ if (filters?.project_tag) {
918
+ query = query.eq("project_tag", filters.project_tag);
919
+ }
920
+ if (filters?.search) {
921
+ query = query.or(`title.ilike.%${filters.search}%,content.ilike.%${filters.search}%`);
922
+ }
923
+ if (filters?.limit) {
924
+ query = query.limit(filters.limit);
925
+ }
926
+ const { data, error } = await query;
927
+ if (error) {
928
+ throw new Error(`Failed to get workspace docs: ${error.message}`);
929
+ }
930
+ return data || [];
931
+ }
932
+ // ============================================
933
+ // CHANGE TRACKING (Git-for-Tasks)
934
+ // ============================================
935
+ /**
936
+ * Log a change to a task (like a git commit)
937
+ */
938
+ async logChange(input) {
939
+ const { data: { user } } = await this.supabase.auth.getUser();
940
+ if (!user) throw new Error("Not authenticated");
941
+ const { data, error } = await this.supabase.from("task_changes").insert({
942
+ task_id: input.taskId,
943
+ user_id: user.id,
944
+ session_id: this.currentSessionId,
945
+ actor_type: input.actorType || "ai",
946
+ change_type: input.changeType,
947
+ field_changed: input.fieldChanged,
948
+ old_value: input.oldValue,
949
+ new_value: input.newValue,
950
+ message: input.message,
951
+ reverted_change_id: input.revertedChangeId
952
+ }).select().single();
953
+ if (error) throw error;
954
+ return data;
955
+ }
956
+ /**
957
+ * Get change history for a task (like git log)
958
+ */
959
+ async getTaskChanges(taskId, options) {
960
+ let query = this.supabase.from("task_changes").select("*").eq("task_id", taskId).order("created_at", { ascending: false });
961
+ if (options?.limit) {
962
+ query = query.limit(options.limit);
963
+ }
964
+ if (options?.since) {
965
+ query = query.gte("created_at", options.since);
966
+ }
967
+ if (options?.until) {
968
+ query = query.lte("created_at", options.until);
969
+ }
970
+ if (options?.changeTypes && options.changeTypes.length > 0) {
971
+ query = query.in("change_type", options.changeTypes);
972
+ }
973
+ const { data, error } = await query;
974
+ if (error) throw error;
975
+ return data || [];
976
+ }
977
+ /**
978
+ * Get a specific change (like git show)
979
+ */
980
+ async getChange(changeId) {
981
+ const { data, error } = await this.supabase.from("task_changes").select("*").eq("id", changeId).single();
982
+ if (error) throw error;
983
+ return data;
984
+ }
985
+ /**
986
+ * Revert a change (non-destructive - creates a new "revert" change)
987
+ */
988
+ async revertChange(changeId, message) {
989
+ const change = await this.getChange(changeId);
990
+ const task = await this.getTask(change.task_id);
991
+ if (change.field_changed && change.old_value !== void 0) {
992
+ const updates = {};
993
+ updates[change.field_changed] = change.old_value;
994
+ await this.updateTask(change.task_id, updates);
995
+ }
996
+ return await this.logChange({
997
+ taskId: change.task_id,
998
+ changeType: "revert",
999
+ fieldChanged: change.field_changed,
1000
+ oldValue: change.new_value,
1001
+ newValue: change.old_value,
1002
+ message: message || `Reverted: ${change.message || change.change_type}`,
1003
+ revertedChangeId: changeId
1004
+ });
1005
+ }
1006
+ /**
1007
+ * Get all tasks touched by a session
1008
+ */
1009
+ async getSessionTasks(sessionId) {
1010
+ const { data, error } = await this.supabase.rpc("get_session_tasks", { p_session_id: sessionId });
1011
+ if (error) throw error;
1012
+ return data || [];
1013
+ }
1014
+ /**
1015
+ * Search task changes by message
1016
+ */
1017
+ async searchChanges(query, limit = 50) {
1018
+ const { data: { user } } = await this.supabase.auth.getUser();
1019
+ if (!user) throw new Error("Not authenticated");
1020
+ const { data, error } = await this.supabase.rpc("search_task_changes", {
1021
+ p_user_id: user.id,
1022
+ p_query: query,
1023
+ p_limit: limit
1024
+ });
1025
+ if (error) throw error;
1026
+ return data || [];
1027
+ }
1028
+ // ============================================
1029
+ // WORKSPACE OPERATIONS
1030
+ // ============================================
1031
+ /**
1032
+ * Create a workspace
1033
+ */
1034
+ async createWorkspace(input) {
1035
+ const { data: { user } } = await this.supabase.auth.getUser();
1036
+ if (!user) throw new Error("Not authenticated");
1037
+ let fullPath = input.displayName;
1038
+ let depth = 0;
1039
+ if (input.parentId) {
1040
+ const { data: parent } = await this.supabase.from("workspaces").select("full_path, depth").eq("id", input.parentId).single();
1041
+ if (parent) {
1042
+ fullPath = `${parent.full_path} > ${input.displayName}`;
1043
+ depth = parent.depth + 1;
1044
+ }
1045
+ }
1046
+ const { data, error } = await this.supabase.from("workspaces").insert({
1047
+ user_id: user.id,
1048
+ slug: input.slug,
1049
+ display_name: input.displayName,
1050
+ full_path: fullPath,
1051
+ parent_id: input.parentId,
1052
+ depth,
1053
+ color: input.color,
1054
+ description: input.description
1055
+ }).select().single();
1056
+ if (error) throw error;
1057
+ return data;
1058
+ }
1059
+ /**
1060
+ * Get workspace tree for the current user
1061
+ */
1062
+ async getWorkspaces(includeArchived = false) {
1063
+ let query = this.supabase.from("workspaces").select("*").order("full_path", { ascending: true });
1064
+ if (!includeArchived) {
1065
+ query = query.is("archived_at", null);
1066
+ }
1067
+ const { data, error } = await query;
1068
+ if (error) throw error;
1069
+ return data || [];
1070
+ }
1071
+ /**
1072
+ * Get workspace by full path
1073
+ */
1074
+ async getWorkspaceByPath(fullPath) {
1075
+ const { data, error } = await this.supabase.from("workspaces").select("*").eq("full_path", fullPath).single();
1076
+ if (error && error.code !== "PGRST116") throw error;
1077
+ return data;
1078
+ }
1079
+ /**
1080
+ * Get or create workspace from a project_tag (auto-creates hierarchy)
1081
+ */
1082
+ async getOrCreateWorkspace(projectTag) {
1083
+ const existing = await this.getWorkspaceByPath(projectTag);
1084
+ if (existing) return existing;
1085
+ const { data: { user } } = await this.supabase.auth.getUser();
1086
+ if (!user) throw new Error("Not authenticated");
1087
+ const parts = projectTag.split(/\s*>\s*/).map((p) => p.trim()).filter(Boolean);
1088
+ let parentId;
1089
+ let currentPath = "";
1090
+ for (let i = 0; i < parts.length; i++) {
1091
+ const displayName = parts[i];
1092
+ const slug = displayName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
1093
+ currentPath = i === 0 ? displayName : `${currentPath} > ${displayName}`;
1094
+ const existing2 = await this.getWorkspaceByPath(currentPath);
1095
+ if (existing2) {
1096
+ parentId = existing2.id;
1097
+ continue;
1098
+ }
1099
+ const { data, error } = await this.supabase.from("workspaces").insert({
1100
+ user_id: user.id,
1101
+ slug,
1102
+ display_name: displayName,
1103
+ full_path: currentPath,
1104
+ parent_id: parentId,
1105
+ depth: i,
1106
+ color: this.generateWorkspaceColor(currentPath)
1107
+ }).select().single();
1108
+ if (error) throw error;
1109
+ parentId = data.id;
1110
+ }
1111
+ return await this.getWorkspaceByPath(projectTag);
1112
+ }
1113
+ /**
1114
+ * Archive a workspace (soft delete)
1115
+ */
1116
+ async archiveWorkspace(workspaceId) {
1117
+ const { data, error } = await this.supabase.from("workspaces").update({ archived_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", workspaceId).select().single();
1118
+ if (error) throw error;
1119
+ return data;
1120
+ }
1121
+ /**
1122
+ * Restore a workspace from archive
1123
+ */
1124
+ async restoreWorkspace(workspaceId) {
1125
+ const { data, error } = await this.supabase.from("workspaces").update({ archived_at: null }).eq("id", workspaceId).select().single();
1126
+ if (error) throw error;
1127
+ return data;
1128
+ }
1129
+ /**
1130
+ * Get workspace descendants (recursive)
1131
+ */
1132
+ async getWorkspaceDescendants(workspaceId) {
1133
+ const { data, error } = await this.supabase.rpc("get_workspace_descendants", { root_id: workspaceId });
1134
+ if (error) throw error;
1135
+ return data || [];
1136
+ }
1137
+ /**
1138
+ * Generate consistent color for workspace
1139
+ */
1140
+ generateWorkspaceColor(name) {
1141
+ const colors = [
1142
+ "#8b5cf6",
1143
+ "#3b82f6",
1144
+ "#10b981",
1145
+ "#f59e0b",
1146
+ "#ef4444",
1147
+ "#06b6d4",
1148
+ "#ec4899",
1149
+ "#84cc16"
1150
+ ];
1151
+ let hash = 0;
1152
+ for (let i = 0; i < name.length; i++) {
1153
+ hash = name.charCodeAt(i) + ((hash << 5) - hash);
1154
+ }
1155
+ return colors[Math.abs(hash) % colors.length];
1156
+ }
1157
+ // ============================================
1158
+ // PAUSE/RESUME OPERATIONS
1159
+ // ============================================
1160
+ /**
1161
+ * Pause a task
1162
+ */
1163
+ async pauseTask(taskId, reason) {
1164
+ const oldTask = await this.getTask(taskId);
1165
+ const { data, error } = await this.supabase.from("tasks").update({ status: "paused" }).eq("id", taskId).select().single();
1166
+ if (error) throw error;
1167
+ await this.logChange({
1168
+ taskId,
1169
+ changeType: "status_changed",
1170
+ fieldChanged: "status",
1171
+ oldValue: oldTask.status,
1172
+ newValue: "paused",
1173
+ message: reason || `Paused task`
1174
+ });
1175
+ return data;
1176
+ }
1177
+ /**
1178
+ * Resume a paused task
1179
+ */
1180
+ async resumeTask(taskId) {
1181
+ const oldTask = await this.getTask(taskId);
1182
+ const { data, error } = await this.supabase.from("tasks").update({ status: "vibing" }).eq("id", taskId).select().single();
1183
+ if (error) throw error;
1184
+ await this.logChange({
1185
+ taskId,
1186
+ changeType: "status_changed",
1187
+ fieldChanged: "status",
1188
+ oldValue: oldTask.status,
1189
+ newValue: "vibing",
1190
+ message: `Resumed task`
1191
+ });
1192
+ return data;
1193
+ }
1194
+ /**
1195
+ * Restore a task from archive
1196
+ */
1197
+ async restoreTask(taskId, toStatus = "todo") {
1198
+ const oldTask = await this.getTask(taskId);
1199
+ const { data, error } = await this.supabase.from("tasks").update({
1200
+ status: toStatus,
1201
+ archived_at: null
1202
+ }).eq("id", taskId).select().single();
1203
+ if (error) throw error;
1204
+ await this.logChange({
1205
+ taskId,
1206
+ changeType: "restored",
1207
+ fieldChanged: "status",
1208
+ oldValue: oldTask.status,
1209
+ newValue: toStatus,
1210
+ message: `Restored from archive`
1211
+ });
1212
+ return data;
1213
+ }
760
1214
  };
761
1215
 
762
1216
  // src/claude-sync.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibetasks/core",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
4
4
  "description": "Shared core logic for VibeTasks MCP server and CLI - authentication, task operations, and config management",
5
5
  "author": "Vyas",
6
6
  "license": "MIT",