@vibetasks/core 0.5.8 → 0.5.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3 +1,7 @@
1
+ import {
2
+ __require
3
+ } from "./chunk-3RG5ZIWI.js";
4
+
1
5
  // src/config-manager.ts
2
6
  import fs from "fs/promises";
3
7
  import path from "path";
@@ -372,11 +376,283 @@ var AuthManager = class {
372
376
  }
373
377
  };
374
378
 
379
+ // src/integration-sync.ts
380
+ var IntegrationSyncService = class {
381
+ supabase;
382
+ constructor(supabase) {
383
+ this.supabase = supabase;
384
+ }
385
+ /**
386
+ * Sync task completion to external integrations
387
+ * Called when a task is marked as done
388
+ */
389
+ async syncTaskComplete(task) {
390
+ const results = [];
391
+ if (task.sentry_issue_id) {
392
+ const sentryResult = await this.resolveSentryIssue(task);
393
+ results.push(sentryResult);
394
+ }
395
+ if (task.github_issue_id || task.github_issue_number) {
396
+ const githubResult = await this.closeGitHubIssue(task);
397
+ results.push(githubResult);
398
+ }
399
+ return results;
400
+ }
401
+ /**
402
+ * Resolve a Sentry issue when the task is completed
403
+ */
404
+ async resolveSentryIssue(task) {
405
+ const result = {
406
+ success: false,
407
+ integration: "sentry",
408
+ action: "resolve",
409
+ externalId: task.sentry_issue_id,
410
+ externalUrl: task.sentry_url
411
+ };
412
+ try {
413
+ const { data: integration, error: integrationError } = await this.supabase.from("sentry_integrations").select("sentry_auth_token, sentry_org_slug, sync_to_sentry, is_enabled").eq("user_id", task.user_id).single();
414
+ if (integrationError || !integration) {
415
+ result.error = "No Sentry integration configured";
416
+ return result;
417
+ }
418
+ if (!integration.is_enabled || !integration.sync_to_sentry) {
419
+ result.error = "Sentry sync is disabled";
420
+ return result;
421
+ }
422
+ if (!integration.sentry_auth_token) {
423
+ result.error = "No Sentry auth token configured";
424
+ return result;
425
+ }
426
+ const projectSlug = task.sentry_project?.split("/").pop() || "";
427
+ const orgSlug = integration.sentry_org_slug || task.sentry_project?.split("/")[0] || "";
428
+ if (!orgSlug || !projectSlug) {
429
+ result.error = "Cannot determine Sentry org/project from task data";
430
+ return result;
431
+ }
432
+ const sentryApiUrl = `https://sentry.io/api/0/issues/${task.sentry_issue_id}/`;
433
+ const response = await fetch(sentryApiUrl, {
434
+ method: "PUT",
435
+ headers: {
436
+ "Authorization": `Bearer ${integration.sentry_auth_token}`,
437
+ "Content-Type": "application/json"
438
+ },
439
+ body: JSON.stringify({
440
+ status: "resolved"
441
+ })
442
+ });
443
+ if (!response.ok) {
444
+ const errorText = await response.text();
445
+ result.error = `Sentry API error: ${response.status} - ${errorText}`;
446
+ await this.logSyncAttempt(task, result, response.status);
447
+ return result;
448
+ }
449
+ result.success = true;
450
+ await this.logSyncAttempt(task, result, response.status);
451
+ return result;
452
+ } catch (error) {
453
+ result.error = error instanceof Error ? error.message : "Unknown error";
454
+ await this.logSyncAttempt(task, result);
455
+ return result;
456
+ }
457
+ }
458
+ /**
459
+ * Close a GitHub issue when the task is completed
460
+ */
461
+ async closeGitHubIssue(task) {
462
+ const result = {
463
+ success: false,
464
+ integration: "github",
465
+ action: "close",
466
+ externalId: task.github_issue_number?.toString() || task.github_issue_id?.toString(),
467
+ externalUrl: task.github_url
468
+ };
469
+ try {
470
+ const { data: integration, error: integrationError } = await this.supabase.from("github_integrations").select("github_token, sync_to_github, is_enabled").eq("user_id", task.user_id).single();
471
+ if (integrationError || !integration) {
472
+ result.error = "No GitHub integration configured";
473
+ return result;
474
+ }
475
+ if (!integration.is_enabled || !integration.sync_to_github) {
476
+ result.error = "GitHub sync is disabled";
477
+ return result;
478
+ }
479
+ if (!integration.github_token) {
480
+ result.error = "No GitHub token configured";
481
+ return result;
482
+ }
483
+ const [owner, repo] = (task.github_repo || "").split("/");
484
+ const issueNumber = task.github_issue_number || task.github_issue_id;
485
+ if (!owner || !repo || !issueNumber) {
486
+ result.error = "Cannot determine GitHub repo/issue from task data";
487
+ return result;
488
+ }
489
+ const githubApiUrl = `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`;
490
+ const response = await fetch(githubApiUrl, {
491
+ method: "PATCH",
492
+ headers: {
493
+ "Authorization": `Bearer ${integration.github_token}`,
494
+ "Accept": "application/vnd.github+json",
495
+ "X-GitHub-Api-Version": "2022-11-28",
496
+ "Content-Type": "application/json"
497
+ },
498
+ body: JSON.stringify({
499
+ state: "closed",
500
+ state_reason: "completed"
501
+ })
502
+ });
503
+ if (!response.ok) {
504
+ const errorText = await response.text();
505
+ result.error = `GitHub API error: ${response.status} - ${errorText}`;
506
+ await this.logSyncAttempt(task, result, response.status);
507
+ return result;
508
+ }
509
+ result.success = true;
510
+ await this.logSyncAttempt(task, result, response.status);
511
+ return result;
512
+ } catch (error) {
513
+ result.error = error instanceof Error ? error.message : "Unknown error";
514
+ await this.logSyncAttempt(task, result);
515
+ return result;
516
+ }
517
+ }
518
+ /**
519
+ * Log sync attempt for debugging and auditing
520
+ */
521
+ async logSyncAttempt(task, result, responseStatus) {
522
+ try {
523
+ await this.supabase.from("integration_sync_logs").insert({
524
+ user_id: task.user_id,
525
+ task_id: task.id,
526
+ integration: result.integration,
527
+ action: result.action,
528
+ external_id: result.externalId,
529
+ external_url: result.externalUrl,
530
+ success: result.success,
531
+ error_message: result.error,
532
+ response_status: responseStatus
533
+ });
534
+ } catch (error) {
535
+ console.error("Failed to log sync attempt:", error);
536
+ }
537
+ }
538
+ /**
539
+ * Sync task reopen to external integrations
540
+ * Called when a task is moved back to todo/vibing from done
541
+ */
542
+ async syncTaskReopen(task) {
543
+ const results = [];
544
+ if (task.sentry_issue_id) {
545
+ const sentryResult = await this.unresolveSentryIssue(task);
546
+ results.push(sentryResult);
547
+ }
548
+ if (task.github_issue_id || task.github_issue_number) {
549
+ const githubResult = await this.reopenGitHubIssue(task);
550
+ results.push(githubResult);
551
+ }
552
+ return results;
553
+ }
554
+ /**
555
+ * Unresolve a Sentry issue when the task is reopened
556
+ */
557
+ async unresolveSentryIssue(task) {
558
+ const result = {
559
+ success: false,
560
+ integration: "sentry",
561
+ action: "unresolve",
562
+ externalId: task.sentry_issue_id,
563
+ externalUrl: task.sentry_url
564
+ };
565
+ try {
566
+ const { data: integration } = await this.supabase.from("sentry_integrations").select("sentry_auth_token, sync_to_sentry, is_enabled").eq("user_id", task.user_id).single();
567
+ if (!integration?.is_enabled || !integration?.sync_to_sentry || !integration?.sentry_auth_token) {
568
+ result.error = "Sentry sync not configured or disabled";
569
+ return result;
570
+ }
571
+ const sentryApiUrl = `https://sentry.io/api/0/issues/${task.sentry_issue_id}/`;
572
+ const response = await fetch(sentryApiUrl, {
573
+ method: "PUT",
574
+ headers: {
575
+ "Authorization": `Bearer ${integration.sentry_auth_token}`,
576
+ "Content-Type": "application/json"
577
+ },
578
+ body: JSON.stringify({
579
+ status: "unresolved"
580
+ })
581
+ });
582
+ if (!response.ok) {
583
+ result.error = `Sentry API error: ${response.status}`;
584
+ await this.logSyncAttempt(task, result, response.status);
585
+ return result;
586
+ }
587
+ result.success = true;
588
+ await this.logSyncAttempt(task, result, response.status);
589
+ return result;
590
+ } catch (error) {
591
+ result.error = error instanceof Error ? error.message : "Unknown error";
592
+ await this.logSyncAttempt(task, result);
593
+ return result;
594
+ }
595
+ }
596
+ /**
597
+ * Reopen a GitHub issue when the task is reopened
598
+ */
599
+ async reopenGitHubIssue(task) {
600
+ const result = {
601
+ success: false,
602
+ integration: "github",
603
+ action: "reopen",
604
+ externalId: task.github_issue_number?.toString(),
605
+ externalUrl: task.github_url
606
+ };
607
+ try {
608
+ const { data: integration } = await this.supabase.from("github_integrations").select("github_token, sync_to_github, is_enabled").eq("user_id", task.user_id).single();
609
+ if (!integration?.is_enabled || !integration?.sync_to_github || !integration?.github_token) {
610
+ result.error = "GitHub sync not configured or disabled";
611
+ return result;
612
+ }
613
+ const [owner, repo] = (task.github_repo || "").split("/");
614
+ const issueNumber = task.github_issue_number || task.github_issue_id;
615
+ if (!owner || !repo || !issueNumber) {
616
+ result.error = "Cannot determine GitHub repo/issue from task data";
617
+ return result;
618
+ }
619
+ const githubApiUrl = `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`;
620
+ const response = await fetch(githubApiUrl, {
621
+ method: "PATCH",
622
+ headers: {
623
+ "Authorization": `Bearer ${integration.github_token}`,
624
+ "Accept": "application/vnd.github+json",
625
+ "X-GitHub-Api-Version": "2022-11-28",
626
+ "Content-Type": "application/json"
627
+ },
628
+ body: JSON.stringify({
629
+ state: "open"
630
+ })
631
+ });
632
+ if (!response.ok) {
633
+ result.error = `GitHub API error: ${response.status}`;
634
+ await this.logSyncAttempt(task, result, response.status);
635
+ return result;
636
+ }
637
+ result.success = true;
638
+ await this.logSyncAttempt(task, result, response.status);
639
+ return result;
640
+ } catch (error) {
641
+ result.error = error instanceof Error ? error.message : "Unknown error";
642
+ await this.logSyncAttempt(task, result);
643
+ return result;
644
+ }
645
+ }
646
+ };
647
+
375
648
  // src/task-operations.ts
