@vibetasks/core 0.5.6 → 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";
@@ -448,6 +456,8 @@ var TaskOperations = class _TaskOperations {
448
456
  async createTask(task) {
449
457
  const { data: { user } } = await this.supabase.auth.getUser();
450
458
  if (!user) throw new Error("Not authenticated");
459
+ const { data: maxIdResult } = await this.supabase.from("tasks").select("short_id").eq("user_id", user.id).order("short_id", { ascending: false }).limit(1).single();
460
+ const nextShortId = (maxIdResult?.short_id || 0) + 1;
451
461
  const { data, error } = await this.supabase.from("tasks").insert({
452
462
  user_id: user.id,
453
463
  title: task.title,
@@ -470,15 +480,26 @@ var TaskOperations = class _TaskOperations {
470
480
  project_tag: task.project_tag,
471
481
  created_by: task.created_by,
472
482
  context_notes: task.context_notes,
473
- energy_required: task.energy_required
483
+ energy_required: task.energy_required,
484
+ short_id: nextShortId,
485
+ // Stable short ID
486
+ session_id: task.session_id,
487
+ session_history: task.session_history || []
474
488
  }).select().single();
475
489
  if (error) throw error;
476
490
  if (task.tags && task.tags.length > 0) {
477
491
  await this.linkTaskTags(data.id, task.tags.map((t) => t.id));
478
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
+ });
479
499
  return data;
480
500
  }
481
- async updateTask(taskId, updates) {
501
+ async updateTask(taskId, updates, logChanges = true) {
502
+ const oldTask = logChanges ? await this.getTask(taskId) : null;
482
503
  const updateData = {};
483
504
  if (updates.title !== void 0) updateData.title = updates.title;
484
505
  if (updates.description !== void 0) updateData.description = updates.description;
@@ -510,6 +531,85 @@ var TaskOperations = class _TaskOperations {
510
531
  await this.linkTaskTags(taskId, updates.tags.map((t) => t.id));
511
532
  }
512
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
+ }
513
613
  return data;
514
614
  }
515
615
  async deleteTask(taskId) {
@@ -517,12 +617,29 @@ var TaskOperations = class _TaskOperations {
517
617
  if (error) throw error;
518
618
  }
519
619
  async completeTask(taskId) {
520
- 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 = {
521
623
  completed: true,
522
624
  completed_at: (/* @__PURE__ */ new Date()).toISOString(),
523
625
  status: "done"
524
- }).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();
525
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
+ });
526
643
  return data;
527
644
  }
528
645
  async uncompleteTask(taskId) {
@@ -541,11 +658,20 @@ var TaskOperations = class _TaskOperations {
541
658
  * Typically used for completed tasks you want to hide from views
542
659
  */
543
660
  async archiveTask(taskId) {
661
+ const oldTask = await this.getTask(taskId);
544
662
  const { data, error } = await this.supabase.from("tasks").update({
545
663
  status: "archived",
546
664
  archived_at: (/* @__PURE__ */ new Date()).toISOString()
547
665
  }).eq("id", taskId).select().single();
548
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
+ });
549
675
  return data;
550
676
  }
551
677
  /**
@@ -751,6 +877,340 @@ var TaskOperations = class _TaskOperations {
751
877
  needs_attention: unacknowledged.length > 3 || byPriority.high > 0
752
878
  };
753
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
+ }
754
1214
  };
755
1215
 
756
1216
  // src/claude-sync.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibetasks/core",
3
- "version": "0.5.6",
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",