@vibescope/mcp-server 0.3.0 → 0.3.3

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.
Files changed (154) hide show
  1. package/dist/api-client/blockers.d.ts +46 -0
  2. package/dist/api-client/blockers.js +43 -0
  3. package/dist/api-client/cost.d.ts +112 -0
  4. package/dist/api-client/cost.js +76 -0
  5. package/dist/api-client/decisions.d.ts +55 -0
  6. package/dist/api-client/decisions.js +32 -0
  7. package/dist/api-client/discovery.d.ts +62 -0
  8. package/dist/api-client/discovery.js +21 -0
  9. package/dist/api-client/ideas.d.ts +75 -0
  10. package/dist/api-client/ideas.js +36 -0
  11. package/dist/api-client/index.d.ts +749 -0
  12. package/dist/api-client/index.js +291 -0
  13. package/dist/api-client/project.d.ts +132 -0
  14. package/dist/api-client/project.js +45 -0
  15. package/dist/api-client/session.d.ts +163 -0
  16. package/dist/api-client/session.js +52 -0
  17. package/dist/api-client/tasks.d.ts +328 -0
  18. package/dist/api-client/tasks.js +132 -0
  19. package/dist/api-client/types.d.ts +25 -0
  20. package/dist/api-client/types.js +4 -0
  21. package/dist/api-client/worktrees.d.ts +33 -0
  22. package/dist/api-client/worktrees.js +26 -0
  23. package/dist/api-client.d.ts +9 -0
  24. package/dist/api-client.js +104 -25
  25. package/dist/cli-init.d.ts +17 -0
  26. package/dist/cli-init.js +445 -0
  27. package/dist/cli.js +0 -0
  28. package/dist/handlers/cloud-agents.d.ts +21 -0
  29. package/dist/handlers/cloud-agents.js +91 -0
  30. package/dist/handlers/discovery.js +7 -0
  31. package/dist/handlers/index.d.ts +1 -0
  32. package/dist/handlers/index.js +3 -0
  33. package/dist/handlers/session.js +3 -1
  34. package/dist/handlers/tasks.js +10 -12
  35. package/dist/handlers/types.d.ts +2 -1
  36. package/dist/handlers/validation.js +5 -1
  37. package/dist/index.js +8 -3
  38. package/dist/token-tracking.js +2 -2
  39. package/dist/tools/blockers.d.ts +13 -0
  40. package/dist/tools/blockers.js +119 -0
  41. package/dist/tools/bodies-of-work.d.ts +19 -0
  42. package/dist/tools/bodies-of-work.js +280 -0
  43. package/dist/tools/cloud-agents.d.ts +9 -0
  44. package/dist/tools/cloud-agents.js +67 -0
  45. package/dist/tools/connectors.d.ts +14 -0
  46. package/dist/tools/connectors.js +188 -0
  47. package/dist/tools/cost.d.ts +11 -0
  48. package/dist/tools/cost.js +108 -0
  49. package/dist/tools/decisions.d.ts +12 -0
  50. package/dist/tools/decisions.js +108 -0
  51. package/dist/tools/deployment.d.ts +24 -0
  52. package/dist/tools/deployment.js +439 -0
  53. package/dist/tools/discovery.d.ts +10 -0
  54. package/dist/tools/discovery.js +73 -0
  55. package/dist/tools/fallback.d.ts +11 -0
  56. package/dist/tools/fallback.js +108 -0
  57. package/dist/tools/file-checkouts.d.ts +13 -0
  58. package/dist/tools/file-checkouts.js +141 -0
  59. package/dist/tools/findings.d.ts +13 -0
  60. package/dist/tools/findings.js +98 -0
  61. package/dist/tools/git-issues.d.ts +11 -0
  62. package/dist/tools/git-issues.js +127 -0
  63. package/dist/tools/ideas.d.ts +13 -0
  64. package/dist/tools/ideas.js +159 -0
  65. package/dist/tools/index.d.ts +71 -0
  66. package/dist/tools/index.js +98 -0
  67. package/dist/tools/milestones.d.ts +12 -0
  68. package/dist/tools/milestones.js +115 -0
  69. package/dist/tools/organizations.d.ts +17 -0
  70. package/dist/tools/organizations.js +221 -0
  71. package/dist/tools/progress.d.ts +9 -0
  72. package/dist/tools/progress.js +70 -0
  73. package/dist/tools/project.d.ts +13 -0
  74. package/dist/tools/project.js +199 -0
  75. package/dist/tools/requests.d.ts +10 -0
  76. package/dist/tools/requests.js +65 -0
  77. package/dist/tools/roles.d.ts +11 -0
  78. package/dist/tools/roles.js +109 -0
  79. package/dist/tools/session.d.ts +15 -0
  80. package/dist/tools/session.js +178 -0
  81. package/dist/tools/sprints.d.ts +18 -0
  82. package/dist/tools/sprints.js +295 -0
  83. package/dist/tools/tasks.d.ts +27 -0
  84. package/dist/tools/tasks.js +539 -0
  85. package/dist/tools/types.d.ts +7 -0
  86. package/dist/tools/types.js +6 -0
  87. package/dist/tools/validation.d.ts +10 -0
  88. package/dist/tools/validation.js +72 -0
  89. package/dist/tools/worktrees.d.ts +9 -0
  90. package/dist/tools/worktrees.js +63 -0
  91. package/dist/utils.d.ts +66 -0
  92. package/dist/utils.js +102 -0
  93. package/docs/TOOLS.md +55 -2
  94. package/package.json +5 -3
  95. package/scripts/generate-docs.ts +1 -1
  96. package/src/api-client/blockers.ts +86 -0
  97. package/src/api-client/cost.ts +185 -0
  98. package/src/api-client/decisions.ts +87 -0
  99. package/src/api-client/discovery.ts +81 -0
  100. package/src/api-client/ideas.ts +112 -0
  101. package/src/api-client/index.ts +378 -0
  102. package/src/api-client/project.ts +179 -0
  103. package/src/api-client/session.ts +220 -0
  104. package/src/api-client/tasks.ts +450 -0
  105. package/src/api-client/types.ts +32 -0
  106. package/src/api-client/worktrees.ts +53 -0
  107. package/src/api-client.test.ts +136 -9
  108. package/src/api-client.ts +125 -27
  109. package/src/cli-init.ts +504 -0
  110. package/src/handlers/__test-utils__.ts +2 -0
  111. package/src/handlers/cloud-agents.ts +138 -0
  112. package/src/handlers/discovery.ts +7 -0
  113. package/src/handlers/index.ts +3 -0
  114. package/src/handlers/session.ts +3 -1
  115. package/src/handlers/tasks.ts +10 -12
  116. package/src/handlers/tool-categories.test.ts +1 -1
  117. package/src/handlers/types.ts +2 -1
  118. package/src/handlers/validation.ts +6 -1
  119. package/src/index.test.ts +2 -2
  120. package/src/index.ts +8 -2
  121. package/src/token-tracking.ts +3 -2
  122. package/src/tools/blockers.ts +122 -0
  123. package/src/tools/bodies-of-work.ts +283 -0
  124. package/src/tools/cloud-agents.ts +70 -0
  125. package/src/tools/connectors.ts +191 -0
  126. package/src/tools/cost.ts +111 -0
  127. package/src/tools/decisions.ts +111 -0
  128. package/src/tools/deployment.ts +442 -0
  129. package/src/tools/discovery.ts +76 -0
  130. package/src/tools/fallback.ts +111 -0
  131. package/src/tools/file-checkouts.ts +145 -0
  132. package/src/tools/findings.ts +101 -0
  133. package/src/tools/git-issues.ts +130 -0
  134. package/src/tools/ideas.ts +162 -0
  135. package/src/tools/index.ts +131 -0
  136. package/src/tools/milestones.ts +118 -0
  137. package/src/tools/organizations.ts +224 -0
  138. package/src/tools/progress.ts +73 -0
  139. package/src/tools/project.ts +202 -0
  140. package/src/tools/requests.ts +68 -0
  141. package/src/tools/roles.ts +112 -0
  142. package/src/tools/session.ts +181 -0
  143. package/src/tools/sprints.ts +298 -0
  144. package/src/tools/tasks.ts +542 -0
  145. package/src/tools/tools.test.ts +222 -0
  146. package/src/tools/types.ts +9 -0
  147. package/src/tools/validation.ts +75 -0
  148. package/src/tools/worktrees.ts +66 -0
  149. package/src/tools.test.ts +1 -1
  150. package/src/utils.test.ts +229 -0
  151. package/src/utils.ts +117 -0
  152. package/dist/tools.d.ts +0 -2
  153. package/dist/tools.js +0 -3602
  154. package/src/tools.ts +0 -3607
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Decisions API Methods
3
+ *
4
+ * Methods for decision tracking:
5
+ * - getDecisions: List decisions for a project
6
+ * - getDecision: Get a single decision
7
+ * - logDecision: Log a new decision
8
+ * - deleteDecision: Delete a decision
9
+ * - getDecisionsStats: Get decision statistics
10
+ */
11
+
12
+ import type { ApiResponse, ProxyFn } from './types.js';
13
+
14
+ export interface DecisionsMethods {
15
+ getDecisions(projectId: string, options?: {
16
+ limit?: number;
17
+ offset?: number;
18
+ search_query?: string;
19
+ }): Promise<ApiResponse<{
20
+ decisions: Array<{
21
+ id: string;
22
+ title: string;
23
+ description: string;
24
+ rationale?: string;
25
+ alternatives_considered?: string[];
26
+ created_at: string;
27
+ }>;
28
+ total_count?: number;
29
+ has_more?: boolean;
30
+ }>>;
31
+
32
+ getDecision(decisionId: string): Promise<ApiResponse<{
33
+ decision: {
34
+ id: string;
35
+ title: string;
36
+ description: string;
37
+ rationale?: string;
38
+ alternatives_considered?: string[];
39
+ created_at: string;
40
+ };
41
+ }>>;
42
+
43
+ logDecision(projectId: string, params: {
44
+ title: string;
45
+ description: string;
46
+ rationale?: string;
47
+ alternatives_considered?: string[];
48
+ }, sessionId?: string): Promise<ApiResponse<{
49
+ success: boolean;
50
+ decision_id: string;
51
+ }>>;
52
+
53
+ deleteDecision(decisionId: string): Promise<ApiResponse<{
54
+ success: boolean;
55
+ }>>;
56
+
57
+ getDecisionsStats(projectId: string): Promise<ApiResponse<{
58
+ total: number;
59
+ }>>;
60
+ }
61
+
62
+ export function createDecisionsMethods(proxy: ProxyFn): DecisionsMethods {
63
+ return {
64
+ async getDecisions(projectId, options) {
65
+ return proxy('get_decisions', { project_id: projectId, ...options });
66
+ },
67
+
68
+ async getDecision(decisionId) {
69
+ return proxy('get_decision', { decision_id: decisionId });
70
+ },
71
+
72
+ async logDecision(projectId, params, sessionId) {
73
+ return proxy('log_decision', {
74
+ project_id: projectId,
75
+ ...params
76
+ }, sessionId ? { session_id: sessionId } : undefined);
77
+ },
78
+
79
+ async deleteDecision(decisionId) {
80
+ return proxy('delete_decision', { decision_id: decisionId });
81
+ },
82
+
83
+ async getDecisionsStats(projectId) {
84
+ return proxy('get_decisions_stats', { project_id: projectId });
85
+ }
86
+ };
87
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Discovery API Methods
3
+ *
4
+ * Methods for knowledge and help discovery:
5
+ * - queryKnowledgeBase: Query aggregated project knowledge
6
+ * - getHelpTopic: Get help for a specific topic
7
+ * - getHelpTopics: List all help topics
8
+ */
9
+
10
+ import type { ApiResponse, RequestFn } from './types.js';
11
+
12
+ export interface DiscoveryMethods {
13
+ queryKnowledgeBase(projectId: string, params?: {
14
+ scope?: 'summary' | 'detailed';
15
+ categories?: Array<'findings' | 'qa' | 'decisions' | 'completed_tasks' | 'blockers' | 'progress'>;
16
+ limit?: number;
17
+ search_query?: string;
18
+ }): Promise<ApiResponse<{
19
+ findings?: Array<{
20
+ id: string;
21
+ title: string;
22
+ category: string;
23
+ severity: string;
24
+ }>;
25
+ qa?: Array<{
26
+ id: string;
27
+ question: string;
28
+ answer?: string;
29
+ }>;
30
+ decisions?: Array<{
31
+ id: string;
32
+ title: string;
33
+ description?: string;
34
+ }>;
35
+ completed_tasks?: Array<{
36
+ id: string;
37
+ title: string;
38
+ completed_at: string;
39
+ }>;
40
+ blockers?: Array<{
41
+ id: string;
42
+ description: string;
43
+ status: string;
44
+ }>;
45
+ progress?: Array<{
46
+ id: string;
47
+ note: string;
48
+ created_at: string;
49
+ }>;
50
+ }>>;
51
+
52
+ getHelpTopic(slug: string): Promise<ApiResponse<{
53
+ topic: {
54
+ slug: string;
55
+ title: string;
56
+ content: string;
57
+ };
58
+ }>>;
59
+
60
+ getHelpTopics(): Promise<ApiResponse<Array<{
61
+ slug: string;
62
+ title: string;
63
+ description?: string;
64
+ }>>>;
65
+ }
66
+
67
+ export function createDiscoveryMethods(request: RequestFn): DiscoveryMethods {
68
+ return {
69
+ async queryKnowledgeBase(projectId, params) {
70
+ return request('POST', `/api/mcp/projects/${projectId}/knowledge`, params || {});
71
+ },
72
+
73
+ async getHelpTopic(slug) {
74
+ return request('GET', `/api/mcp/help/${slug}`);
75
+ },
76
+
77
+ async getHelpTopics() {
78
+ return request('GET', '/api/mcp/help');
79
+ }
80
+ };
81
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Ideas API Methods
3
+ *
4
+ * Methods for idea management:
5
+ * - getIdeas: List ideas for a project
6
+ * - getIdea: Get a single idea
7
+ * - addIdea: Create a new idea
8
+ * - updateIdea: Update an idea
9
+ * - deleteIdea: Delete an idea
10
+ * - convertIdeaToTask: Convert an idea to a task
11
+ */
12
+
13
+ import type { ApiResponse, ProxyFn } from './types.js';
14
+
15
+ export interface IdeasMethods {
16
+ getIdeas(projectId: string, params?: {
17
+ status?: string;
18
+ limit?: number;
19
+ offset?: number;
20
+ search_query?: string;
21
+ }): Promise<ApiResponse<{
22
+ ideas: Array<{
23
+ id: string;
24
+ title: string;
25
+ description?: string;
26
+ status: string;
27
+ doc_url?: string;
28
+ created_at: string;
29
+ }>;
30
+ total_count?: number;
31
+ has_more?: boolean;
32
+ }>>;
33
+
34
+ getIdea(ideaId: string): Promise<ApiResponse<{
35
+ idea: {
36
+ id: string;
37
+ title: string;
38
+ description?: string;
39
+ status: string;
40
+ doc_url?: string;
41
+ created_at: string;
42
+ };
43
+ }>>;
44
+
45
+ addIdea(projectId: string, params: {
46
+ title: string;
47
+ description?: string;
48
+ status?: string;
49
+ }, sessionId?: string): Promise<ApiResponse<{
50
+ success: boolean;
51
+ idea_id: string;
52
+ }>>;
53
+
54
+ updateIdea(ideaId: string, updates: {
55
+ title?: string;
56
+ description?: string;
57
+ status?: string;
58
+ doc_url?: string;
59
+ }): Promise<ApiResponse<{
60
+ success: boolean;
61
+ idea_id: string;
62
+ }>>;
63
+
64
+ deleteIdea(ideaId: string): Promise<ApiResponse<{
65
+ success: boolean;
66
+ }>>;
67
+
68
+ convertIdeaToTask(ideaId: string, params?: {
69
+ priority?: number;
70
+ estimated_minutes?: number;
71
+ update_status?: boolean;
72
+ }): Promise<ApiResponse<{
73
+ success: boolean;
74
+ task_id?: string;
75
+ task_title?: string;
76
+ idea_id?: string;
77
+ idea_status?: string;
78
+ message?: string;
79
+ error?: string;
80
+ }>>;
81
+ }
82
+
83
+ export function createIdeasMethods(proxy: ProxyFn): IdeasMethods {
84
+ return {
85
+ async getIdeas(projectId, params) {
86
+ return proxy('get_ideas', { project_id: projectId, ...params });
87
+ },
88
+
89
+ async getIdea(ideaId) {
90
+ return proxy('get_idea', { idea_id: ideaId });
91
+ },
92
+
93
+ async addIdea(projectId, params, sessionId) {
94
+ return proxy('add_idea', {
95
+ project_id: projectId,
96
+ ...params
97
+ }, sessionId ? { session_id: sessionId } : undefined);
98
+ },
99
+
100
+ async updateIdea(ideaId, updates) {
101
+ return proxy('update_idea', { idea_id: ideaId, ...updates });
102
+ },
103
+
104
+ async deleteIdea(ideaId) {
105
+ return proxy('delete_idea', { idea_id: ideaId });
106
+ },
107
+
108
+ async convertIdeaToTask(ideaId, params) {
109
+ return proxy('convert_idea_to_task', { idea_id: ideaId, ...params });
110
+ }
111
+ };
112
+ }
@@ -0,0 +1,378 @@
1
+ /**
2
+ * Vibescope API Client
3
+ *
4
+ * HTTP client for communicating with the Vibescope API.
5
+ * All database operations are handled server-side through these endpoints.
6
+ *
7
+ * This module composes domain-specific API methods into a unified client.
8
+ */
9
+
10
+ import type { ApiClientConfig, ApiResponse, RetryConfig, RequestFn, ProxyFn } from './types.js';
11
+ import { createSessionMethods, type SessionMethods } from './session.js';
12
+ import { createProjectMethods, type ProjectMethods } from './project.js';
13
+ import { createBlockersMethods, type BlockersMethods } from './blockers.js';
14
+ import { createCostMethods, type CostMethods } from './cost.js';
15
+ import { createWorktreesMethods, type WorktreesMethods } from './worktrees.js';
16
+ import { createDiscoveryMethods, type DiscoveryMethods } from './discovery.js';
17
+ import { createDecisionsMethods, type DecisionsMethods } from './decisions.js';
18
+ import { createIdeasMethods, type IdeasMethods } from './ideas.js';
19
+ import { createTasksMethods, type TasksMethods } from './tasks.js';
20
+
21
+ // Re-export types
22
+ export type { ApiClientConfig, ApiResponse, RetryConfig, RequestFn, ProxyFn } from './types.js';
23
+ export type { SessionMethods } from './session.js';
24
+ export type { ProjectMethods } from './project.js';
25
+ export type { BlockersMethods } from './blockers.js';
26
+ export type { CostMethods } from './cost.js';
27
+ export type { WorktreesMethods } from './worktrees.js';
28
+ export type { DiscoveryMethods } from './discovery.js';
29
+ export type { DecisionsMethods } from './decisions.js';
30
+ export type { IdeasMethods } from './ideas.js';
31
+ export type { TasksMethods } from './tasks.js';
32
+
33
+ const DEFAULT_API_URL = 'https://vibescope.dev';
34
+
35
+ // Retry configuration defaults
36
+ const DEFAULT_RETRY_STATUS_CODES = [429, 503, 504];
37
+ const DEFAULT_MAX_RETRIES = 3;
38
+ const DEFAULT_BASE_DELAY_MS = 1000;
39
+ const DEFAULT_MAX_DELAY_MS = 30000;
40
+
41
+ /**
42
+ * Calculate delay for exponential backoff with jitter
43
+ */
44
+ function calculateBackoffDelay(
45
+ attempt: number,
46
+ baseDelayMs: number,
47
+ maxDelayMs: number,
48
+ retryAfter?: number
49
+ ): number {
50
+ if (retryAfter !== undefined && retryAfter > 0) {
51
+ return Math.min(retryAfter * 1000, maxDelayMs);
52
+ }
53
+ const exponentialDelay = baseDelayMs * Math.pow(2, attempt);
54
+ const jitter = Math.random() * 0.3 * exponentialDelay;
55
+ return Math.min(exponentialDelay + jitter, maxDelayMs);
56
+ }
57
+
58
+ /**
59
+ * Sleep for a specified duration
60
+ */
61
+ function sleep(ms: number): Promise<void> {
62
+ return new Promise(resolve => setTimeout(resolve, ms));
63
+ }
64
+
65
+ /**
66
+ * Combined interface for all API methods
67
+ */
68
+ export interface VibescopeApiClientMethods
69
+ extends SessionMethods,
70
+ ProjectMethods,
71
+ BlockersMethods,
72
+ CostMethods,
73
+ WorktreesMethods,
74
+ DiscoveryMethods,
75
+ DecisionsMethods,
76
+ IdeasMethods,
77
+ TasksMethods {}
78
+
79
+ export class VibescopeApiClient implements VibescopeApiClientMethods {
80
+ private apiKey: string;
81
+ private baseUrl: string;
82
+ private retryConfig: Required<RetryConfig>;
83
+
84
+ // Domain method implementations
85
+ private sessionMethods: SessionMethods;
86
+ private projectMethods: ProjectMethods;
87
+ private blockersMethods: BlockersMethods;
88
+ private costMethods: CostMethods;
89
+ private worktreesMethods: WorktreesMethods;
90
+ private discoveryMethods: DiscoveryMethods;
91
+ private decisionsMethods: DecisionsMethods;
92
+ private ideasMethods: IdeasMethods;
93
+ private tasksMethods: TasksMethods;
94
+
95
+ constructor(config: ApiClientConfig) {
96
+ this.apiKey = config.apiKey;
97
+ this.baseUrl = config.baseUrl || process.env.VIBESCOPE_API_URL || DEFAULT_API_URL;
98
+ this.retryConfig = {
99
+ maxRetries: config.retry?.maxRetries ?? DEFAULT_MAX_RETRIES,
100
+ baseDelayMs: config.retry?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS,
101
+ maxDelayMs: config.retry?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS,
102
+ retryStatusCodes: config.retry?.retryStatusCodes ?? DEFAULT_RETRY_STATUS_CODES,
103
+ };
104
+
105
+ // Create bound request and proxy functions for domain modules
106
+ const request: RequestFn = this.request.bind(this);
107
+ const proxy: ProxyFn = this.proxy.bind(this);
108
+ const getApiKey = () => this.apiKey;
109
+
110
+ // Initialize domain methods
111
+ this.sessionMethods = createSessionMethods(request, getApiKey);
112
+ this.projectMethods = createProjectMethods(request);
113
+ this.blockersMethods = createBlockersMethods(request);
114
+ this.costMethods = createCostMethods(request);
115
+ this.worktreesMethods = createWorktreesMethods(request);
116
+ this.discoveryMethods = createDiscoveryMethods(request);
117
+ this.decisionsMethods = createDecisionsMethods(proxy);
118
+ this.ideasMethods = createIdeasMethods(proxy);
119
+ this.tasksMethods = createTasksMethods(request, proxy);
120
+ }
121
+
122
+ private async request<T>(method: string, path: string, body?: unknown): Promise<ApiResponse<T>> {
123
+ const url = `${this.baseUrl}${path}`;
124
+ const { maxRetries, baseDelayMs, maxDelayMs, retryStatusCodes } = this.retryConfig;
125
+ let lastError: Error | null = null;
126
+ let lastResponse: Response | null = null;
127
+
128
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
129
+ try {
130
+ const response = await fetch(url, {
131
+ method,
132
+ headers: {
133
+ 'Content-Type': 'application/json',
134
+ 'X-API-Key': this.apiKey
135
+ },
136
+ body: body ? JSON.stringify(body) : undefined
137
+ });
138
+
139
+ if (retryStatusCodes.includes(response.status) && attempt < maxRetries) {
140
+ lastResponse = response;
141
+ const retryAfterHeader = response.headers.get('Retry-After');
142
+ let retryAfter: number | undefined;
143
+ if (retryAfterHeader) {
144
+ const parsed = parseInt(retryAfterHeader, 10);
145
+ if (!isNaN(parsed)) {
146
+ retryAfter = parsed;
147
+ }
148
+ }
149
+ const delay = calculateBackoffDelay(attempt, baseDelayMs, maxDelayMs, retryAfter);
150
+ await sleep(delay);
151
+ continue;
152
+ }
153
+
154
+ if (!response.ok) {
155
+ const errorData = await response.json().catch(() => ({}));
156
+ return {
157
+ ok: false,
158
+ status: response.status,
159
+ error: errorData.error || errorData.message || `HTTP ${response.status}`
160
+ };
161
+ }
162
+
163
+ const data = await response.json();
164
+ return {
165
+ ok: true,
166
+ status: response.status,
167
+ data
168
+ };
169
+ } catch (error) {
170
+ lastError = error as Error;
171
+ if (attempt < maxRetries) {
172
+ const delay = calculateBackoffDelay(attempt, baseDelayMs, maxDelayMs);
173
+ await sleep(delay);
174
+ continue;
175
+ }
176
+ }
177
+ }
178
+
179
+ if (lastResponse && !lastResponse.ok) {
180
+ return {
181
+ ok: false,
182
+ status: lastResponse.status,
183
+ error: `Request failed after ${maxRetries + 1} attempts with status ${lastResponse.status}`
184
+ };
185
+ }
186
+
187
+ return {
188
+ ok: false,
189
+ status: 0,
190
+ error: lastError?.message || 'Request failed after all retries'
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Generic proxy method for operations that go through the MCP proxy endpoint
196
+ */
197
+ async proxy<T>(operation: string, args: Record<string, unknown>, sessionContext?: {
198
+ session_id?: string;
199
+ project_id?: string;
200
+ }): Promise<ApiResponse<T>> {
201
+ return this.request('POST', '/api/mcp/proxy', {
202
+ operation,
203
+ args,
204
+ ...sessionContext
205
+ });
206
+ }
207
+
208
+ // ============================================================================
209
+ // Session methods (delegated)
210
+ // ============================================================================
211
+ validateAuth = () => this.sessionMethods.validateAuth();
212
+ startSession = (params: Parameters<SessionMethods['startSession']>[0]) => this.sessionMethods.startSession(params);
213
+ heartbeat = (sessionId: string, options?: Parameters<SessionMethods['heartbeat']>[1]) => this.sessionMethods.heartbeat(sessionId, options);
214
+ endSession = (sessionId: string) => this.sessionMethods.endSession(sessionId);
215
+ signalIdle = (sessionId: string) => this.sessionMethods.signalIdle(sessionId);
216
+ syncSession = (sessionId: string, params?: Parameters<SessionMethods['syncSession']>[1]) => this.sessionMethods.syncSession(sessionId, params);
217
+ confirmAgentSetup = (projectId: string, agentType: string) => this.sessionMethods.confirmAgentSetup(projectId, agentType);
218
+
219
+ // ============================================================================
220
+ // Project methods (delegated)
221
+ // ============================================================================
222
+ listProjects = () => this.projectMethods.listProjects();
223
+ createProject = (params: Parameters<ProjectMethods['createProject']>[0]) => this.projectMethods.createProject(params);
224
+ getProject = (projectId: string, gitUrl?: string) => this.projectMethods.getProject(projectId, gitUrl);
225
+ updateProject = (projectId: string, updates: Parameters<ProjectMethods['updateProject']>[1]) => this.projectMethods.updateProject(projectId, updates);
226
+ getGitWorkflow = (projectId: string, taskId?: string) => this.projectMethods.getGitWorkflow(projectId, taskId);
227
+ updateProjectReadme = (projectId: string, readmeContent: string) => this.projectMethods.updateProjectReadme(projectId, readmeContent);
228
+ getProjectSummary = (projectId: string) => this.projectMethods.getProjectSummary(projectId);
229
+
230
+ // ============================================================================
231
+ // Blockers methods (delegated)
232
+ // ============================================================================
233
+ getBlockers = (projectId: string, params?: Parameters<BlockersMethods['getBlockers']>[1]) => this.blockersMethods.getBlockers(projectId, params);
234
+ addBlocker = (projectId: string, description: string, sessionId?: string) => this.blockersMethods.addBlocker(projectId, description, sessionId);
235
+ resolveBlocker = (blockerId: string, resolutionNote?: string) => this.blockersMethods.resolveBlocker(blockerId, resolutionNote);
236
+ deleteBlocker = (blockerId: string) => this.blockersMethods.deleteBlocker(blockerId);
237
+ getBlockersStats = (projectId: string) => this.blockersMethods.getBlockersStats(projectId);
238
+
239
+ // ============================================================================
240
+ // Cost methods (delegated)
241
+ // ============================================================================
242
+ getCostSummary = (projectId: string, params?: Parameters<CostMethods['getCostSummary']>[1]) => this.costMethods.getCostSummary(projectId, params);
243
+ getCostAlerts = () => this.costMethods.getCostAlerts();
244
+ addCostAlert = (params: Parameters<CostMethods['addCostAlert']>[0]) => this.costMethods.addCostAlert(params);
245
+ updateCostAlert = (alertId: string, updates: Parameters<CostMethods['updateCostAlert']>[1]) => this.costMethods.updateCostAlert(alertId, updates);
246
+ deleteCostAlert = (alertId: string) => this.costMethods.deleteCostAlert(alertId);
247
+ getTaskCosts = (projectId: string, limit?: number) => this.costMethods.getTaskCosts(projectId, limit);
248
+ getBodyOfWorkCosts = (params: Parameters<CostMethods['getBodyOfWorkCosts']>[0]) => this.costMethods.getBodyOfWorkCosts(params);
249
+ getSprintCosts = (params: Parameters<CostMethods['getSprintCosts']>[0]) => this.costMethods.getSprintCosts(params);
250
+ getTokenUsage = () => this.costMethods.getTokenUsage();
251
+ reportTokenUsage = (sessionId: string, params: Parameters<CostMethods['reportTokenUsage']>[1]) => this.costMethods.reportTokenUsage(sessionId, params);
252
+
253
+ // ============================================================================
254
+ // Worktrees methods (delegated)
255
+ // ============================================================================
256
+ getStaleWorktrees = (projectId: string, params?: Parameters<WorktreesMethods['getStaleWorktrees']>[1]) => this.worktreesMethods.getStaleWorktrees(projectId, params);
257
+ clearWorktreePath = (taskId: string) => this.worktreesMethods.clearWorktreePath(taskId);
258
+
259
+ // ============================================================================
260
+ // Discovery methods (delegated)
261
+ // ============================================================================
262
+ queryKnowledgeBase = (projectId: string, params?: Parameters<DiscoveryMethods['queryKnowledgeBase']>[1]) => this.discoveryMethods.queryKnowledgeBase(projectId, params);
263
+ getHelpTopic = (slug: string) => this.discoveryMethods.getHelpTopic(slug);
264
+ getHelpTopics = () => this.discoveryMethods.getHelpTopics();
265
+
266
+ // ============================================================================
267
+ // Decisions methods (delegated)
268
+ // ============================================================================
269
+ getDecisions = (projectId: string, options?: Parameters<DecisionsMethods['getDecisions']>[1]) => this.decisionsMethods.getDecisions(projectId, options);
270
+ getDecision = (decisionId: string) => this.decisionsMethods.getDecision(decisionId);
271
+ logDecision = (projectId: string, params: Parameters<DecisionsMethods['logDecision']>[1], sessionId?: string) => this.decisionsMethods.logDecision(projectId, params, sessionId);
272
+ deleteDecision = (decisionId: string) => this.decisionsMethods.deleteDecision(decisionId);
273
+ getDecisionsStats = (projectId: string) => this.decisionsMethods.getDecisionsStats(projectId);
274
+
275
+ // ============================================================================
276
+ // Ideas methods (delegated)
277
+ // ============================================================================
278
+ getIdeas = (projectId: string, params?: Parameters<IdeasMethods['getIdeas']>[1]) => this.ideasMethods.getIdeas(projectId, params);
279
+ getIdea = (ideaId: string) => this.ideasMethods.getIdea(ideaId);
280
+ addIdea = (projectId: string, params: Parameters<IdeasMethods['addIdea']>[1], sessionId?: string) => this.ideasMethods.addIdea(projectId, params, sessionId);
281
+ updateIdea = (ideaId: string, updates: Parameters<IdeasMethods['updateIdea']>[1]) => this.ideasMethods.updateIdea(ideaId, updates);
282
+ deleteIdea = (ideaId: string) => this.ideasMethods.deleteIdea(ideaId);
283
+ convertIdeaToTask = (ideaId: string, params?: Parameters<IdeasMethods['convertIdeaToTask']>[1]) => this.ideasMethods.convertIdeaToTask(ideaId, params);
284
+
285
+ // ============================================================================
286
+ // Tasks methods (delegated)
287
+ // ============================================================================
288
+ getTasks = (projectId: string, params?: Parameters<TasksMethods['getTasks']>[1]) => this.tasksMethods.getTasks(projectId, params);
289
+ createTask = (projectId: string, params: Parameters<TasksMethods['createTask']>[1]) => this.tasksMethods.createTask(projectId, params);
290
+ getNextTask = (projectId: string, sessionId?: string) => this.tasksMethods.getNextTask(projectId, sessionId);
291
+ getTask = (taskId: string) => this.tasksMethods.getTask(taskId);
292
+ getTaskById = (taskId: string, params?: Parameters<TasksMethods['getTaskById']>[1]) => this.tasksMethods.getTaskById(taskId, params);
293
+ searchTasks = (projectId: string, params: Parameters<TasksMethods['searchTasks']>[1]) => this.tasksMethods.searchTasks(projectId, params);
294
+ getTasksByPriority = (projectId: string, params?: Parameters<TasksMethods['getTasksByPriority']>[1]) => this.tasksMethods.getTasksByPriority(projectId, params);
295
+ getRecentTasks = (projectId: string, params?: Parameters<TasksMethods['getRecentTasks']>[1]) => this.tasksMethods.getRecentTasks(projectId, params);
296
+ getTaskStats = (projectId: string) => this.tasksMethods.getTaskStats(projectId);
297
+ updateTask = (taskId: string, updates: Parameters<TasksMethods['updateTask']>[1]) => this.tasksMethods.updateTask(taskId, updates);
298
+ completeTask = (taskId: string, params: Parameters<TasksMethods['completeTask']>[1]) => this.tasksMethods.completeTask(taskId, params);
299
+ deleteTask = (taskId: string) => this.tasksMethods.deleteTask(taskId);
300
+ releaseTask = (taskId: string, params?: Parameters<TasksMethods['releaseTask']>[1]) => this.tasksMethods.releaseTask(taskId, params);
301
+ cancelTask = (taskId: string, params?: Parameters<TasksMethods['cancelTask']>[1]) => this.tasksMethods.cancelTask(taskId, params);
302
+ addTaskReference = (taskId: string, url: string, label?: string) => this.tasksMethods.addTaskReference(taskId, url, label);
303
+ removeTaskReference = (taskId: string, url: string) => this.tasksMethods.removeTaskReference(taskId, url);
304
+ batchUpdateTasks = (updates: Parameters<TasksMethods['batchUpdateTasks']>[0]) => this.tasksMethods.batchUpdateTasks(updates);
305
+ batchCompleteTasks = (completions: Parameters<TasksMethods['batchCompleteTasks']>[0]) => this.tasksMethods.batchCompleteTasks(completions);
306
+
307
+ // ============================================================================
308
+ // TODO: Additional methods to be migrated from original api-client.ts
309
+ // The following domains need to be split into separate files:
310
+ // - findings.ts (getFindings, addFinding, etc.)
311
+ // - milestones.ts (getMilestones, addMilestone, etc.)
312
+ // - requests.ts (getPendingRequests, answerQuestion, etc.)
313
+ // - validation.ts (getTasksAwaitingValidation, validateTask, etc.)
314
+ // - fallback.ts (startFallbackActivity, stopFallbackActivity)
315
+ // - deployment.ts (requestDeployment, startDeployment, etc.)
316
+ // - organizations.ts (listOrganizations, etc.)
317
+ // - bodies-of-work.ts (createBodyOfWork, etc.)
318
+ // - git-issues.ts (addGitIssue, etc.)
319
+ // - file-checkouts.ts (checkoutFile, checkinFile, etc.)
320
+ // - connectors.ts (getConnectors, addConnector, etc.)
321
+ // - sprints.ts (getSprints, createSprint, etc.)
322
+ // - subtasks.ts (addSubtask, getSubtasks)
323
+ // - progress.ts (logProgress, getActivityHistory, etc.)
324
+ // ============================================================================
325
+ }
326
+
327
+ // Singleton instance
328
+ let apiClient: VibescopeApiClient | null = null;
329
+
330
+ export function getApiClient(): VibescopeApiClient {
331
+ if (!apiClient) {
332
+ const apiKey = resolveApiKeyFromSources();
333
+ apiClient = new VibescopeApiClient({ apiKey });
334
+ }
335
+ return apiClient;
336
+ }
337
+
338
+ /**
339
+ * Resolve API key from multiple sources in priority order:
340
+ * 1. VIBESCOPE_API_KEY env var (CI/cloud agents)
341
+ * 2. ~/.vibescope/credentials.json (local dev)
342
+ * 3. Error with helpful message
343
+ */
344
+ function resolveApiKeyFromSources(): string {
345
+ // 1. Environment variable
346
+ if (process.env.VIBESCOPE_API_KEY) {
347
+ return process.env.VIBESCOPE_API_KEY;
348
+ }
349
+
350
+ // 2. Credentials file
351
+ try {
352
+ const { existsSync, readFileSync } = require('node:fs');
353
+ const { homedir } = require('node:os');
354
+ const { join } = require('node:path');
355
+ const credPath = join(homedir(), '.vibescope', 'credentials.json');
356
+ if (existsSync(credPath)) {
357
+ const data = JSON.parse(readFileSync(credPath, 'utf-8'));
358
+ if (data.apiKey) {
359
+ return data.apiKey;
360
+ }
361
+ }
362
+ } catch {
363
+ // ignore read errors
364
+ }
365
+
366
+ // 3. Not found
367
+ throw new Error(
368
+ 'Vibescope API key not found.\n\n' +
369
+ 'Set it up with:\n' +
370
+ ' npx vibescope init\n\n' +
371
+ 'Or set the VIBESCOPE_API_KEY environment variable.'
372
+ );
373
+ }
374
+
375
+ export function initApiClient(config: ApiClientConfig): VibescopeApiClient {
376
+ apiClient = new VibescopeApiClient(config);
377
+ return apiClient;
378
+ }