@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 +162 -6
- package/dist/index.js +467 -7
- package/package.json +1 -1
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
|
-
|
|
270
|
-
|
|
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
|
|
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
|
-
|
|
383
|
-
|
|
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
|
|
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
|
-
}
|
|
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