376
649
  var TaskOperations = class _TaskOperations {
377
650
  supabase;
378
- constructor(supabase) {
651
+ // 🔒 WORKSPACE ISOLATION: Track workspace context
652
+ workspaceId;
653
+ constructor(supabase, workspaceId) {
379
654
  this.supabase = supabase;
655
+ this.workspaceId = workspaceId;
380
656
  }
381
657
  // Current session ID for change tracking
382
658
  currentSessionId;
@@ -386,13 +662,27 @@ var TaskOperations = class _TaskOperations {
386
662
  setSessionId(sessionId) {
387
663
  this.currentSessionId = sessionId;
388
664
  }
665
+ /**
666
+ * Set the workspace ID for workspace isolation
667
+ * 🔒 WORKSPACE ISOLATION: Filter all queries by workspace
668
+ */
669
+ setWorkspaceId(workspaceId) {
670
+ this.workspaceId = workspaceId;
671
+ }
672
+ /**
673
+ * Get current workspace ID
674
+ */
675
+ getWorkspaceId() {
676
+ return this.workspaceId;
677
+ }
389
678
  /**
390
679
  * Create TaskOperations from AuthManager
391
680
  * Automatically refreshes expired tokens
681
+ * 🔒 WORKSPACE ISOLATION: Accepts optional workspaceId parameter
392
682
  */
393
- static async fromAuthManager(authManager) {
683
+ static async fromAuthManager(authManager, workspaceId) {
394
684
  const supabaseUrl = await authManager.getConfig("supabase_url") || "https://ihmayqzxqyednchbezya.supabase.co";
395
- const supabaseKey = await authManager.getConfig("supabase_key") || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNia2t6dGJjb2l0cmZjbGVnaGZkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Njc3NTc0MjgsImV4cCI6MjA4MzMzMzQyOH0.G7ILx-nntP0NbxO1gKt5yASb7nt7OmpJ8qtykeGYbQA";
685
+ const supabaseKey = await authManager.getConfig("supabase_key") || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImlobWF5cXp4cXllZG5jaGJlenlhIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjgxNzcyOTgsImV4cCI6MjA4Mzc1MzI5OH0._xhGhkqoy7AMm7M5dxSHXM_yeP8zVAJG-BaGMICtHFo";
396
686
  const accessToken = await authManager.getValidAccessToken();
397
687
  if (!accessToken) {
398
688
  throw new Error("Not authenticated or session expired. Run: vibetasks login --browser");
@@ -402,16 +692,21 @@ var TaskOperations = class _TaskOperations {
402
692
  supabaseKey,
403
693
  accessToken
404
694
  });
405
- return new _TaskOperations(supabase);
695
+ return new _TaskOperations(supabase, workspaceId);
406
696
  }
407
697
  // ============================================
408
698
  // TASK CRUD OPERATIONS
409
699
  // ============================================
410
700
  async getTasks(filter = "all", includeArchived = false) {
701
+ const { data: { user } } = await this.supabase.auth.getUser();
702
+ if (!user) throw new Error("Not authenticated");
411
703
  let query = this.supabase.from("tasks").select(`
412
704
  *,
413
705
  tags:task_tags(tag:tags(*))
414
- `).is("parent_task_id", null).order("position", { ascending: true });
706
+ `).eq("user_id", user.id).is("parent_task_id", null).order("position", { ascending: true });
707
+ if (this.workspaceId) {
708
+ query = query.eq("workspace_id", this.workspaceId);
709
+ }
415
710
  if (!includeArchived) {
416
711
  query = query.neq("status", "archived");
417
712
  }
@@ -439,11 +734,13 @@ var TaskOperations = class _TaskOperations {
439
734
  }));
440
735
  }
441
736
  async getTask(taskId) {
737
+ const { data: { user } } = await this.supabase.auth.getUser();
738
+ if (!user) throw new Error("Not authenticated");
442
739
  const { data, error } = await this.supabase.from("tasks").select(`
443
740
  *,
444
741
  tags:task_tags(tag:tags(*)),
445
742
  attachments:task_attachments(*)
446
- `).eq("id", taskId).single();
743
+ `).eq("id", taskId).eq("user_id", user.id).single();
447
744
  if (error) throw error;
448
745
  return {
449
746
  ...data,
@@ -460,6 +757,8 @@ var TaskOperations = class _TaskOperations {
460
757
  const nextShortId = (maxIdResult?.short_id || 0) + 1;
461
758
  const { data, error } = await this.supabase.from("tasks").insert({
462
759
  user_id: user.id,
760
+ workspace_id: task.workspace_id || this.workspaceId || null,
761
+ // 🔒 WORKSPACE ISOLATION
463
762
  title: task.title,
464
763
  description: task.description,
465
764
  notes: task.notes,
@@ -523,6 +822,10 @@ var TaskOperations = class _TaskOperations {
523
822
  if (updates.created_by !== void 0) updateData.created_by = updates.created_by;
524
823
  if (updates.context_notes !== void 0) updateData.context_notes = updates.context_notes;
525
824
  if (updates.energy_required !== void 0) updateData.energy_required = updates.energy_required;
825
+ if (updates.semantic_vector !== void 0) updateData.semantic_vector = updates.semantic_vector;
826
+ if (updates.intent !== void 0) updateData.intent = updates.intent;
827
+ if (updates.context_tags !== void 0) updateData.context_tags = updates.context_tags;
828
+ if (updates.needs_embedding_update !== void 0) updateData.needs_embedding_update = updates.needs_embedding_update;
526
829
  const { data, error } = await this.supabase.from("tasks").update(updateData).eq("id", taskId).select().single();
527
830
  if (error) throw error;
528
831
  if (updates.tags !== void 0) {
@@ -617,7 +920,7 @@ var TaskOperations = class _TaskOperations {
617
920
  if (error) throw error;
618
921
  }
619
922
  async completeTask(taskId) {
620
- const { data: existingTask } = await this.supabase.from("tasks").select("subtasks, acceptance_criteria, status, title").eq("id", taskId).single();
923
+ const { data: existingTask } = await this.supabase.from("tasks").select("subtasks, acceptance_criteria, status, title, user_id, sentry_issue_id, sentry_project, sentry_url, github_issue_id, github_issue_number, github_repo, github_url").eq("id", taskId).single();
621
924
  const oldStatus = existingTask?.status || "todo";
622
925
  const updatePayload = {
623
926
  completed: true,
@@ -630,8 +933,18 @@ var TaskOperations = class _TaskOperations {
630
933
  if (existingTask?.acceptance_criteria && Array.isArray(existingTask.acceptance_criteria)) {
631
934
  updatePayload.acceptance_criteria = existingTask.acceptance_criteria.map((c) => ({ ...c, done: true }));
632
935
  }
633
- const { data, error } = await this.supabase.from("tasks").update(updatePayload).eq("id", taskId).select().single();
634
- if (error) throw error;
936
+ const { data, error } = await this.supabase.from("tasks").update(updatePayload).eq("id", taskId).select();
937
+ if (error) {
938
+ console.error("Error updating task:", error);
939
+ throw new Error(`Failed to complete task: ${error.message}`);
940
+ }
941
+ if (!data || data.length === 0) {
942
+ throw new Error(`Task ${taskId} not found or update failed`);
943
+ }
944
+ if (data.length > 1) {
945
+ console.warn(`Warning: Multiple rows updated for task ${taskId}`);
946
+ }
947
+ const updatedTask = data[0];
635
948
  await this.logChange({
636
949
  taskId,
637
950
  changeType: "status_changed",
@@ -640,7 +953,33 @@ var TaskOperations = class _TaskOperations {
640
953
  newValue: "done",
641
954
  message: `Completed: ${existingTask?.title || "task"}`
642
955
  });
643
- return data;
956
+ if (existingTask?.sentry_issue_id || existingTask?.github_issue_id || existingTask?.github_issue_number) {
957
+ try {
958
+ const syncService = new IntegrationSyncService(this.supabase);
959
+ const syncResults = await syncService.syncTaskComplete({
960
+ id: taskId,
961
+ user_id: existingTask.user_id,
962
+ title: existingTask.title,
963
+ sentry_issue_id: existingTask.sentry_issue_id,
964
+ sentry_project: existingTask.sentry_project,
965
+ sentry_url: existingTask.sentry_url,
966
+ github_issue_id: existingTask.github_issue_id,
967
+ github_issue_number: existingTask.github_issue_number,
968
+ github_repo: existingTask.github_repo,
969
+ github_url: existingTask.github_url
970
+ });
971
+ for (const result of syncResults) {
972
+ if (result.success) {
973
+ console.log(`[IntegrationSync] ${result.integration}: ${result.action} successful`);
974
+ } else if (result.error && !result.error.includes("disabled") && !result.error.includes("not configured")) {
975
+ console.warn(`[IntegrationSync] ${result.integration}: ${result.error}`);
976
+ }
977
+ }
978
+ } catch (syncError) {
979
+ console.error("[IntegrationSync] Error syncing task completion:", syncError);
980
+ }
981
+ }
982
+ return updatedTask;
644
983
  }
645
984
  async uncompleteTask(taskId) {
646
985
  const { data, error } = await this.supabase.from("tasks").update({
@@ -700,6 +1039,260 @@ var TaskOperations = class _TaskOperations {
700
1039
  }));
701
1040
  }
702
1041
  // ============================================
1042
+ // TASK HEALTH & DEPENDENCY OPERATIONS
1043
+ // (Inspired by beads: ready, stale, orphans, blocked)
1044
+ // ============================================
1045
+ /**
1046
+ * Get tasks ready to work on (no blockers, not deferred)
1047
+ */
1048
+ async getReadyTasks(options = {}) {
1049
+ const { data: { user } } = await this.supabase.auth.getUser();
1050
+ if (!user) throw new Error("Not authenticated");
1051
+ const status = options.status || "vibing";
1052
+ const limit = options.limit || 50;
1053
+ const { data, error } = await this.supabase.rpc("get_ready_tasks", {
1054
+ p_user_id: user.id,
1055
+ p_workspace_id: this.workspaceId || null,
1056
+ p_status: status,
1057
+ p_limit: limit
1058
+ });
1059
+ if (error) {
1060
+ if (error.message.includes("function") || error.code === "42883") {
1061
+ return this.getReadyTasksFallback(status, limit);
1062
+ }
1063
+ throw error;
1064
+ }
1065
+ return data || [];
1066
+ }
1067
+ /**
1068
+ * Fallback for ready tasks when RPC function doesn't exist
1069
+ */
1070
+ async getReadyTasksFallback(status, limit) {
1071
+ const { data: { user } } = await this.supabase.auth.getUser();
1072
+ if (!user) throw new Error("Not authenticated");
1073
+ let query = this.supabase.from("tasks").select(`
1074
+ *,
1075
+ tags:task_tags(tag:tags(*))
1076
+ `).eq("user_id", user.id).is("parent_task_id", null).order("priority", { ascending: false }).order("due_date", { ascending: true, nullsFirst: false }).limit(limit);
1077
+ if (this.workspaceId) {
1078
+ query = query.eq("workspace_id", this.workspaceId);
1079
+ }
1080
+ if (status) {
1081
+ query = query.eq("status", status);
1082
+ } else {
1083
+ query = query.in("status", ["todo", "vibing"]);
1084
+ }
1085
+ const { data, error } = await query;
1086
+ if (error) throw error;
1087
+ return (data || []).map((task) => ({
1088
+ ...task,
1089
+ tags: task.tags?.map((t) => t.tag).filter(Boolean) || []
1090
+ }));
1091
+ }
1092
+ /**
1093
+ * Get stale tasks (not updated in X days)
1094
+ */
1095
+ async getStaleTasks(options = {}) {
1096
+ const { data: { user } } = await this.supabase.auth.getUser();
1097
+ if (!user) throw new Error("Not authenticated");
1098
+ const days = options.days || 7;
1099
+ const limit = options.limit || 50;
1100
+ const { data, error } = await this.supabase.rpc("get_stale_tasks", {
1101
+ p_user_id: user.id,
1102
+ p_days: days,
1103
+ p_status: options.status || null,
1104
+ p_workspace_id: this.workspaceId || null,
1105
+ p_limit: limit
1106
+ });
1107
+ if (error) {
1108
+ if (error.message.includes("function") || error.code === "42883") {
1109
+ return this.getStaleTasksFallback(days, options.status, limit);
1110
+ }
1111
+ throw error;
1112
+ }
1113
+ return data || [];
1114
+ }
1115
+ /**
1116
+ * Fallback for stale tasks when RPC function doesn't exist
1117
+ */
1118
+ async getStaleTasksFallback(days, status, limit = 50) {
1119
+ const { data: { user } } = await this.supabase.auth.getUser();
1120
+ if (!user) throw new Error("Not authenticated");
1121
+ const staleDate = /* @__PURE__ */ new Date();
1122
+ staleDate.setDate(staleDate.getDate() - days);
1123
+ let query = this.supabase.from("tasks").select("*").eq("user_id", user.id).not("status", "in", '("done","archived")').lt("updated_at", staleDate.toISOString()).order("updated_at", { ascending: true }).limit(limit);
1124
+ if (this.workspaceId) {
1125
+ query = query.eq("workspace_id", this.workspaceId);
1126
+ }
1127
+ if (status) {
1128
+ query = query.eq("status", status);
1129
+ }
1130
+ const { data, error } = await query;
1131
+ if (error) throw error;
1132
+ return (data || []).map((task) => ({
1133
+ ...task,
1134
+ days_stale: Math.floor((Date.now() - new Date(task.updated_at).getTime()) / (1e3 * 60 * 60 * 24))
1135
+ }));
1136
+ }
1137
+ /**
1138
+ * Get orphaned tasks (incomplete tasks from old AI sessions)
1139
+ */
1140
+ async getOrphanedTasks(options = {}) {
1141
+ const { data: { user } } = await this.supabase.auth.getUser();
1142
+ if (!user) throw new Error("Not authenticated");
1143
+ const limit = options.limit || 50;
1144
+ const { data, error } = await this.supabase.rpc("get_orphaned_tasks", {
1145
+ p_user_id: user.id,
1146
+ p_current_session_id: options.currentSessionId || null,
1147
+ p_workspace_id: this.workspaceId || null,
1148
+ p_limit: limit
1149
+ });
1150
+ if (error) {
1151
+ if (error.message.includes("function") || error.code === "42883") {
1152
+ return this.getOrphanedTasksFallback(options.currentSessionId, limit);
1153
+ }
1154
+ throw error;
1155
+ }
1156
+ return data || [];
1157
+ }
1158
+ /**
1159
+ * Fallback for orphaned tasks when RPC function doesn't exist
1160
+ */
1161
+ async getOrphanedTasksFallback(currentSessionId, limit = 50) {
1162
+ const { data: { user } } = await this.supabase.auth.getUser();
1163
+ if (!user) throw new Error("Not authenticated");
1164
+ let query = this.supabase.from("tasks").select("*").eq("user_id", user.id).in("status", ["vibing", "todo"]).order("updated_at", { ascending: false }).limit(limit);
1165
+ if (this.workspaceId) {
1166
+ query = query.eq("workspace_id", this.workspaceId);
1167
+ }
1168
+ if (currentSessionId) {
1169
+ query = query.or(`session_id.is.null,session_id.neq.${currentSessionId}`);
1170
+ }
1171
+ const { data, error } = await query;
1172
+ if (error) throw error;
1173
+ return (data || []).map((task) => {
1174
+ const subtasks = task.subtasks || [];
1175
+ return {
1176
+ ...task,
1177
+ subtasks_total: subtasks.length,
1178
+ subtasks_done: subtasks.filter((s) => s.done).length
1179
+ };
1180
+ });
1181
+ }
1182
+ /**
1183
+ * Get blocked tasks (tasks with open dependencies)
1184
+ */
1185
+ async getBlockedTasks(options = {}) {
1186
+ const { data: { user } } = await this.supabase.auth.getUser();
1187
+ if (!user) throw new Error("Not authenticated");
1188
+ const limit = options.limit || 50;
1189
+ const { data, error } = await this.supabase.rpc("get_blocked_tasks", {
1190
+ p_user_id: user.id,
1191
+ p_workspace_id: this.workspaceId || null,
1192
+ p_limit: limit
1193
+ });
1194
+ if (error) {
1195
+ if (error.message.includes("function") || error.code === "42883") {
1196
+ return this.getBlockedTasksFallback(limit);
1197
+ }
1198
+ throw error;
1199
+ }
1200
+ return data || [];
1201
+ }
1202
+ /**
1203
+ * Fallback for blocked tasks when RPC function doesn't exist
1204
+ */
1205
+ async getBlockedTasksFallback(limit = 50) {
1206
+ const { data: { user } } = await this.supabase.auth.getUser();
1207
+ if (!user) throw new Error("Not authenticated");
1208
+ const { data, error } = await this.supabase.from("task_dependencies").select(`
1209
+ task_id,
1210
+ depends_on_task_id,
1211
+ task:tasks!task_dependencies_task_id_fkey(
1212
+ id, title, status, priority, short_id
1213
+ ),
1214
+ blocker:tasks!task_dependencies_depends_on_task_id_fkey(
1215
+ id, title, status
1216
+ )
1217
+ `).eq("dependency_type", "blocks").limit(limit * 2);
1218
+ if (error) throw error;
1219
+ const blockedMap = /* @__PURE__ */ new Map();
1220
+ for (const dep of data || []) {
1221
+ const blocker = Array.isArray(dep.blocker) ? dep.blocker[0] : dep.blocker;
1222
+ const task = Array.isArray(dep.task) ? dep.task[0] : dep.task;
1223
+ if (blocker?.status !== "done" && blocker?.status !== "archived") {
1224
+ const taskId = dep.task_id;
1225
+ if (!blockedMap.has(taskId)) {
1226
+ blockedMap.set(taskId, {
1227
+ ...task,
1228
+ blocked_by_count: 0,
1229
+ blocker_ids: [],
1230
+ blocker_titles: []
1231
+ });
1232
+ }
1233
+ const mappedTask = blockedMap.get(taskId);
1234
+ mappedTask.blocked_by_count++;
1235
+ mappedTask.blocker_ids.push(blocker.id);
1236
+ mappedTask.blocker_titles.push(blocker.title);
1237
+ }
1238
+ }
1239
+ return Array.from(blockedMap.values()).slice(0, limit);
1240
+ }
1241
+ // ============================================
1242
+ // TASK COMPACTION OPERATIONS
1243
+ // ============================================
1244
+ /**
1245
+ * Get tasks eligible for compaction (completed, old, not yet compacted)
1246
+ */
1247
+ async getCompactableTasks(options = {}) {
1248
+ const { data: { user } } = await this.supabase.auth.getUser();
1249
+ if (!user) throw new Error("Not authenticated");
1250
+ const minDays = options.minDaysOld || 7;
1251
+ const maxLevel = options.maxCompactionLevel || 0;
1252
+ const limit = options.limit || 10;
1253
+ const cutoffDate = /* @__PURE__ */ new Date();
1254
+ cutoffDate.setDate(cutoffDate.getDate() - minDays);
1255
+ let query = this.supabase.from("tasks").select("*").eq("user_id", user.id).eq("status", "done").lte("completed_at", cutoffDate.toISOString()).or(`compaction_level.is.null,compaction_level.lte.${maxLevel}`).order("completed_at", { ascending: true }).limit(limit);
1256
+ if (this.workspaceId) {
1257
+ query = query.eq("workspace_id", this.workspaceId);
1258
+ }
1259
+ const { data, error } = await query;
1260
+ if (error) throw error;
1261
+ return data || [];
1262
+ }
1263
+ /**
1264
+ * Update a task with compaction results
1265
+ */
1266
+ async applyCompaction(taskId, summary) {
1267
+ const { data, error } = await this.supabase.from("tasks").update({
1268
+ context_summary: summary,
1269
+ compaction_level: 1,
1270
+ compacted_at: (/* @__PURE__ */ new Date()).toISOString(),
1271
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1272
+ }).eq("id", taskId).select().single();
1273
+ if (error) throw error;
1274
+ return data;
1275
+ }
1276
+ /**
1277
+ * Get compaction statistics for the user
1278
+ */
1279
+ async getCompactionStats() {
1280
+ const { data: { user } } = await this.supabase.auth.getUser();
1281
+ if (!user) throw new Error("Not authenticated");
1282
+ const { count: totalCompleted } = await this.supabase.from("tasks").select("*", { count: "exact", head: true }).eq("user_id", user.id).eq("status", "done");
1283
+ const { count: compacted } = await this.supabase.from("tasks").select("*", { count: "exact", head: true }).eq("user_id", user.id).eq("status", "done").gt("compaction_level", 0);
1284
+ const cutoffDate = /* @__PURE__ */ new Date();
1285
+ cutoffDate.setDate(cutoffDate.getDate() - 7);
1286
+ const { count: eligible } = await this.supabase.from("tasks").select("*", { count: "exact", head: true }).eq("user_id", user.id).eq("status", "done").lte("completed_at", cutoffDate.toISOString()).or("compaction_level.is.null,compaction_level.eq.0");
1287
+ return {
1288
+ totalCompleted: totalCompleted || 0,
1289
+ compacted: compacted || 0,
1290
+ eligible: eligible || 0,
1291
+ savedBytes: 0
1292
+ // Would need to track original vs compacted sizes
1293
+ };
1294
+ }
1295
+ // ============================================
703
1296
  // TAG OPERATIONS
704
1297
  // ============================================
705
1298
  async getTags() {
@@ -767,6 +1360,10 @@ var TaskOperations = class _TaskOperations {
767
1360
  * Get all attachments for a task with signed URLs
768
1361
  */
769
1362
  async getTaskAttachments(taskId) {
1363
+ const { data: { user } } = await this.supabase.auth.getUser();
1364
+ if (!user) throw new Error("Not authenticated");
1365
+ const task = await this.getTask(taskId);
1366
+ if (!task) throw new Error("Task not found or access denied");
770
1367
  const { data, error } = await this.supabase.from("task_attachments").select("*").eq("task_id", taskId);
771
1368
  if (error) throw error;
772
1369
  const attachmentsWithUrls = await Promise.all(
@@ -930,6 +1527,43 @@ var TaskOperations = class _TaskOperations {
930
1527
  return data || [];
931
1528
  }
932
1529
  // ============================================
1530
+ // SESSION ACTIONS
1531
+ // ============================================
1532
+ /**
1533
+ * Log an action to the current AI session
1534
+ */
1535
+ async logSessionAction(sessionId, action) {
1536
+ const { data: { user } } = await this.supabase.auth.getUser();
1537
+ if (!user) return;
1538
+ const { data: session } = await this.supabase.from("ai_sessions").select("id").eq("user_id", user.id).eq("session_id", sessionId).single();
1539
+ if (!session) {
1540
+ console.warn(`Session ${sessionId} not found, skipping action log`);
1541
+ return;
1542
+ }
1543
+ const { error } = await this.supabase.from("session_actions").insert({
1544
+ session_id: session.id,
1545
+ user_id: user.id,
1546
+ action_type: action.action_type,
1547
+ description: action.description,
1548
+ task_id: action.task_id,
1549
+ doc_id: action.doc_id,
1550
+ metadata: action.metadata || {},
1551
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1552
+ });
1553
+ if (error) {
1554
+ console.error("Failed to log session action:", error);
1555
+ return;
1556
+ }
1557
+ const { data: stats } = await this.supabase.rpc("get_session_stats", { p_session_id: session.id });
1558
+ if (stats && stats[0]) {
1559
+ await this.supabase.from("ai_sessions").update({
1560
+ tasks_created: stats[0].tasks_created,
1561
+ tasks_completed: stats[0].tasks_completed,
1562
+ action_count: stats[0].action_count
1563
+ }).eq("id", session.id);
1564
+ }
1565
+ }
1566
+ // ============================================
933
1567
  // CHANGE TRACKING (Git-for-Tasks)
934
1568
  // ============================================
935
1569
  /**
@@ -978,9 +1612,18 @@ var TaskOperations = class _TaskOperations {
978
1612
  * Get a specific change (like git show)
979
1613
  */
980
1614
  async getChange(changeId) {
981
- const { data, error } = await this.supabase.from("task_changes").select("*").eq("id", changeId).single();
1615
+ const { data, error } = await this.supabase.from("task_changes").select(`
1616
+ *,
1617
+ task:tasks!inner(user_id)
1618
+ `).eq("id", changeId).single();
982
1619
  if (error) throw error;
983
- return data;
1620
+ const { data: { user } } = await this.supabase.auth.getUser();
1621
+ if (!user) throw new Error("Not authenticated");
1622
+ const change = data;
1623
+ if (change.task?.user_id !== user.id) {
1624
+ throw new Error("Access denied: Change belongs to another user");
1625
+ }
1626
+ return change;
984
1627
  }
985
1628
  /**
986
1629
  * Revert a change (non-destructive - creates a new "revert" change)
@@ -1030,26 +1673,25 @@ var TaskOperations = class _TaskOperations {
1030
1673
  // ============================================
1031
1674
  /**
1032
1675
  * Create a workspace
1676
+ *
1677
+ * FLAT WORKSPACE MODEL (2026):
1678
+ * - Workspaces are top-level only (no parent/child hierarchy)
1679
+ * - full_path = display_name (no hierarchy separator needed)
1680
+ * - depth is always 0
1681
+ * - Use tags for sub-organization within a workspace
1033
1682
  */
1034
1683
  async createWorkspace(input) {
1035
1684
  const { data: { user } } = await this.supabase.auth.getUser();
1036
1685
  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
1686
  const { data, error } = await this.supabase.from("workspaces").insert({
1047
1687
  user_id: user.id,
1048
1688
  slug: input.slug,
1049
1689
  display_name: input.displayName,
1050
- full_path: fullPath,
1051
- parent_id: input.parentId,
1052
- depth,
1690
+ full_path: input.displayName,
1691
+ // No hierarchy - just the name
1692
+ // parent_id omitted - workspaces are top-level only
1693
+ depth: 0,
1694
+ // Always 0 in flat model
1053
1695
  color: input.color,
1054
1696
  description: input.description
1055
1697
  }).select().single();
@@ -1077,38 +1719,36 @@ var TaskOperations = class _TaskOperations {
1077
1719
  return data;
1078
1720
  }
1079
1721
  /**
1080
- * Get or create workspace from a project_tag (auto-creates hierarchy)
1722
+ * Get or create workspace from a project_tag
1723
+ *
1724
+ * FLAT WORKSPACE MODEL (2026):
1725
+ * - Only creates top-level workspace from the LAST segment of the tag
1726
+ * - Legacy hierarchical tags like "sparktory > vibetasks > web" → creates "web" workspace
1727
+ * - For new code, just pass the workspace name directly
1081
1728
  */
1082
1729
  async getOrCreateWorkspace(projectTag) {
1083
1730
  const existing = await this.getWorkspaceByPath(projectTag);
1084
1731
  if (existing) return existing;
1085
1732
  const { data: { user } } = await this.supabase.auth.getUser();
1086
1733
  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);
1734
+ const parts = projectTag.split(/\s*[>›]\s*/).map((p) => p.trim()).filter(Boolean);
1735
+ const displayName = parts[parts.length - 1] || projectTag;
1736
+ const slug = displayName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
1737
+ const { data: existingByName } = await this.supabase.from("workspaces").select("*").eq("display_name", displayName).single();
1738
+ if (existingByName) return existingByName;
1739
+ const { data, error } = await this.supabase.from("workspaces").insert({
1740
+ user_id: user.id,
1741
+ slug,
1742
+ display_name: displayName,
1743
+ full_path: displayName,
1744
+ // Flat model: full_path = display_name
1745
+ // parent_id omitted - workspaces are top-level only
1746
+ depth: 0,
1747
+ // Always 0 in flat model
1748
+ color: this.generateWorkspaceColor(displayName)
1749
+ }).select().single();
1750
+ if (error) throw error;
1751
+ return data;
1112
1752
  }
1113
1753
  /**
1114
1754
  * Archive a workspace (soft delete)
@@ -1211,14 +1851,135 @@ var TaskOperations = class _TaskOperations {
1211
1851
  });
1212
1852
  return data;
1213
1853
  }
1214
- };
1215
-
1216
- // src/claude-sync.ts
1217
- function mapStatus(claudeStatus) {
1218
- switch (claudeStatus) {
1219
- case "pending":
1220
- return "todo";
1221
- case "in_progress":
1854
+ // ============================================
1855
+ // MEETING OPERATIONS
1856
+ // ============================================
1857
+ /**
1858
+ * Get meetings for the current user
1859
+ */
1860
+ async getMeetings(options = {}) {
1861
+ const { data: { user } } = await this.supabase.auth.getUser();
1862
+ if (!user) throw new Error("Not authenticated");
1863
+ const limit = options.limit || 20;
1864
+ let query = this.supabase.from("meetings").select(`
1865
+ id,
1866
+ title,
1867
+ description,
1868
+ meeting_type,
1869
+ started_at,
1870
+ ended_at,
1871
+ duration_seconds,
1872
+ participants,
1873
+ organizer_name,
1874
+ organizer_email,
1875
+ summary,
1876
+ key_points,
1877
+ decisions,
1878
+ tags,
1879
+ sync_status,
1880
+ created_at,
1881
+ updated_at
1882
+ `).eq("user_id", user.id).order("started_at", { ascending: false }).limit(limit);
1883
+ if (options.workspace_id) {
1884
+ query = query.eq("workspace_id", options.workspace_id);
1885
+ } else if (this.workspaceId) {
1886
+ query = query.eq("workspace_id", this.workspaceId);
1887
+ }
1888
+ if (options.since) {
1889
+ query = query.gte("started_at", options.since);
1890
+ }
1891
+ const { data: meetings, error } = await query;
1892
+ if (error) {
1893
+ if (error.code === "42P01") {
1894
+ return [];
1895
+ }
1896
+ throw error;
1897
+ }
1898
+ const meetingIds = (meetings || []).map((m) => m.id);
1899
+ if (meetingIds.length > 0) {
1900
+ const { data: actionItems } = await this.supabase.from("meeting_action_items").select("meeting_id").in("meeting_id", meetingIds);
1901
+ const actionCounts = {};
1902
+ (actionItems || []).forEach((ai) => {
1903
+ actionCounts[ai.meeting_id] = (actionCounts[ai.meeting_id] || 0) + 1;
1904
+ });
1905
+ return (meetings || []).map((m) => ({
1906
+ ...m,
1907
+ action_items_count: actionCounts[m.id] || 0
1908
+ }));
1909
+ }
1910
+ return meetings || [];
1911
+ }
1912
+ // ============================================
1913
+ // SKILL OPERATIONS
1914
+ // ============================================
1915
+ /**
1916
+ * Create a custom skill
1917
+ */
1918
+ async createSkill(skill) {
1919
+ const { data: { user } } = await this.supabase.auth.getUser();
1920
+ if (!user) throw new Error("Not authenticated");
1921
+ const slug = skill.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
1922
+ const instructions = `## ${skill.name}
1923
+
1924
+ ${skill.description}
1925
+
1926
+ ### Steps:
1927
+ ${skill.steps.map((s, i) => `${i + 1}. ${s.action}${s.tool ? ` (use ${s.tool})` : ""}`).join("\n")}`;
1928
+ const skillData = {
1929
+ user_id: user.id,
1930
+ workspace_id: skill.workspace_id || this.workspaceId,
1931
+ name: skill.name,
1932
+ slug,
1933
+ description: skill.description,
1934
+ category: skill.category,
1935
+ instructions,
1936
+ trigger_phrases: skill.trigger_phrases,
1937
+ workflow_dag: { nodes: skill.steps.map((s, i) => ({ id: `step-${i + 1}`, data: s })) },
1938
+ is_enabled: true
1939
+ };
1940
+ const { data, error } = await this.supabase.from("skills").insert(skillData).select().single();
1941
+ if (error) {
1942
+ if (error.code === "23505") {
1943
+ throw new Error(`A skill with slug "${slug}" already exists. Choose a different name.`);
1944
+ }
1945
+ if (error.code === "42P01") {
1946
+ throw new Error("Skills system not yet set up. Run migrations first.");
1947
+ }
1948
+ throw error;
1949
+ }
1950
+ return data;
1951
+ }
1952
+ /**
1953
+ * List user's skills
1954
+ */
1955
+ async getSkills(options = {}) {
1956
+ const { data: { user } } = await this.supabase.auth.getUser();
1957
+ if (!user) throw new Error("Not authenticated");
1958
+ let query = this.supabase.from("skills").select("*").eq("user_id", user.id).order("created_at", { ascending: false });
1959
+ if (options.workspace_id) {
1960
+ query = query.eq("workspace_id", options.workspace_id);
1961
+ }
1962
+ if (options.category) {
1963
+ query = query.eq("category", options.category);
1964
+ }
1965
+ if (options.enabled_only !== false) {
1966
+ query = query.eq("is_enabled", true);
1967
+ }
1968
+ const { data, error } = await query;
1969
+ if (error) {
1970
+ if (error.code === "42P01") return [];
1971
+ throw error;
1972
+ }
1973
+ return data || [];
1974
+ }
1975
+ };
1976
+
1977
+ // src/claude-sync.ts
1978
+ function mapStatus(claudeStatus) {
1979
+ switch (claudeStatus) {
1980
+ case "pending":
1981
+ return "todo";
1982
+ case "in_progress":
1222
1983
  return "vibing";
1223
1984
  case "completed":
1224
1985
  return "done";
@@ -1226,6 +1987,19 @@ function mapStatus(claudeStatus) {
1226
1987
  return "todo";
1227
1988
  }
1228
1989
  }
1990
+ function generateA2ATaskId(title, workspaceId = "00000000-0000-0000-0000-000000000000", createdBy = "unknown") {
1991
+ const normalizedTitle = title.toLowerCase().trim().replace(/\s+/g, " ");
1992
+ const hashInput = `${normalizedTitle}::${workspaceId}::${createdBy}`;
1993
+ const crypto = __require("crypto");
1994
+ const hash = crypto.createHash("md5").update(hashInput).digest("hex");
1995
+ return [
1996
+ hash.substring(0, 8),
1997
+ hash.substring(8, 12),
1998
+ hash.substring(12, 16),
1999
+ hash.substring(16, 20),
2000
+ hash.substring(20, 32)
2001
+ ].join("-");
2002
+ }
1229
2003
  function calculateSimilarity(str1, str2) {
1230
2004
  const s1 = str1.toLowerCase().trim();
1231
2005
  const s2 = str2.toLowerCase().trim();
@@ -1246,19 +2020,27 @@ function calculateSimilarity(str1, str2) {
1246
2020
  if (totalWords === 0) return 0;
1247
2021
  return commonWords / totalWords;
1248
2022
  }
1249
- function findBestMatch(todoContent, tasks, threshold = 0.6) {
2023
+ function findBestMatch(todoContent, tasks, threshold = 0.6, workspaceId, createdBy) {
2024
+ if (workspaceId && createdBy) {
2025
+ const targetA2AId = generateA2ATaskId(todoContent, workspaceId, createdBy);
2026
+ for (const task of tasks) {
2027
+ if (task.a2a_task_id === targetA2AId) {
2028
+ return { task, similarity: 1, matchType: "exact" };
2029
+ }
2030
+ }
2031
+ }
1250
2032
  let bestMatch = null;
1251
2033
  for (const task of tasks) {
1252
2034
  const similarity = calculateSimilarity(todoContent, task.title);
1253
2035
  if (similarity >= threshold) {
1254
2036
  if (!bestMatch || similarity > bestMatch.similarity) {
1255
- bestMatch = { task, similarity };
2037
+ bestMatch = { task, similarity, matchType: "fuzzy" };
1256
2038
  }
1257
2039
  }
1258
2040
  if (task.context_notes) {
1259
2041
  const noteSimilarity = calculateSimilarity(todoContent, task.context_notes);
1260
2042
  if (noteSimilarity >= threshold && noteSimilarity > (bestMatch?.similarity || 0)) {
1261
- bestMatch = { task, similarity: noteSimilarity };
2043
+ bestMatch = { task, similarity: noteSimilarity, matchType: "fuzzy" };
1262
2044
  }
1263
2045
  }
1264
2046
  }
@@ -1277,18 +2059,22 @@ var ClaudeSync = class {
1277
2059
  createMissing = false,
1278
2060
  projectTag,
1279
2061
  matchThreshold = 0.6,
1280
- syncPending = true
2062
+ syncPending = true,
2063
+ workspaceId,
2064
+ createdBy
1281
2065
  } = options;
1282
2066
  try {
2067
+ const a2aTaskId = workspaceId && createdBy ? generateA2ATaskId(todo.content, workspaceId, createdBy) : void 0;
1283
2068
  if (todo.status === "pending" && !syncPending) {
1284
2069
  return {
1285
2070
  content: todo.content,
1286
2071
  matched: false,
1287
- action: "skipped"
2072
+ action: "skipped",
2073
+ a2aTaskId
1288
2074
  };
1289
2075
  }
1290
2076
  const tasks = existingTasks || await this.taskOps.getTasks("all");
1291
- const match = findBestMatch(todo.content, tasks, matchThreshold);
2077
+ const match = findBestMatch(todo.content, tasks, matchThreshold, workspaceId, createdBy);
1292
2078
  if (match) {
1293
2079
  const vibeStatus = mapStatus(todo.status);
1294
2080
  const currentStatus = match.task.status || "todo";
@@ -1307,14 +2093,18 @@ var ClaudeSync = class {
1307
2093
  taskId: match.task.id,
1308
2094
  action: "updated",
1309
2095
  previousStatus: currentStatus,
1310
- newStatus: vibeStatus
2096
+ newStatus: vibeStatus,
2097
+ matchType: match.matchType,
2098
+ a2aTaskId: match.task.a2a_task_id || a2aTaskId
1311
2099
  };
1312
2100
  }
1313
2101
  return {
1314
2102
  content: todo.content,
1315
2103
  matched: true,
1316
2104
  taskId: match.task.id,
1317
- action: "skipped"
2105
+ action: "skipped",
2106
+ matchType: match.matchType,
2107
+ a2aTaskId: match.task.a2a_task_id || a2aTaskId
1318
2108
  };
1319
2109
  }
1320
2110
  if (createMissing) {
@@ -1325,20 +2115,27 @@ var ClaudeSync = class {
1325
2115
  project_tag: projectTag,
1326
2116
  created_by: "ai",
1327
2117
  context_notes: todo.activeForm ? `Started: ${todo.activeForm}` : void 0,
1328
- completed: vibeStatus === "done"
2118
+ completed: vibeStatus === "done",
2119
+ workspace_id: workspaceId,
2120
+ a2a_task_id: a2aTaskId
2121
+ // Set A2A task ID on creation
1329
2122
  });
1330
2123
  return {
1331
2124
  content: todo.content,
1332
2125
  matched: false,
1333
2126
  taskId: newTask.id,
1334
2127
  action: "created",
1335
- newStatus: vibeStatus
2128
+ newStatus: vibeStatus,
2129
+ matchType: "exact",
2130
+ // Created with A2A ID
2131
+ a2aTaskId: newTask.a2a_task_id || a2aTaskId
1336
2132
  };
1337
2133
  }
1338
2134
  return {
1339
2135
  content: todo.content,
1340
2136
  matched: false,
1341
- action: "not_found"
2137
+ action: "not_found",
2138
+ a2aTaskId
1342
2139
  };
1343
2140
  } catch (error) {
1344
2141
  return {
@@ -1416,12 +2213,1394 @@ async function syncClaudeTodos(taskOps, todos, options = {}) {
1416
2213
  const sync = new ClaudeSync(taskOps);
1417
2214
  return await sync.syncAllTodos(todos, options);
1418
2215
  }
2216
+
2217
+ // src/openai-embeddings.ts
2218
+ import OpenAI from "openai";
2219
+ var openaiClient = null;
2220
+ function getOpenAIClient() {
2221
+ if (!openaiClient) {
2222
+ const apiKey = process.env.OPENAI_API_KEY;
2223
+ if (!apiKey) {
2224
+ throw new Error("OPENAI_API_KEY environment variable not set. Semantic vector generation requires an OpenAI API key.");
2225
+ }
2226
+ openaiClient = new OpenAI({ apiKey });
2227
+ }
2228
+ return openaiClient;
2229
+ }
2230
+ async function generateTaskEmbedding(input) {
2231
+ const client = getOpenAIClient();
2232
+ const textParts = [input.title];
2233
+ if (input.notes) textParts.push(input.notes);
2234
+ if (input.intent) textParts.push(input.intent);
2235
+ const combinedText = textParts.join("\n");
2236
+ try {
2237
+ const response = await client.embeddings.create({
2238
+ model: "text-embedding-3-small",
2239
+ input: combinedText,
2240
+ encoding_format: "float"
2241
+ });
2242
+ return response.data[0].embedding;
2243
+ } catch (error) {
2244
+ console.error("Error generating embedding:", error);
2245
+ throw new Error(`Failed to generate semantic vector: ${error instanceof Error ? error.message : "Unknown error"}`);
2246
+ }
2247
+ }
2248
+ function generateContextTags(input) {
2249
+ const tags = /* @__PURE__ */ new Set();
2250
+ const keywords = [
2251
+ // Languages
2252
+ "typescript",
2253
+ "javascript",
2254
+ "python",
2255
+ "rust",
2256
+ "go",
2257
+ "java",
2258
+ "sql",
2259
+ // Frameworks
2260
+ "react",
2261
+ "vue",
2262
+ "angular",
2263
+ "next",
2264
+ "expo",
2265
+ "supabase",
2266
+ "postgres",
2267
+ // Areas
2268
+ "frontend",
2269
+ "backend",
2270
+ "database",
2271
+ "api",
2272
+ "ui",
2273
+ "ux",
2274
+ "auth",
2275
+ "oauth",
2276
+ // Actions
2277
+ "fix",
2278
+ "bug",
2279
+ "feature",
2280
+ "refactor",
2281
+ "test",
2282
+ "docs",
2283
+ "deploy",
2284
+ "migration",
2285
+ // Tools
2286
+ "github",
2287
+ "git",
2288
+ "docker",
2289
+ "vercel",
2290
+ "aws",
2291
+ "gcp"
2292
+ ];
2293
+ const text = `${input.title} ${input.notes || ""}`.toLowerCase();
2294
+ for (const keyword of keywords) {
2295
+ if (text.includes(keyword)) {
2296
+ tags.add(keyword);
2297
+ if (tags.size >= 5) break;
2298
+ }
2299
+ }
2300
+ if (input.projectTag) {
2301
+ tags.add(input.projectTag.toLowerCase());
2302
+ }
2303
+ if (tags.size < 3) {
2304
+ const titleWords = input.title.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((w) => w.length > 3 && !["this", "that", "with", "from", "into"].includes(w));
2305
+ for (const word of titleWords) {
2306
+ tags.add(word);
2307
+ if (tags.size >= 5) break;
2308
+ }
2309
+ }
2310
+ return Array.from(tags).slice(0, 5);
2311
+ }
2312
+ function extractIntent(input) {
2313
+ let intent = input.description || input.notes || input.title;
2314
+ const firstSentence = intent.match(/^[^.!?]+[.!?]/);
2315
+ if (firstSentence) {
2316
+ intent = firstSentence[0].trim();
2317
+ }
2318
+ if (intent.length > 200) {
2319
+ intent = intent.slice(0, 197) + "...";
2320
+ }
2321
+ return intent;
2322
+ }
2323
+
2324
+ // src/claude-embeddings.ts
2325
+ import Anthropic from "@anthropic-ai/sdk";
2326
+ import { RECOMMENDED_MODELS } from "@vibetasks/ralph-engine";
2327
+ var anthropicClient = null;
2328
+ function getAnthropicClient() {
2329
+ if (!anthropicClient) {
2330
+ const apiKey = process.env.ANTHROPIC_API_KEY;
2331
+ if (!apiKey) {
2332
+ throw new Error("ANTHROPIC_API_KEY environment variable is required");
2333
+ }
2334
+ anthropicClient = new Anthropic({ apiKey });
2335
+ }
2336
+ return anthropicClient;
2337
+ }
2338
+ async function extractIntentWithClaude(input) {
2339
+ const client = getAnthropicClient();
2340
+ const combinedText = [input.title, input.notes, input.description].filter(Boolean).join("\n");
2341
+ const quickModel = RECOMMENDED_MODELS.quick;
2342
+ const response = await client.messages.create({
2343
+ model: quickModel.apiModel,
2344
+ max_tokens: 200,
2345
+ messages: [
2346
+ {
2347
+ role: "user",
2348
+ content: `Extract the core intent/purpose from this task in one concise sentence (max 200 chars):
2349
+
2350
+ Task: ${combinedText}
2351
+
2352
+ Intent:`
2353
+ }
2354
+ ]
2355
+ });
2356
+ const intent = response.content[0].type === "text" ? response.content[0].text.trim() : "";
2357
+ return intent.slice(0, 200);
2358
+ }
2359
+ async function generateContextTagsWithClaude(input) {
2360
+ const client = getAnthropicClient();
2361
+ const combinedText = [input.title, input.notes].filter(Boolean).join("\n");
2362
+ const quickModel = RECOMMENDED_MODELS.quick;
2363
+ const response = await client.messages.create({
2364
+ model: quickModel.apiModel,
2365
+ max_tokens: 100,
2366
+ messages: [
2367
+ {
2368
+ role: "user",
2369
+ content: `Extract 3-5 semantic tags from this task. Choose from common categories like:
2370
+ Languages: typescript, javascript, python, rust, go, java, sql
2371
+ Frameworks: react, vue, next, expo, supabase, postgres
2372
+ Areas: frontend, backend, database, api, ui, auth, oauth
2373
+ Actions: fix, bug, feature, refactor, test, docs, deploy
2374
+
2375
+ Task: ${combinedText}
2376
+
2377
+ Return ONLY a comma-separated list of tags (lowercase, no spaces after commas):`
2378
+ }
2379
+ ]
2380
+ });
2381
+ const tagsText = response.content[0].type === "text" ? response.content[0].text.trim() : "";
2382
+ let tags = tagsText.toLowerCase().split(",").map((t) => t.trim()).filter((t) => t.length > 0).slice(0, 5);
2383
+ if (input.projectTag && !tags.includes(input.projectTag.toLowerCase())) {
2384
+ tags.push(input.projectTag.toLowerCase());
2385
+ }
2386
+ return tags.slice(0, 5);
2387
+ }
2388
+ async function generateTaskEmbeddingWithClaude(input) {
2389
+ const { generateVoyageEmbedding } = await import("./voyage-embeddings-VXPM2I5X.js");
2390
+ const combinedText = [input.title, input.notes, input.intent].filter(Boolean).join("\n");
2391
+ return generateVoyageEmbedding(combinedText);
2392
+ }
2393
+ async function batchProcessTasksWithClaude(tasks, onProgress) {
2394
+ const results = [];
2395
+ for (let i = 0; i < tasks.length; i++) {
2396
+ const task = tasks[i];
2397
+ try {
2398
+ const [intent, contextTags, embedding] = await Promise.all([
2399
+ extractIntentWithClaude(task),
2400
+ generateContextTagsWithClaude(task),
2401
+ generateTaskEmbeddingWithClaude(task)
2402
+ ]);
2403
+ results.push({
2404
+ id: task.id,
2405
+ intent,
2406
+ contextTags,
2407
+ embedding
2408
+ });
2409
+ if (onProgress) {
2410
+ onProgress(i + 1, tasks.length);
2411
+ }
2412
+ if (i < tasks.length - 1) {
2413
+ await new Promise((resolve) => setTimeout(resolve, 50));
2414
+ }
2415
+ } catch (error) {
2416
+ console.error(`Failed to process task ${task.id}:`, error);
2417
+ }
2418
+ }
2419
+ return results;
2420
+ }
2421
+
2422
+ // src/rlm/engine.ts
2423
+ import Anthropic2 from "@anthropic-ai/sdk";
2424
+ import { VM } from "vm2";
2425
+ import { RECOMMENDED_MODELS as RECOMMENDED_MODELS2 } from "@vibetasks/ralph-engine";
2426
+ var RLMEngine = class {
2427
+ client;
2428
+ config;
2429
+ currentDepth = 0;
2430
+ callCount = 0;
2431
+ totalTokens = { input: 0, output: 0 };
2432
+ startTime = 0;
2433
+ logs = [];
2434
+ constructor(config) {
2435
+ this.client = new Anthropic2({ apiKey: config.apiKey });
2436
+ const codingModel = RECOMMENDED_MODELS2.coding;
2437
+ const quickModel = RECOMMENDED_MODELS2.quick;
2438
+ this.config = {
2439
+ model: config.model || codingModel.apiModel,
2440
+ subLLMModel: config.subLLMModel || quickModel.apiModel,
2441
+ maxDepth: config.maxDepth || 3,
2442
+ timeout: config.timeout || 6e4,
2443
+ verbose: config.verbose || false,
2444
+ apiKey: config.apiKey
2445
+ };
2446
+ }
2447
+ /**
2448
+ * Main RLM completion function
2449
+ * Replaces traditional llm.completion(prompt) calls
2450
+ */
2451
+ async completion(query, context) {
2452
+ this.startTime = Date.now();
2453
+ this.currentDepth = 0;
2454
+ this.callCount = 0;
2455
+ this.totalTokens = { input: 0, output: 0 };
2456
+ this.logs = [];
2457
+ this.log(`[RLM] Starting query with context size: ${context.length} chars`);
2458
+ this.log(`[RLM] Context preview: ${context.substring(0, 200)}...`);
2459
+ const repl = this.createREPLEnvironment(context);
2460
+ const systemPrompt = this.buildSystemPrompt(context.length);
2461
+ try {
2462
+ const answer = await this.recursiveQuery(
2463
+ query,
2464
+ context,
2465
+ repl,
2466
+ systemPrompt,
2467
+ 0
2468
+ );
2469
+ const executionTime = Date.now() - this.startTime;
2470
+ const totalCost = this.calculateCost();
2471
+ this.log(`[RLM] Completed in ${executionTime}ms with ${this.callCount} calls`);
2472
+ return {
2473
+ answer,
2474
+ totalCost,
2475
+ tokensUsed: this.totalTokens,
2476
+ callCount: this.callCount,
2477
+ depth: this.currentDepth,
2478
+ executionTime
2479
+ };
2480
+ } catch (error) {
2481
+ this.log(`[RLM] Error: ${error}`);
2482
+ throw error;
2483
+ }
2484
+ }
2485
+ /**
2486
+ * Recursive query function - core of RLM
2487
+ */
2488
+ async recursiveQuery(query, contextSlice, repl, systemPrompt, depth) {
2489
+ if (depth > this.config.maxDepth) {
2490
+ this.log(`[RLM] Max depth ${this.config.maxDepth} reached, using base LLM`);
2491
+ return this.baseLLMQuery(query, contextSlice.substring(0, 1e5));
2492
+ }
2493
+ this.currentDepth = Math.max(this.currentDepth, depth);
2494
+ this.callCount++;
2495
+ const model = depth === 0 ? this.config.model : this.config.subLLMModel;
2496
+ this.log(`[RLM] Depth ${depth}: Using model ${model}`);
2497
+ const prompt = this.buildREPLPrompt(query, contextSlice, depth);
2498
+ try {
2499
+ const response = await this.client.messages.create({
2500
+ model,
2501
+ max_tokens: 4096,
2502
+ system: systemPrompt,
2503
+ messages: [{
2504
+ role: "user",
2505
+ content: prompt
2506
+ }]
2507
+ });
2508
+ this.totalTokens.input += response.usage.input_tokens;
2509
+ this.totalTokens.output += response.usage.output_tokens;
2510
+ const content = response.content[0];
2511
+ if (content.type !== "text") {
2512
+ throw new Error("Unexpected response type");
2513
+ }
2514
+ const llmResponse = content.text;
2515
+ this.log(`[RLM] Depth ${depth} response: ${llmResponse.substring(0, 200)}...`);
2516
+ if (llmResponse.includes("FINAL(") || llmResponse.includes("FINAL_VAR(")) {
2517
+ return this.extractFinalAnswer(llmResponse);
2518
+ }
2519
+ const codeBlocks = this.extractCodeBlocks(llmResponse);
2520
+ if (codeBlocks.length > 0) {
2521
+ return this.executeREPLCode(codeBlocks, repl, query, systemPrompt, depth);
2522
+ }
2523
+ return llmResponse;
2524
+ } catch (error) {
2525
+ this.log(`[RLM] Error at depth ${depth}: ${error}`);
2526
+ throw error;
2527
+ }
2528
+ }
2529
+ /**
2530
+ * Create REPL environment with context and tools
2531
+ */
2532
+ createREPLEnvironment(context) {
2533
+ const self = this;
2534
+ return {
2535
+ context,
2536
+ // Core recursive function available in REPL
2537
+ llm_query: async (query, contextSlice) => {
2538
+ const slice = contextSlice || context;
2539
+ return self.recursiveQuery(
2540
+ query,
2541
+ slice,
2542
+ self.createREPLEnvironment(slice),
2543
+ self.buildSystemPrompt(slice.length),
2544
+ self.currentDepth + 1
2545
+ );
2546
+ },
2547
+ print: (...args) => {
2548
+ self.log(`[REPL] ${args.join(" ")}`);
2549
+ },
2550
+ // Helper: Regex search
2551
+ regex_search: (pattern, text) => {
2552
+ const target = text || context;
2553
+ const regex = new RegExp(pattern, "gm");
2554
+ const matches = target.match(regex);
2555
+ return matches || [];
2556
+ },
2557
+ // Helper: Chunk text with overlap
2558
+ chunk_text: (text, chunkSize, overlap = 0) => {
2559
+ const chunks = [];
2560
+ const step = chunkSize - overlap;
2561
+ for (let i = 0; i < text.length; i += step) {
2562
+ chunks.push(text.substring(i, i + chunkSize));
2563
+ }
2564
+ return chunks;
2565
+ },
2566
+ // Helper: Extract line range
2567
+ extract_lines: (text, start, end) => {
2568
+ const lines = text.split("\n");
2569
+ return lines.slice(start, end).join("\n");
2570
+ }
2571
+ };
2572
+ }
2573
+ /**
2574
+ * Execute Python code in sandboxed VM
2575
+ */
2576
+ async executeREPLCode(codeBlocks, repl, query, systemPrompt, depth) {
2577
+ this.log(`[RLM] Executing ${codeBlocks.length} code blocks`);
2578
+ const vm = new VM({
2579
+ timeout: this.config.timeout,
2580
+ sandbox: {
2581
+ context: repl.context,
2582
+ llm_query: repl.llm_query,
2583
+ print: repl.print,
2584
+ regex_search: repl.regex_search,
2585
+ chunk_text: repl.chunk_text,
2586
+ extract_lines: repl.extract_lines,
2587
+ // Results storage
2588
+ results: []
2589
+ }
2590
+ });
2591
+ let finalResult;
2592
+ for (const code of codeBlocks) {
2593
+ try {
2594
+ this.log(`[REPL] Executing code:
2595
+ ${code}`);
2596
+ finalResult = await vm.run(code);
2597
+ this.log(`[REPL] Code result: ${JSON.stringify(finalResult)?.substring(0, 200)}`);
2598
+ } catch (error) {
2599
+ this.log(`[REPL] Code execution error: ${error}`);
2600
+ throw new Error(`REPL execution failed: ${error}`);
2601
+ }
2602
+ }
2603
+ if (finalResult) {
2604
+ return String(finalResult);
2605
+ }
2606
+ return this.recursiveQuery(
2607
+ `Based on the REPL execution results, answer: ${query}`,
2608
+ repl.context,
2609
+ repl,
2610
+ systemPrompt,
2611
+ depth
2612
+ );
2613
+ }
2614
+ /**
2615
+ * Base LLM query without recursion (for depth limit or simple queries)
2616
+ */
2617
+ async baseLLMQuery(query, context) {
2618
+ this.callCount++;
2619
+ this.log(`[RLM] Base LLM query (no recursion)`);
2620
+ const response = await this.client.messages.create({
2621
+ model: this.config.subLLMModel,
2622
+ max_tokens: 2048,
2623
+ messages: [{
2624
+ role: "user",
2625
+ content: `Context:
2626
+ ${context}
2627
+
2628
+ Query: ${query}`
2629
+ }]
2630
+ });
2631
+ this.totalTokens.input += response.usage.input_tokens;
2632
+ this.totalTokens.output += response.usage.output_tokens;
2633
+ const content = response.content[0];
2634
+ return content.type === "text" ? content.text : "";
2635
+ }
2636
+ /**
2637
+ * Build system prompt teaching LLM how to use RLM
2638
+ */
2639
+ buildSystemPrompt(contextLength) {
2640
+ return `You are an RLM (Recursive Language Model) agent. You have access to a massive context (${contextLength} characters) stored in the 'context' variable within a Python REPL environment.
2641
+
2642
+ AVAILABLE TOOLS IN REPL:
2643
+
2644
+ 1. **context** (str): The full input context you need to analyze
2645
+ 2. **llm_query(query: str, context_slice: str = None) -> str**: Recursively call yourself on a sub-problem or context chunk
2646
+ 3. **print(*args)**: Debug output
2647
+ 4. **regex_search(pattern: str, text: str = None) -> List[str]**: Search for patterns
2648
+ 5. **chunk_text(text: str, chunk_size: int, overlap: int = 0) -> List[str]**: Split text into chunks
2649
+ 6. **extract_lines(text: str, start: int, end: int) -> str**: Get specific line range
2650
+
2651
+ YOUR STRATEGY:
2652
+
2653
+ 1. **Explore**: First, examine the context structure using print() or regex_search()
2654
+ 2. **Decompose**: Break the problem into sub-queries
2655
+ 3. **Chunk**: Use chunk_text() or regex to find relevant sections
2656
+ 4. **Recurse**: Call llm_query() on each chunk with specific questions
2657
+ 5. **Aggregate**: Combine results from sub-queries
2658
+ 6. **Respond**: Wrap your final answer in FINAL("your answer here")
2659
+
2660
+ IMPORTANT RULES:
2661
+
2662
+ - Do NOT try to process the entire context at once (it's too large)
2663
+ - DO write Python code to search, filter, and chunk the context
2664
+ - DO use llm_query() for recursive sub-problems
2665
+ - DO return your final answer wrapped in FINAL("...")
2666
+ - Keep code simple and focused on the task
2667
+
2668
+ EXAMPLE PATTERN:
2669
+
2670
+ \`\`\`python
2671
+ # 1. Explore context structure
2672
+ print(f"Context length: {len(context)}")
2673
+ print(f"First 500 chars: {context[:500]}")
2674
+
2675
+ # 2. Find relevant sections
2676
+ matches = regex_search(r"function \\w+\\(", context)
2677
+ print(f"Found {len(matches)} functions")
2678
+
2679
+ # 3. Chunk and recurse
2680
+ chunks = chunk_text(context, chunk_size=5000, overlap=200)
2681
+ results = []
2682
+
2683
+ for i, chunk in enumerate(chunks):
2684
+ result = llm_query(
2685
+ f"In this code chunk, find all TODO comments",
2686
+ chunk
2687
+ )
2688
+ results.append(result)
2689
+
2690
+ # 4. Return final answer
2691
+ FINAL(f"Found {len(results)} chunks with TODOs: {results}")
2692
+ \`\`\``;
2693
+ }
2694
+ /**
2695
+ * Build prompt for REPL interaction
2696
+ */
2697
+ buildREPLPrompt(query, contextSlice, depth) {
2698
+ if (depth === 0) {
2699
+ return `You have access to a large context in the 'context' variable.
2700
+
2701
+ User Query: ${query}
2702
+
2703
+ Write Python code to explore the context and answer the query. Use llm_query() for recursive sub-problems.`;
2704
+ } else {
2705
+ return `Sub-query at depth ${depth}:
2706
+
2707
+ ${query}
2708
+
2709
+ Context slice:
2710
+ ${contextSlice.substring(0, 1e3)}...
2711
+
2712
+ Analyze this context slice and provide a focused answer.`;
2713
+ }
2714
+ }
2715
+ /**
2716
+ * Extract code blocks from LLM response
2717
+ */
2718
+ extractCodeBlocks(text) {
2719
+ const codeBlockRegex = /```(?:python)?\n([\s\S]*?)```/g;
2720
+ const blocks = [];
2721
+ let match;
2722
+ while ((match = codeBlockRegex.exec(text)) !== null) {
2723
+ blocks.push(match[1].trim());
2724
+ }
2725
+ return blocks;
2726
+ }
2727
+ /**
2728
+ * Extract final answer from FINAL() or FINAL_VAR()
2729
+ */
2730
+ extractFinalAnswer(text) {
2731
+ const finalRegex = /FINAL\(["']?(.*?)["']?\)/s;
2732
+ const match = text.match(finalRegex);
2733
+ if (match) {
2734
+ return match[1];
2735
+ }
2736
+ return text;
2737
+ }
2738
+ /**
2739
+ * Calculate total cost based on token usage
2740
+ */
2741
+ calculateCost() {
2742
+ const sonnetInputCost = 3 / 1e6;
2743
+ const sonnetOutputCost = 15 / 1e6;
2744
+ const haikuInputCost = 0.8 / 1e6;
2745
+ const haikuOutputCost = 4 / 1e6;
2746
+ const avgInputCost = (sonnetInputCost + haikuInputCost) / 2;
2747
+ const avgOutputCost = (sonnetOutputCost + haikuOutputCost) / 2;
2748
+ return this.totalTokens.input * avgInputCost + this.totalTokens.output * avgOutputCost;
2749
+ }
2750
+ /**
2751
+ * Logging helper
2752
+ */
2753
+ log(message) {
2754
+ if (this.config.verbose) {
2755
+ console.log(message);
2756
+ }
2757
+ this.logs.push(message);
2758
+ }
2759
+ /**
2760
+ * Get execution logs
2761
+ */
2762
+ getLogs() {
2763
+ return this.logs;
2764
+ }
2765
+ };
2766
+
2767
+ // src/rlm/context-loader.ts
2768
+ import * as fs2 from "fs/promises";
2769
+ import * as path2 from "path";
2770
+ import { glob } from "glob";
2771
+ var RLMContextLoader = class {
2772
+ defaultExcludePatterns = [
2773
+ "**/node_modules/**",
2774
+ "**/.git/**",
2775
+ "**/.next/**",
2776
+ "**/.vercel/**",
2777
+ "**/.turbo/**",
2778
+ "**/dist/**",
2779
+ "**/build/**",
2780
+ "**/*.min.js",
2781
+ "**/*.map",
2782
+ "**/package-lock.json",
2783
+ "**/yarn.lock",
2784
+ "**/pnpm-lock.yaml",
2785
+ "**/.DS_Store",
2786
+ "**/*.log",
2787
+ "**/.env*",
2788
+ "**/*.sqlite",
2789
+ "**/*.db"
2790
+ ];
2791
+ defaultIncludePatterns = [
2792
+ "**/*.ts",
2793
+ "**/*.tsx",
2794
+ "**/*.js",
2795
+ "**/*.jsx",
2796
+ "**/*.py",
2797
+ "**/*.java",
2798
+ "**/*.go",
2799
+ "**/*.rs",
2800
+ "**/*.c",
2801
+ "**/*.cpp",
2802
+ "**/*.md",
2803
+ "**/*.json",
2804
+ "**/*.yaml",
2805
+ "**/*.yml",
2806
+ "**/*.sql",
2807
+ "**/*.sh"
2808
+ ];
2809
+ /**
2810
+ * Load entire codebase into single context string
2811
+ */
2812
+ async loadCodebase(options) {
2813
+ const {
2814
+ rootPath,
2815
+ excludePatterns = [],
2816
+ includePatterns = this.defaultIncludePatterns,
2817
+ maxFileSize = 1e6,
2818
+ // 1MB default
2819
+ addFileHeaders = true,
2820
+ addLineNumbers = false
2821
+ } = options;
2822
+ console.log(`[RLM Context Loader] Loading codebase from ${rootPath}`);
2823
+ const allExcludes = [...this.defaultExcludePatterns, ...excludePatterns];
2824
+ const files = await this.findFiles(rootPath, includePatterns, allExcludes);
2825
+ console.log(`[RLM Context Loader] Found ${files.length} files`);
2826
+ const fileContents = [];
2827
+ const fileMetadata = [];
2828
+ let totalSize = 0;
2829
+ for (const filePath of files) {
2830
+ try {
2831
+ const stats = await fs2.stat(filePath);
2832
+ if (stats.size > maxFileSize) {
2833
+ console.log(`[RLM Context Loader] Skipping large file: ${filePath} (${stats.size} bytes)`);
2834
+ continue;
2835
+ }
2836
+ const content = await fs2.readFile(filePath, "utf-8");
2837
+ const relativePath = path2.relative(rootPath, filePath);
2838
+ const lines = content.split("\n").length;
2839
+ let formattedContent = "";
2840
+ if (addFileHeaders) {
2841
+ formattedContent += `
2842
+ ${"=".repeat(80)}
2843
+ `;
2844
+ formattedContent += `FILE: ${relativePath}
2845
+ `;
2846
+ formattedContent += `SIZE: ${stats.size} bytes | LINES: ${lines}
2847
+ `;
2848
+ formattedContent += `${"=".repeat(80)}
2849
+
2850
+ `;
2851
+ }
2852
+ if (addLineNumbers) {
2853
+ const numberedLines = content.split("\n").map((line, i) => {
2854
+ const lineNum = (i + 1).toString().padStart(5, " ");
2855
+ return `${lineNum} | ${line}`;
2856
+ });
2857
+ formattedContent += numberedLines.join("\n");
2858
+ } else {
2859
+ formattedContent += content;
2860
+ }
2861
+ formattedContent += "\n\n";
2862
+ fileContents.push(formattedContent);
2863
+ fileMetadata.push({
2864
+ path: relativePath,
2865
+ size: stats.size,
2866
+ lines
2867
+ });
2868
+ totalSize += stats.size;
2869
+ } catch (error) {
2870
+ console.error(`[RLM Context Loader] Error reading ${filePath}:`, error);
2871
+ }
2872
+ }
2873
+ const fullContext = fileContents.join("");
2874
+ console.log(`[RLM Context Loader] Loaded ${fileMetadata.length} files, ${totalSize} bytes`);
2875
+ console.log(`[RLM Context Loader] Context length: ${fullContext.length} characters`);
2876
+ return {
2877
+ content: fullContext,
2878
+ fileCount: fileMetadata.length,
2879
+ totalSize,
2880
+ files: fileMetadata,
2881
+ metadata: {
2882
+ generatedAt: /* @__PURE__ */ new Date(),
2883
+ rootPath,
2884
+ excludePatterns: allExcludes
2885
+ }
2886
+ };
2887
+ }
2888
+ /**
2889
+ * Load specific files or directories
2890
+ */
2891
+ async loadPaths(rootPath, paths, options) {
2892
+ const includePatterns = paths.map((p) => {
2893
+ if (p.endsWith("/")) {
2894
+ return `${p}**/*`;
2895
+ }
2896
+ return p;
2897
+ });
2898
+ return this.loadCodebase({
2899
+ rootPath,
2900
+ includePatterns,
2901
+ ...options
2902
+ });
2903
+ }
2904
+ /**
2905
+ * Find files matching patterns
2906
+ */
2907
+ async findFiles(rootPath, includePatterns, excludePatterns) {
2908
+ const allFiles = /* @__PURE__ */ new Set();
2909
+ for (const pattern of includePatterns) {
2910
+ const matches = await glob(pattern, {
2911
+ cwd: rootPath,
2912
+ absolute: true,
2913
+ ignore: excludePatterns,
2914
+ nodir: true
2915
+ });
2916
+ matches.forEach((file) => allFiles.add(file));
2917
+ }
2918
+ return Array.from(allFiles).sort();
2919
+ }
2920
+ /**
2921
+ * Create a focused context for specific query
2922
+ * Uses existing context engine's semantic search to pre-filter
2923
+ */
2924
+ async loadFocusedContext(rootPath, query, options) {
2925
+ const { maxFiles = 50, semanticSearch = true } = options || {};
2926
+ console.log(`[RLM Context Loader] Creating focused context for query: ${query}`);
2927
+ const keywords = this.extractKeywords(query);
2928
+ const allFiles = await this.findFiles(
2929
+ rootPath,
2930
+ this.defaultIncludePatterns,
2931
+ this.defaultExcludePatterns
2932
+ );
2933
+ const scoredFiles = allFiles.map((file) => {
2934
+ const relativePath = path2.relative(rootPath, file).toLowerCase();
2935
+ let score = 0;
2936
+ for (const keyword of keywords) {
2937
+ if (relativePath.includes(keyword.toLowerCase())) {
2938
+ score += 10;
2939
+ }
2940
+ }
2941
+ return { file, score };
2942
+ });
2943
+ const topFiles = scoredFiles.sort((a, b) => b.score - a.score).slice(0, maxFiles).map((f) => f.file);
2944
+ console.log(`[RLM Context Loader] Selected ${topFiles.length} focused files`);
2945
+ return this.loadCodebase({
2946
+ rootPath,
2947
+ includePatterns: topFiles.map((f) => path2.relative(rootPath, f)),
2948
+ excludePatterns: []
2949
+ });
2950
+ }
2951
+ /**
2952
+ * Extract keywords from query for relevance scoring
2953
+ */
2954
+ extractKeywords(query) {
2955
+ const stopWords = /* @__PURE__ */ new Set(["the", "a", "an", "in", "on", "at", "for", "to", "of", "and", "or", "is", "are", "was", "were", "how", "what", "where", "when", "why", "which"]);
2956
+ return query.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/).filter((word) => word.length > 2 && !stopWords.has(word));
2957
+ }
2958
+ /**
2959
+ * Stream large codebase in chunks (for progressive loading)
2960
+ */
2961
+ async *streamCodebase(options, chunkSize = 50) {
2962
+ const {
2963
+ rootPath,
2964
+ excludePatterns = [],
2965
+ includePatterns = this.defaultIncludePatterns
2966
+ } = options;
2967
+ const allExcludes = [...this.defaultExcludePatterns, ...excludePatterns];
2968
+ const files = await this.findFiles(rootPath, includePatterns, allExcludes);
2969
+ for (let i = 0; i < files.length; i += chunkSize) {
2970
+ const chunkFiles = files.slice(i, i + chunkSize);
2971
+ const context = await this.loadCodebase({
2972
+ ...options,
2973
+ includePatterns: chunkFiles.map((f) => path2.relative(rootPath, f)),
2974
+ excludePatterns: []
2975
+ });
2976
+ yield {
2977
+ chunk: context.content,
2978
+ progress: Math.min((i + chunkSize) / files.length, 1)
2979
+ };
2980
+ }
2981
+ }
2982
+ /**
2983
+ * Get context statistics without loading full content
2984
+ */
2985
+ async getCodebaseStats(options) {
2986
+ const {
2987
+ rootPath,
2988
+ excludePatterns = [],
2989
+ includePatterns = this.defaultIncludePatterns
2990
+ } = options;
2991
+ const allExcludes = [...this.defaultExcludePatterns, ...excludePatterns];
2992
+ const files = await this.findFiles(rootPath, includePatterns, allExcludes);
2993
+ let totalSize = 0;
2994
+ const fileTypes = /* @__PURE__ */ new Map();
2995
+ for (const file of files) {
2996
+ try {
2997
+ const stats = await fs2.stat(file);
2998
+ totalSize += stats.size;
2999
+ const ext = path2.extname(file);
3000
+ fileTypes.set(ext, (fileTypes.get(ext) || 0) + 1);
3001
+ } catch (error) {
3002
+ }
3003
+ }
3004
+ const topFileTypes = Array.from(fileTypes.entries()).map(([ext, count]) => ({ ext, count })).sort((a, b) => b.count - a.count).slice(0, 10);
3005
+ const estimatedTokens = Math.ceil(totalSize / 4);
3006
+ return {
3007
+ fileCount: files.length,
3008
+ totalSize,
3009
+ estimatedTokens,
3010
+ topFileTypes
3011
+ };
3012
+ }
3013
+ };
3014
+
3015
+ // src/rlm/mcp-tool.ts
3016
+ var RLMMCPTool = class {
3017
+ engine;
3018
+ contextLoader;
3019
+ contextCache = /* @__PURE__ */ new Map();
3020
+ constructor(anthropicApiKey) {
3021
+ this.engine = new RLMEngine({
3022
+ apiKey: anthropicApiKey,
3023
+ verbose: false
3024
+ // Controlled per query
3025
+ });
3026
+ this.contextLoader = new RLMContextLoader();
3027
+ }
3028
+ /**
3029
+ * Main RLM query tool
3030
+ */
3031
+ async queryCodebase(input) {
3032
+ const {
3033
+ query,
3034
+ workspacePath = process.cwd(),
3035
+ focused = true,
3036
+ maxFiles = 100,
3037
+ verbose = false,
3038
+ maxDepth = 3
3039
+ } = input;
3040
+ console.log(`[RLM MCP Tool] Query: ${query}`);
3041
+ console.log(`[RLM MCP Tool] Workspace: ${workspacePath}`);
3042
+ console.log(`[RLM MCP Tool] Focused mode: ${focused}`);
3043
+ this.engine = new RLMEngine({
3044
+ apiKey: this.engine["config"].apiKey,
3045
+ verbose,
3046
+ maxDepth
3047
+ });
3048
+ let context;
3049
+ const cacheKey = `${workspacePath}-${focused}-${maxFiles}`;
3050
+ if (this.contextCache.has(cacheKey)) {
3051
+ console.log(`[RLM MCP Tool] Using cached context`);
3052
+ context = this.contextCache.get(cacheKey);
3053
+ } else {
3054
+ console.log(`[RLM MCP Tool] Loading context...`);
3055
+ if (focused) {
3056
+ context = await this.contextLoader.loadFocusedContext(
3057
+ workspacePath,
3058
+ query,
3059
+ { maxFiles }
3060
+ );
3061
+ } else {
3062
+ context = await this.contextLoader.loadCodebase({
3063
+ rootPath: workspacePath,
3064
+ addFileHeaders: true,
3065
+ addLineNumbers: false
3066
+ });
3067
+ }
3068
+ this.contextCache.set(cacheKey, context);
3069
+ setTimeout(() => this.contextCache.delete(cacheKey), 5 * 60 * 1e3);
3070
+ }
3071
+ console.log(`[RLM MCP Tool] Context loaded: ${context.fileCount} files, ${context.content.length} chars`);
3072
+ const result = await this.engine.completion(query, context.content);
3073
+ console.log(`[RLM MCP Tool] Query completed in ${result.executionTime}ms`);
3074
+ console.log(`[RLM MCP Tool] Cost: $${result.totalCost.toFixed(4)}`);
3075
+ return {
3076
+ ...result,
3077
+ contextUsed: {
3078
+ fileCount: context.fileCount,
3079
+ totalSize: context.totalSize,
3080
+ estimatedTokens: Math.ceil(context.content.length / 4)
3081
+ },
3082
+ logs: verbose ? this.engine.getLogs() : void 0
3083
+ };
3084
+ }
3085
+ /**
3086
+ * Get codebase statistics (fast, no LLM calls)
3087
+ */
3088
+ async getCodebaseStats(workspacePath) {
3089
+ const path3 = workspacePath || process.cwd();
3090
+ const stats = await this.contextLoader.getCodebaseStats({
3091
+ rootPath: path3
3092
+ });
3093
+ const estimatedCost = stats.estimatedTokens / 1e5 * 0.01;
3094
+ return {
3095
+ ...stats,
3096
+ estimatedCost
3097
+ };
3098
+ }
3099
+ /**
3100
+ * Clear context cache
3101
+ */
3102
+ clearCache() {
3103
+ this.contextCache.clear();
3104
+ console.log(`[RLM MCP Tool] Context cache cleared`);
3105
+ }
3106
+ /**
3107
+ * Prefetch and cache codebase context
3108
+ */
3109
+ async prefetchContext(workspacePath, focused = true) {
3110
+ const path3 = workspacePath || process.cwd();
3111
+ const cacheKey = `${path3}-${focused}-100`;
3112
+ console.log(`[RLM MCP Tool] Prefetching context for ${path3}...`);
3113
+ const context = await this.contextLoader.loadCodebase({
3114
+ rootPath: path3,
3115
+ addFileHeaders: true,
3116
+ addLineNumbers: false
3117
+ });
3118
+ this.contextCache.set(cacheKey, context);
3119
+ console.log(`[RLM MCP Tool] Context prefetched and cached`);
3120
+ }
3121
+ };
3122
+ var RLM_MCP_TOOLS = [
3123
+ {
3124
+ name: "rlm_query_codebase",
3125
+ description: `Query massive codebases (10M+ tokens) using Recursive Language Models (RLM).
3126
+
3127
+ RLM treats the codebase as an external environment and recursively searches through it,
3128
+ allowing you to ask complex questions across thousands of files without context window limits.
3129
+
3130
+ Use this for:
3131
+ - "Find all API endpoints that handle user authentication"
3132
+ - "What files depend on UserService and how do they use it?"
3133
+ - "List all TODO comments across the entire codebase"
3134
+ - "How does the payment flow work end-to-end?"
3135
+ - "Find all functions that make database queries"
3136
+
3137
+ Key benefits:
3138
+ - Handles 10M+ tokens (thousands of files)
3139
+ - 29% more accurate than traditional approaches
3140
+ - 3x cheaper than loading everything into context
3141
+ - No information loss (no summarization)
3142
+
3143
+ Parameters:
3144
+ - query: Your question about the codebase
3145
+ - focused: Use semantic search to pre-filter relevant files (faster, cheaper, recommended)
3146
+ - maxFiles: Max files to include in focused mode (default: 100)
3147
+ - verbose: Show detailed execution logs`,
3148
+ inputSchema: {
3149
+ type: "object",
3150
+ properties: {
3151
+ query: {
3152
+ type: "string",
3153
+ description: "Your question about the codebase"
3154
+ },
3155
+ focused: {
3156
+ type: "boolean",
3157
+ description: "Use semantic search to pre-filter files (recommended)",
3158
+ default: true
3159
+ },
3160
+ maxFiles: {
3161
+ type: "number",
3162
+ description: "Max files to include (focused mode only)",
3163
+ default: 100
3164
+ },
3165
+ verbose: {
3166
+ type: "boolean",
3167
+ description: "Show detailed execution logs",
3168
+ default: false
3169
+ }
3170
+ },
3171
+ required: ["query"]
3172
+ }
3173
+ },
3174
+ {
3175
+ name: "rlm_get_codebase_stats",
3176
+ description: `Get statistics about the current codebase without querying the LLM.
3177
+
3178
+ Shows:
3179
+ - File count
3180
+ - Total size (bytes)
3181
+ - Estimated tokens
3182
+ - Estimated cost for RLM query
3183
+ - Top file types`,
3184
+ inputSchema: {
3185
+ type: "object",
3186
+ properties: {}
3187
+ }
3188
+ },
3189
+ {
3190
+ name: "rlm_clear_cache",
3191
+ description: "Clear the RLM context cache to force reloading files",
3192
+ inputSchema: {
3193
+ type: "object",
3194
+ properties: {}
3195
+ }
3196
+ }
3197
+ ];
3198
+
3199
+ // src/task-compaction.ts
3200
+ import Anthropic3 from "@anthropic-ai/sdk";
3201
+ import { RECOMMENDED_MODELS as RECOMMENDED_MODELS3 } from "@vibetasks/ralph-engine";
3202
+ var MAX_RETRIES = 3;
3203
+ var INITIAL_BACKOFF_MS = 1e3;
3204
+ var TaskCompactionService = class {
3205
+ anthropic;
3206
+ model;
3207
+ constructor(apiKey) {
3208
+ const key = apiKey || process.env.ANTHROPIC_API_KEY;
3209
+ if (!key) {
3210
+ throw new Error("ANTHROPIC_API_KEY required for task compaction");
3211
+ }
3212
+ this.anthropic = new Anthropic3({ apiKey: key });
3213
+ this.model = RECOMMENDED_MODELS3.quick.apiModel;
3214
+ }
3215
+ /**
3216
+ * Compact a single task by generating a summary
3217
+ */
3218
+ async compactTask(task) {
3219
+ const originalContent = this.buildTaskContent(task);
3220
+ const originalSize = originalContent.length;
3221
+ try {
3222
+ const summary = await this.generateSummaryWithRetry(task, originalContent);
3223
+ const compactedSize = summary.length;
3224
+ return {
3225
+ taskId: task.id,
3226
+ originalSize,
3227
+ compactedSize,
3228
+ summary,
3229
+ success: true
3230
+ };
3231
+ } catch (error) {
3232
+ return {
3233
+ taskId: task.id,
3234
+ originalSize,
3235
+ compactedSize: 0,
3236
+ summary: "",
3237
+ success: false,
3238
+ error: error.message
3239
+ };
3240
+ }
3241
+ }
3242
+ /**
3243
+ * Build the full content of a task for summarization
3244
+ */
3245
+ buildTaskContent(task) {
3246
+ const parts = [];
3247
+ parts.push(`**Title:** ${task.title}`);
3248
+ if (task.description) {
3249
+ parts.push(`
3250
+ **Description:**
3251
+ ${task.description}`);
3252
+ }
3253
+ if (task.notes) {
3254
+ parts.push(`
3255
+ **Notes:**
3256
+ ${task.notes}`);
3257
+ }
3258
+ if (task.context_notes) {
3259
+ parts.push(`
3260
+ **Context Notes:**
3261
+ ${task.context_notes}`);
3262
+ }
3263
+ const criteria = task.acceptance_criteria;
3264
+ if (criteria && Array.isArray(criteria) && criteria.length > 0) {
3265
+ const criteriaList = criteria.map((c) => `- [${c.met ? "x" : " "}] ${c.description}`).join("\n");
3266
+ parts.push(`
3267
+ **Acceptance Criteria:**
3268
+ ${criteriaList}`);
3269
+ }
3270
+ const subtasks = task.subtasks || [];
3271
+ if (subtasks.length > 0) {
3272
+ const subtaskList = subtasks.map((s) => `- [${s.done ? "x" : " "}] ${s.title}`).join("\n");
3273
+ parts.push(`
3274
+ **Subtasks:**
3275
+ ${subtaskList}`);
3276
+ }
3277
+ return parts.join("\n");
3278
+ }
3279
+ /**
3280
+ * Generate summary with exponential backoff retry
3281
+ */
3282
+ async generateSummaryWithRetry(task, content) {
3283
+ let lastError = null;
3284
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
3285
+ if (attempt > 0) {
3286
+ const backoff = INITIAL_BACKOFF_MS * Math.pow(2, attempt - 1);
3287
+ await this.sleep(backoff);
3288
+ }
3289
+ try {
3290
+ return await this.generateSummary(task, content);
3291
+ } catch (error) {
3292
+ lastError = error;
3293
+ if (!this.isRetryable(error)) {
3294
+ throw error;
3295
+ }
3296
+ }
3297
+ }
3298
+ throw lastError || new Error("Failed after retries");
3299
+ }
3300
+ /**
3301
+ * Generate the actual summary using Claude
3302
+ */
3303
+ async generateSummary(task, content) {
3304
+ const prompt = this.buildPrompt(task, content);
3305
+ const response = await this.anthropic.messages.create({
3306
+ model: this.model,
3307
+ max_tokens: 512,
3308
+ messages: [
3309
+ {
3310
+ role: "user",
3311
+ content: prompt
3312
+ }
3313
+ ]
3314
+ });
3315
+ if (response.content.length === 0) {
3316
+ throw new Error("Empty response from API");
3317
+ }
3318
+ const textBlock = response.content.find((block) => block.type === "text");
3319
+ if (!textBlock || textBlock.type !== "text") {
3320
+ throw new Error("No text content in response");
3321
+ }
3322
+ return textBlock.text;
3323
+ }
3324
+ /**
3325
+ * Build the compaction prompt
3326
+ */
3327
+ buildPrompt(task, content) {
3328
+ return `You are summarizing a completed software task for long-term storage. Your goal is to COMPRESS the content - the output MUST be significantly shorter than the input while preserving key technical decisions and outcomes.
3329
+
3330
+ ${content}
3331
+
3332
+ IMPORTANT: Your summary must be shorter than the original. Be concise and eliminate redundancy. Focus on what was actually accomplished.
3333
+
3334
+ Provide a summary in this exact format:
3335
+
3336
+ **Summary:** [2-3 concise sentences covering what was done and why]
3337
+
3338
+ **Key Decisions:** [Brief bullet points of only the most important technical choices, if any]
3339
+
3340
+ **Resolution:** [One sentence on final outcome]`;
3341
+ }
3342
+ /**
3343
+ * Check if an error is retryable
3344
+ */
3345
+ isRetryable(error) {
3346
+ if (!error) return false;
3347
+ if (error.code === "ETIMEDOUT" || error.code === "ECONNRESET") {
3348
+ return true;
3349
+ }
3350
+ const status = error.status || error.statusCode;
3351
+ if (status === 429 || status >= 500 && status < 600) {
3352
+ return true;
3353
+ }
3354
+ return false;
3355
+ }
3356
+ sleep(ms) {
3357
+ return new Promise((resolve) => setTimeout(resolve, ms));
3358
+ }
3359
+ };
3360
+ function createCompactionService(apiKey) {
3361
+ return new TaskCompactionService(apiKey);
3362
+ }
3363
+
3364
+ // src/task-auto-complete.ts
3365
+ import issueParser from "issue-parser";
3366
+ var createVibetasksParser = () => issueParser({
3367
+ actions: {
3368
+ // Keywords that close/complete a task
3369
+ close: [
3370
+ "close",
3371
+ "closes",
3372
+ "closed",
3373
+ "closing",
3374
+ "fix",
3375
+ "fixes",
3376
+ "fixed",
3377
+ "fixing",
3378
+ "resolve",
3379
+ "resolves",
3380
+ "resolved",
3381
+ "resolving",
3382
+ "complete",
3383
+ "completes",
3384
+ "completed",
3385
+ "completing",
3386
+ "done",
3387
+ "finish",
3388
+ "finishes",
3389
+ "finished",
3390
+ "finishing",
3391
+ "implement",
3392
+ "implements",
3393
+ "implemented",
3394
+ "implementing",
3395
+ "ship",
3396
+ "ships",
3397
+ "shipped",
3398
+ "shipping"
3399
+ ],
3400
+ // Keywords that indicate progress (not closing)
3401
+ progress: [
3402
+ "ref",
3403
+ "refs",
3404
+ "reference",
3405
+ "references",
3406
+ "see",
3407
+ "related",
3408
+ "relates",
3409
+ "wip",
3410
+ "working",
3411
+ "progress",
3412
+ "part",
3413
+ "partial",
3414
+ "towards",
3415
+ "for"
3416
+ ]
3417
+ },
3418
+ // Prefixes that identify task IDs
3419
+ issuePrefixes: ["#", "VT-", "VT#", "vt-", "vt#", "TASK-", "task-"]
3420
+ });
3421
+ var parseTaskRefs = createVibetasksParser();
3422
+ var BRANCH_PATTERNS = [
3423
+ // feature/VT-42-description or fix/VT-42-description
3424
+ /^(?:feature|fix|bugfix|hotfix|chore|refactor|docs|test)\/VT-(\d+)/i,
3425
+ // feature/42-description (just the number)
3426
+ /^(?:feature|fix|bugfix|hotfix|chore|refactor|docs|test)\/(\d+)-/i,
3427
+ // VT-42 anywhere in branch name
3428
+ /VT-(\d+)/i,
3429
+ // vt-42 lowercase
3430
+ /vt-(\d+)/i,
3431
+ // TASK-42 format
3432
+ /TASK-(\d+)/i,
3433
+ // Just issue-42 or issue/42
3434
+ /issue[-\/](\d+)/i
3435
+ ];
3436
+ function extractFromBranch(branchName) {
3437
+ for (const pattern of BRANCH_PATTERNS) {
3438
+ const match = branchName.match(pattern);
3439
+ if (match && match[1]) {
3440
+ return {
3441
+ taskId: match[1],
3442
+ action: "progress",
3443
+ // Branch creation = in progress, not closing
3444
+ source: "branch_name",
3445
+ raw: branchName,
3446
+ prefix: match[0].replace(match[1], "")
3447
+ };
3448
+ }
3449
+ }
3450
+ return null;
3451
+ }
3452
+ function extractFromText(text, source) {
3453
+ if (!text || typeof text !== "string") return [];
3454
+ const refs = [];
3455
+ const parsed = parseTaskRefs(text);
3456
+ if (parsed.actions?.close) {
3457
+ for (const ref of parsed.actions.close) {
3458
+ refs.push({
3459
+ taskId: ref.issue,
3460
+ action: "close",
3461
+ source,
3462
+ raw: ref.raw,
3463
+ prefix: ref.prefix
3464
+ });
3465
+ }
3466
+ }
3467
+ if (parsed.actions?.progress) {
3468
+ for (const ref of parsed.actions.progress) {
3469
+ refs.push({
3470
+ taskId: ref.issue,
3471
+ action: "progress",
3472
+ source,
3473
+ raw: ref.raw,
3474
+ prefix: ref.prefix
3475
+ });
3476
+ }
3477
+ }
3478
+ const barePattern = /\bVT-(\d+)\b/gi;
3479
+ let match;
3480
+ while ((match = barePattern.exec(text)) !== null) {
3481
+ const taskId = match[1];
3482
+ if (!refs.some((r) => r.taskId === taskId)) {
3483
+ refs.push({
3484
+ taskId,
3485
+ action: "progress",
3486
+ source,
3487
+ raw: match[0],
3488
+ prefix: "VT-"
3489
+ });
3490
+ }
3491
+ }
3492
+ return refs;
3493
+ }
3494
+ function extractTaskRefs(options) {
3495
+ const refs = [];
3496
+ if (options.commits?.length) {
3497
+ for (const commit of options.commits) {
3498
+ if (commit.message) {
3499
+ refs.push(...extractFromText(commit.message, "commit_message"));
3500
+ }
3501
+ }
3502
+ }
3503
+ if (options.branchName) {
3504
+ const branchRef = extractFromBranch(options.branchName);
3505
+ if (branchRef) {
3506
+ refs.push(branchRef);
3507
+ }
3508
+ }
3509
+ if (options.prTitle) {
3510
+ refs.push(...extractFromText(options.prTitle, "pr_title"));
3511
+ }
3512
+ if (options.prBody) {
3513
+ refs.push(...extractFromText(options.prBody, "pr_body"));
3514
+ }
3515
+ return dedupeRefs(refs);
3516
+ }
3517
+ function dedupeRefs(refs) {
3518
+ const byTaskId = /* @__PURE__ */ new Map();
3519
+ for (const ref of refs) {
3520
+ const existing = byTaskId.get(ref.taskId);
3521
+ if (!existing || ref.action === "close" && existing.action === "progress") {
3522
+ byTaskId.set(ref.taskId, ref);
3523
+ }
3524
+ }
3525
+ return Array.from(byTaskId.values());
3526
+ }
3527
+ function processTaskRefs(options) {
3528
+ const refs = extractTaskRefs(options);
3529
+ const tasksToClose = refs.filter((r) => r.action === "close");
3530
+ const tasksToProgress = refs.filter((r) => r.action === "progress");
3531
+ const allTaskIds = [...new Set(refs.map((r) => r.taskId))];
3532
+ return {
3533
+ tasksToClose,
3534
+ tasksToProgress,
3535
+ allTaskIds
3536
+ };
3537
+ }
3538
+ function createBranchName(task, type = "feature") {
3539
+ const slug = task.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
3540
+ return `${type}/VT-${task.short_id}-${slug}`;
3541
+ }
3542
+ function branchHasTaskRef(branchName) {
3543
+ return extractFromBranch(branchName) !== null;
3544
+ }
3545
+ function getTaskIdFromBranch(branchName) {
3546
+ const ref = extractFromBranch(branchName);
3547
+ return ref?.taskId ?? null;
3548
+ }
3549
+ function processGitHubPush(payload) {
3550
+ const branchName = payload.ref.replace("refs/heads/", "");
3551
+ return processTaskRefs({
3552
+ commits: payload.commits,
3553
+ branchName
3554
+ });
3555
+ }
3556
+ function processGitHubPullRequest(payload) {
3557
+ const pr = payload.pull_request;
3558
+ return processTaskRefs({
3559
+ branchName: pr.head.ref,
3560
+ prTitle: pr.title,
3561
+ prBody: pr.body ?? void 0
3562
+ });
3563
+ }
3564
+ function generateCompletionNotes(options) {
3565
+ if (options.source === "pr_merged" && options.prNumber) {
3566
+ const mergedBy = options.mergedBy ? ` by @${options.mergedBy}` : "";
3567
+ return `Auto-completed: PR #${options.prNumber} merged${mergedBy}`;
3568
+ }
3569
+ if (options.source === "push" && options.commitId) {
3570
+ return `Auto-completed: Commit ${options.commitId.substring(0, 7)} pushed`;
3571
+ }
3572
+ return "Auto-completed via GitHub integration";
3573
+ }
1419
3574
  export {
1420
3575
  AuthManager,
1421
3576
  ClaudeSync,
1422
3577
  ConfigManager,
3578
+ IntegrationSyncService,
3579
+ RLMContextLoader,
3580
+ RLMEngine,
3581
+ RLMMCPTool,
3582
+ RLM_MCP_TOOLS,
3583
+ TaskCompactionService,
1423
3584
  TaskOperations,
3585
+ batchProcessTasksWithClaude,
3586
+ branchHasTaskRef,
3587
+ createBranchName,
3588
+ createCompactionService,
1424
3589
  createSupabaseClient,
1425
3590
  createSupabaseClientFromEnv,
3591
+ extractFromBranch,
3592
+ extractFromText,
3593
+ extractIntent,
3594
+ extractIntentWithClaude,
3595
+ extractTaskRefs,
3596
+ generateCompletionNotes,
3597
+ generateContextTags,
3598
+ generateContextTagsWithClaude,
3599
+ generateTaskEmbedding,
3600
+ generateTaskEmbeddingWithClaude,
3601
+ getTaskIdFromBranch,
3602
+ processGitHubPullRequest,
3603
+ processGitHubPush,
3604
+ processTaskRefs,
1426
3605
  syncClaudeTodos
1427
3606
  };