@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/chunk-3RG5ZIWI.js +10 -0
- package/dist/index.d.ts +828 -28
- package/dist/index.js +2248 -69
- package/dist/voyage-embeddings-VXPM2I5X.js +98 -0
- package/package.json +55 -48
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
|
-
|
|
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.
|
|
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()
|
|
634
|
-
if (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
|
-
|
|
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(
|
|
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
|
-
|
|
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:
|
|
1051
|
-
|
|
1052
|
-
|
|
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
|
|
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
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
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
|
-
//
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
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
|
};
|