@vibetasks/core 0.2.1 → 0.4.0

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
@@ -125,20 +125,39 @@ interface Tag {
125
125
  created_at?: string;
126
126
  }
127
127
  /**
128
- * Task status - simple 3-state flow for VibeTasks
128
+ * Task status - simple 3-state flow + archive for VibeTasks
129
129
  * - todo: Not started yet, waiting in backlog
130
130
  * - vibing: Currently in progress (actively working on it)
131
131
  * - done: Completed
132
+ * - archived: Archived (hidden from default views)
132
133
  */
133
- type TaskStatus = 'todo' | 'vibing' | 'done';
134
+ type TaskStatus = 'todo' | 'vibing' | 'done' | 'archived';
135
+ interface Subtask {
136
+ id: string;
137
+ title: string;
138
+ done: boolean;
139
+ notes?: string;
140
+ }
141
+ interface Attachment {
142
+ id: string;
143
+ task_id: string;
144
+ file_name: string;
145
+ file_type: string;
146
+ storage_path: string;
147
+ is_image?: boolean;
148
+ alt_text?: string;
149
+ ai_description?: string;
150
+ }
134
151
  interface Task {
135
152
  id: string;
136
153
  user_id?: string;
137
154
  title: string;
155
+ description?: string;
138
156
  notes?: string;
139
157
  notes_format?: 'html' | 'markdown';
140
158
  completed?: boolean;
141
159
  completed_at?: string;
160
+ archived_at?: string;
142
161
  due_date?: string;
143
162
  start_date?: string;
144
163
  duration_minutes?: number;
@@ -151,8 +170,9 @@ interface Task {
151
170
  created_at?: string;
152
171
  updated_at?: string;
153
172
  tags?: Tag[];
154
- attachments?: any[];
173
+ attachments?: Attachment[];
155
174
  subtasks?: Task[];
175
+ subtasks_json?: Subtask[];
156
176
  status?: TaskStatus;
157
177
  project_tag?: string;
158
178
  created_by?: 'ai' | 'human';
@@ -170,22 +190,150 @@ declare class TaskOperations {
170
190
  * Create TaskOperations from AuthManager
171
191
  */
172
192
  static fromAuthManager(authManager: AuthManager): Promise<TaskOperations>;
173
- getTasks(filter?: TaskFilter): Promise<Task[]>;
193
+ getTasks(filter?: TaskFilter, includeArchived?: boolean): Promise<Task[]>;
174
194
  getTask(taskId: string): Promise<Task>;
175
195
  createTask(task: Partial<Task>): Promise<Task>;
176
196
  updateTask(taskId: string, updates: Partial<Task>): Promise<Task>;
177
197
  deleteTask(taskId: string): Promise<void>;
178
198
  completeTask(taskId: string): Promise<Task>;
179
199
  uncompleteTask(taskId: string): Promise<Task>;
200
+ /**
201
+ * Archive a task - moves it to archived status
202
+ * Typically used for completed tasks you want to hide from views
203
+ */
204
+ archiveTask(taskId: string): Promise<Task>;
205
+ /**
206
+ * Unarchive a task - restores it to its previous state (done by default)
207
+ */
208
+ unarchiveTask(taskId: string, restoreStatus?: TaskStatus): Promise<Task>;
209
+ /**
210
+ * Get all archived tasks
211
+ */
212
+ getArchivedTasks(limit?: number): Promise<Task[]>;
180
213
  getTags(): Promise<Tag[]>;
181
214
  createTag(name: string, color?: string): Promise<Tag>;
182
215
  linkTaskTags(taskId: string, tagIds: string[]): Promise<void>;
183
216
  unlinkAllTaskTags(taskId: string): Promise<void>;
184
- searchTasks(query: string, limit?: number): Promise<Task[]>;
217
+ searchTasks(query: string, limit?: number, includeArchived?: boolean): Promise<Task[]>;
185
218
  /**
186
219
  * Find tag by name (case-insensitive), create if doesn't exist
187
220
  */
188
221
  findOrCreateTag(name: string, color?: string): Promise<Tag>;
222
+ /**
223
+ * Get a signed URL for an attachment (valid for 1 hour)
224
+ */
225
+ getAttachmentUrl(storagePath: string): Promise<string>;
226
+ /**
227
+ * Get all attachments for a task with signed URLs
228
+ */
229
+ getTaskAttachments(taskId: string): Promise<(Attachment & {
230
+ url: string;
231
+ })[]>;
189
232
  }
190
233
 
191
- export { AuthManager, ConfigManager, type SupabaseConfig, type Tag, type Task, type TaskFilter, type TaskFlowConfig, TaskOperations, type TaskPriority, type TaskStatus, createSupabaseClient, createSupabaseClientFromEnv };
234
+ /**
235
+ * Claude Code TodoWrite Sync
236
+ *
237
+ * This module provides synchronization between Claude's TodoWrite state
238
+ * and VibeTasks task statuses.
239
+ *
240
+ * Status mapping:
241
+ * Claude "pending" -> VibeTasks "todo"
242
+ * Claude "in_progress" -> VibeTasks "vibing"
243
+ * Claude "completed" -> VibeTasks "done"
244
+ */
245
+
246
+ /**
247
+ * Claude Code TodoWrite item structure
248
+ */
249
+ interface ClaudeTodoItem {
250
+ /** The task description/content */
251
+ content: string;
252
+ /** Current status: pending, in_progress, or completed */
253
+ status: 'pending' | 'in_progress' | 'completed';
254
+ /** Active form description (used during in_progress) */
255
+ activeForm?: string;
256
+ }
257
+ /**
258
+ * Result of syncing a single todo item
259
+ */
260
+ interface SyncResult {
261
+ /** The Claude todo content */
262
+ content: string;
263
+ /** Whether a matching VibeTasks task was found */
264
+ matched: boolean;
265
+ /** The VibeTasks task ID if matched */
266
+ taskId?: string;
267
+ /** The action taken */
268
+ action: 'created' | 'updated' | 'skipped' | 'not_found';
269
+ /** Previous status (if updated) */
270
+ previousStatus?: TaskStatus;
271
+ /** New status (if updated) */
272
+ newStatus?: TaskStatus;
273
+ /** Error message if any */
274
+ error?: string;
275
+ }
276
+ /**
277
+ * Result of syncing all todos
278
+ */
279
+ interface SyncAllResult {
280
+ /** Whether the overall sync was successful */
281
+ success: boolean;
282
+ /** Number of todos processed */
283
+ processed: number;
284
+ /** Number of todos synced successfully */
285
+ synced: number;
286
+ /** Number of todos that failed to sync */
287
+ failed: number;
288
+ /** Individual sync results */
289
+ results: SyncResult[];
290
+ }
291
+ /**
292
+ * Options for the sync operation
293
+ */
294
+ interface SyncOptions {
295
+ /** Create new tasks for unmatched todos (default: false) */
296
+ createMissing?: boolean;
297
+ /** Project tag to use for created tasks */
298
+ projectTag?: string;
299
+ /** Minimum similarity score for matching (0-1, default: 0.6) */
300
+ matchThreshold?: number;
301
+ /** Whether to sync 'pending' todos as 'todo' status (default: true) */
302
+ syncPending?: boolean;
303
+ }
304
+ /**
305
+ * Claude Sync class for managing TodoWrite synchronization
306
+ */
307
+ declare class ClaudeSync {
308
+ private taskOps;
309
+ constructor(taskOps: TaskOperations);
310
+ /**
311
+ * Sync a single Claude todo item with VibeTasks
312
+ */
313
+ syncTodo(todo: ClaudeTodoItem, existingTasks?: Task[], options?: SyncOptions): Promise<SyncResult>;
314
+ /**
315
+ * Sync all Claude todos with VibeTasks
316
+ */
317
+ syncAllTodos(todos: ClaudeTodoItem[], options?: SyncOptions): Promise<SyncAllResult>;
318
+ /**
319
+ * Get current vibing tasks (for comparison with Claude's in_progress)
320
+ */
321
+ getVibingTasks(): Promise<Task[]>;
322
+ /**
323
+ * Auto-start vibing on a task when Claude marks it as in_progress
324
+ */
325
+ startVibing(taskId: string): Promise<Task>;
326
+ /**
327
+ * Complete a task when Claude marks it as completed
328
+ */
329
+ completeTask(taskId: string): Promise<Task>;
330
+ }
331
+ /**
332
+ * MCP Tool: sync_claude_todos
333
+ *
334
+ * This function is designed to be called as an MCP tool.
335
+ * It accepts Claude's current todo list and syncs it with VibeTasks.
336
+ */
337
+ declare function syncClaudeTodos(taskOps: TaskOperations, todos: ClaudeTodoItem[], options?: SyncOptions): Promise<SyncAllResult>;
338
+
339
+ export { AuthManager, ClaudeSync, type ClaudeTodoItem, ConfigManager, type SupabaseConfig, type SyncAllResult, type SyncOptions, type SyncResult, type Tag, type Task, type TaskFilter, type TaskFlowConfig, TaskOperations, type TaskPriority, type TaskStatus, createSupabaseClient, createSupabaseClientFromEnv, syncClaudeTodos };
package/dist/index.js CHANGED
@@ -287,11 +287,14 @@ var TaskOperations = class _TaskOperations {
287
287
  // ============================================
288
288
  // TASK CRUD OPERATIONS
289
289
  // ============================================
290
- async getTasks(filter = "all") {
290
+ async getTasks(filter = "all", includeArchived = false) {
291
291
  let query = this.supabase.from("tasks").select(`
292
292
  *,
293
293
  tags:task_tags(tag:tags(*))
294
294
  `).is("parent_task_id", null).order("position", { ascending: true });
295
+ if (!includeArchived) {
296
+ query = query.neq("status", "archived");
297
+ }
295
298
  if (filter === "today") {
296
299
  const today = /* @__PURE__ */ new Date();
297
300
  today.setHours(0, 0, 0, 0);
@@ -318,12 +321,16 @@ var TaskOperations = class _TaskOperations {
318
321
  async getTask(taskId) {
319
322
  const { data, error } = await this.supabase.from("tasks").select(`
320
323
  *,
321
- tags:task_tags(tag:tags(*))
324
+ tags:task_tags(tag:tags(*)),
325
+ attachments:task_attachments(*)
322
326
  `).eq("id", taskId).single();
323
327
  if (error) throw error;
324
328
  return {
325
329
  ...data,
326
- tags: data.tags?.map((t) => t.tag).filter(Boolean) || []
330
+ tags: data.tags?.map((t) => t.tag).filter(Boolean) || [],
331
+ attachments: data.attachments || [],
332
+ subtasks_json: data.subtasks || []
333
+ // DB column is 'subtasks', maps to subtasks_json
327
334
  };
328
335
  }
329
336
  async createTask(task) {
@@ -332,6 +339,7 @@ var TaskOperations = class _TaskOperations {
332
339
  const { data, error } = await this.supabase.from("tasks").insert({
333
340
  user_id: user.id,
334
341
  title: task.title,
342
+ description: task.description,
335
343
  notes: task.notes,
336
344
  notes_format: task.notes_format || "markdown",
337
345
  due_date: task.due_date,
@@ -343,6 +351,8 @@ var TaskOperations = class _TaskOperations {
343
351
  priority: task.priority || "none",
344
352
  parent_task_id: task.parent_task_id,
345
353
  position: task.position || 0,
354
+ subtasks: task.subtasks_json || [],
355
+ // Inline subtasks as JSON
346
356
  // VibeTasks-specific fields
347
357
  status: task.status || "todo",
348
358
  project_tag: task.project_tag,
@@ -359,6 +369,7 @@ var TaskOperations = class _TaskOperations {
359
369
  async updateTask(taskId, updates) {
360
370
  const updateData = {};
361
371
  if (updates.title !== void 0) updateData.title = updates.title;
372
+ if (updates.description !== void 0) updateData.description = updates.description;
362
373
  if (updates.notes !== void 0) updateData.notes = updates.notes;
363
374
  if (updates.notes_format !== void 0) updateData.notes_format = updates.notes_format;
364
375
  if (updates.completed !== void 0) {
@@ -373,6 +384,7 @@ var TaskOperations = class _TaskOperations {
373
384
  if (updates.recurrence_end !== void 0) updateData.recurrence_end = updates.recurrence_end;
374
385
  if (updates.priority !== void 0) updateData.priority = updates.priority;
375
386
  if (updates.position !== void 0) updateData.position = updates.position;
387
+ if (updates.subtasks_json !== void 0) updateData.subtasks = updates.subtasks_json;
376
388
  if (updates.status !== void 0) updateData.status = updates.status;
377
389
  if (updates.project_tag !== void 0) updateData.project_tag = updates.project_tag;
378
390
  if (updates.created_by !== void 0) updateData.created_by = updates.created_by;
@@ -409,6 +421,46 @@ var TaskOperations = class _TaskOperations {
409
421
  return data;
410
422
  }
411
423
  // ============================================
424
+ // ARCHIVE OPERATIONS
425
+ // ============================================
426
+ /**
427
+ * Archive a task - moves it to archived status
428
+ * Typically used for completed tasks you want to hide from views
429
+ */
430
+ async archiveTask(taskId) {
431
+ const { data, error } = await this.supabase.from("tasks").update({
432
+ status: "archived",
433
+ archived_at: (/* @__PURE__ */ new Date()).toISOString()
434
+ }).eq("id", taskId).select().single();
435
+ if (error) throw error;
436
+ return data;
437
+ }
438
+ /**
439
+ * Unarchive a task - restores it to its previous state (done by default)
440
+ */
441
+ async unarchiveTask(taskId, restoreStatus = "done") {
442
+ const { data, error } = await this.supabase.from("tasks").update({
443
+ status: restoreStatus,
444
+ archived_at: null
445
+ }).eq("id", taskId).select().single();
446
+ if (error) throw error;
447
+ return data;
448
+ }
449
+ /**
450
+ * Get all archived tasks
451
+ */
452
+ async getArchivedTasks(limit = 50) {
453
+ const { data, error } = await this.supabase.from("tasks").select(`
454
+ *,
455
+ tags:task_tags(tag:tags(*))
456
+ `).eq("status", "archived").is("parent_task_id", null).order("archived_at", { ascending: false }).limit(limit);
457
+ if (error) throw error;
458
+ return (data || []).map((task) => ({
459
+ ...task,
460
+ tags: task.tags?.map((t) => t.tag).filter(Boolean) || []
461
+ }));
462
+ }
463
+ // ============================================
412
464
  // TAG OPERATIONS
413
465
  // ============================================
414
466
  async getTags() {
@@ -435,11 +487,15 @@ var TaskOperations = class _TaskOperations {
435
487
  // ============================================
436
488
  // SEARCH & UTILITY
437
489
  // ============================================
438
- async searchTasks(query, limit = 20) {
439
- const { data, error } = await this.supabase.from("tasks").select(`
490
+ async searchTasks(query, limit = 20, includeArchived = false) {
491
+ let dbQuery = this.supabase.from("tasks").select(`
440
492
  *,
441
493
  tags:task_tags(tag:tags(*))
442
494
  `).ilike("title", `%${query}%`).is("parent_task_id", null).eq("completed", false).order("position", { ascending: true }).limit(limit);
495
+ if (!includeArchived) {
496
+ dbQuery = dbQuery.neq("status", "archived");
497
+ }
498
+ const { data, error } = await dbQuery;
443
499
  if (error) throw error;
444
500
  return (data || []).map((task) => ({
445
501
  ...task,
@@ -457,11 +513,242 @@ var TaskOperations = class _TaskOperations {
457
513
  }
458
514
  return await this.createTag(name, color);
459
515
  }
516
+ // ============================================
517
+ // ATTACHMENT OPERATIONS
518
+ // ============================================
519
+ /**
520
+ * Get a signed URL for an attachment (valid for 1 hour)
521
+ */
522
+ async getAttachmentUrl(storagePath) {
523
+ const { data, error } = await this.supabase.storage.from("task-attachments").createSignedUrl(storagePath, 3600);
524
+ if (error) throw error;
525
+ return data.signedUrl;
526
+ }
527
+ /**
528
+ * Get all attachments for a task with signed URLs
529
+ */
530
+ async getTaskAttachments(taskId) {
531
+ const { data, error } = await this.supabase.from("task_attachments").select("*").eq("task_id", taskId);
532
+ if (error) throw error;
533
+ const attachmentsWithUrls = await Promise.all(
534
+ (data || []).map(async (attachment) => {
535
+ const url = await this.getAttachmentUrl(attachment.storage_path);
536
+ return { ...attachment, url };
537
+ })
538
+ );
539
+ return attachmentsWithUrls;
540
+ }
460
541
  };
542
+
543
+ // src/claude-sync.ts
544
+ function mapStatus(claudeStatus) {
545
+ switch (claudeStatus) {
546
+ case "pending":
547
+ return "todo";
548
+ case "in_progress":
549
+ return "vibing";
550
+ case "completed":
551
+ return "done";
552
+ default:
553
+ return "todo";
554
+ }
555
+ }
556
+ function calculateSimilarity(str1, str2) {
557
+ const s1 = str1.toLowerCase().trim();
558
+ const s2 = str2.toLowerCase().trim();
559
+ if (s1 === s2) return 1;
560
+ if (s1.length === 0 || s2.length === 0) return 0;
561
+ if (s1.includes(s2) || s2.includes(s1)) {
562
+ return 0.8;
563
+ }
564
+ const words1 = new Set(s1.split(/\s+/));
565
+ const words2 = new Set(s2.split(/\s+/));
566
+ let commonWords = 0;
567
+ for (const word of words1) {
568
+ if (words2.has(word)) {
569
+ commonWords++;
570
+ }
571
+ }
572
+ const totalWords = Math.max(words1.size, words2.size);
573
+ if (totalWords === 0) return 0;
574
+ return commonWords / totalWords;
575
+ }
576
+ function findBestMatch(todoContent, tasks, threshold = 0.6) {
577
+ let bestMatch = null;
578
+ for (const task of tasks) {
579
+ const similarity = calculateSimilarity(todoContent, task.title);
580
+ if (similarity >= threshold) {
581
+ if (!bestMatch || similarity > bestMatch.similarity) {
582
+ bestMatch = { task, similarity };
583
+ }
584
+ }
585
+ if (task.context_notes) {
586
+ const noteSimilarity = calculateSimilarity(todoContent, task.context_notes);
587
+ if (noteSimilarity >= threshold && noteSimilarity > (bestMatch?.similarity || 0)) {
588
+ bestMatch = { task, similarity: noteSimilarity };
589
+ }
590
+ }
591
+ }
592
+ return bestMatch;
593
+ }
594
+ var ClaudeSync = class {
595
+ taskOps;
596
+ constructor(taskOps) {
597
+ this.taskOps = taskOps;
598
+ }
599
+ /**
600
+ * Sync a single Claude todo item with VibeTasks
601
+ */
602
+ async syncTodo(todo, existingTasks, options = {}) {
603
+ const {
604
+ createMissing = false,
605
+ projectTag,
606
+ matchThreshold = 0.6,
607
+ syncPending = true
608
+ } = options;
609
+ try {
610
+ if (todo.status === "pending" && !syncPending) {
611
+ return {
612
+ content: todo.content,
613
+ matched: false,
614
+ action: "skipped"
615
+ };
616
+ }
617
+ const tasks = existingTasks || await this.taskOps.getTasks("all");
618
+ const match = findBestMatch(todo.content, tasks, matchThreshold);
619
+ if (match) {
620
+ const vibeStatus = mapStatus(todo.status);
621
+ const currentStatus = match.task.status || "todo";
622
+ if (currentStatus !== vibeStatus) {
623
+ if (vibeStatus === "done" && !match.task.completed) {
624
+ await this.taskOps.completeTask(match.task.id);
625
+ } else {
626
+ await this.taskOps.updateTask(match.task.id, {
627
+ status: vibeStatus,
628
+ completed: vibeStatus === "done"
629
+ });
630
+ }
631
+ return {
632
+ content: todo.content,
633
+ matched: true,
634
+ taskId: match.task.id,
635
+ action: "updated",
636
+ previousStatus: currentStatus,
637
+ newStatus: vibeStatus
638
+ };
639
+ }
640
+ return {
641
+ content: todo.content,
642
+ matched: true,
643
+ taskId: match.task.id,
644
+ action: "skipped"
645
+ };
646
+ }
647
+ if (createMissing) {
648
+ const vibeStatus = mapStatus(todo.status);
649
+ const newTask = await this.taskOps.createTask({
650
+ title: todo.content,
651
+ status: vibeStatus,
652
+ project_tag: projectTag,
653
+ created_by: "ai",
654
+ context_notes: todo.activeForm ? `Started: ${todo.activeForm}` : void 0,
655
+ completed: vibeStatus === "done"
656
+ });
657
+ return {
658
+ content: todo.content,
659
+ matched: false,
660
+ taskId: newTask.id,
661
+ action: "created",
662
+ newStatus: vibeStatus
663
+ };
664
+ }
665
+ return {
666
+ content: todo.content,
667
+ matched: false,
668
+ action: "not_found"
669
+ };
670
+ } catch (error) {
671
+ return {
672
+ content: todo.content,
673
+ matched: false,
674
+ action: "skipped",
675
+ error: error.message
676
+ };
677
+ }
678
+ }
679
+ /**
680
+ * Sync all Claude todos with VibeTasks
681
+ */
682
+ async syncAllTodos(todos, options = {}) {
683
+ const results = [];
684
+ let synced = 0;
685
+ let failed = 0;
686
+ try {
687
+ const tasks = await this.taskOps.getTasks("all");
688
+ for (const todo of todos) {
689
+ const result = await this.syncTodo(todo, tasks, options);
690
+ results.push(result);
691
+ if (result.action === "updated" || result.action === "created") {
692
+ synced++;
693
+ } else if (result.error) {
694
+ failed++;
695
+ }
696
+ }
697
+ return {
698
+ success: failed === 0,
699
+ processed: todos.length,
700
+ synced,
701
+ failed,
702
+ results
703
+ };
704
+ } catch (error) {
705
+ return {
706
+ success: false,
707
+ processed: 0,
708
+ synced: 0,
709
+ failed: todos.length,
710
+ results: [{
711
+ content: "All todos",
712
+ matched: false,
713
+ action: "skipped",
714
+ error: error.message
715
+ }]
716
+ };
717
+ }
718
+ }
719
+ /**
720
+ * Get current vibing tasks (for comparison with Claude's in_progress)
721
+ */
722
+ async getVibingTasks() {
723
+ const allTasks = await this.taskOps.getTasks("all");
724
+ return allTasks.filter((t) => t.status === "vibing" && !t.completed);
725
+ }
726
+ /**
727
+ * Auto-start vibing on a task when Claude marks it as in_progress
728
+ */
729
+ async startVibing(taskId) {
730
+ return await this.taskOps.updateTask(taskId, {
731
+ status: "vibing",
732
+ completed: false
733
+ });
734
+ }
735
+ /**
736
+ * Complete a task when Claude marks it as completed
737
+ */
738
+ async completeTask(taskId) {
739
+ return await this.taskOps.completeTask(taskId);
740
+ }
741
+ };
742
+ async function syncClaudeTodos(taskOps, todos, options = {}) {
743
+ const sync = new ClaudeSync(taskOps);
744
+ return await sync.syncAllTodos(todos, options);
745
+ }
461
746
  export {
462
747
  AuthManager,
748
+ ClaudeSync,
463
749
  ConfigManager,
464
750
  TaskOperations,
465
751
  createSupabaseClient,
466
- createSupabaseClientFromEnv
752
+ createSupabaseClientFromEnv,
753
+ syncClaudeTodos
467
754
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibetasks/core",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
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",