vibe-weaver 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Code Block Parser for Vibe-Weaver CLI
3
+ * Parses markdown code blocks with language and file path
4
+ * Format: ```lang path\n...```
5
+ */
6
+
7
+ export interface ParsedCodeBlock {
8
+ language: string;
9
+ path: string;
10
+ content: string;
11
+ startLine: number;
12
+ endLine: number;
13
+ }
14
+
15
+ export interface ParseResult {
16
+ blocks: ParsedCodeBlock[];
17
+ explanation?: string;
18
+ quality?: {
19
+ score: number;
20
+ issues: string[];
21
+ suggestions: string[];
22
+ };
23
+ stats?: {
24
+ mode: string;
25
+ parameters: string;
26
+ architecture: string;
27
+ inferenceTimeMs: number;
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Parse code blocks from the AI response
33
+ * Supports format: ```typescript src/App.tsx\n...content...```
34
+ */
35
+ export function parseCodeBlocks(response: string): ParseResult {
36
+ const blocks: ParsedCodeBlock[] = [];
37
+
38
+ // Regex to match code blocks with optional language and path
39
+ // Format: ```[lang[ path]]\n...content...\n```
40
+ const codeBlockRegex = /```(\w+)?\s+(.+?)\n([\s\S]*?)```/g;
41
+
42
+ let match;
43
+ while ((match = codeBlockRegex.exec(response)) !== null) {
44
+ const language = match[1] || 'text';
45
+ const path = match[2].trim();
46
+ const content = match[3];
47
+
48
+ // Calculate line numbers
49
+ const beforeBlock = response.substring(0, match.index);
50
+ const startLine = (beforeBlock.match(/\n/g) || []).length + 1;
51
+ const endLine = startLine + (content.match(/\n/g) || []).length;
52
+
53
+ blocks.push({
54
+ language,
55
+ path,
56
+ content,
57
+ startLine,
58
+ endLine
59
+ });
60
+ }
61
+
62
+ // Try to extract explanation (text before first code block)
63
+ let explanation: string | undefined;
64
+ const firstCodeBlockIndex = response.indexOf('```');
65
+ if (firstCodeBlockIndex > 0) {
66
+ explanation = response.substring(0, firstCodeBlockIndex).trim();
67
+ }
68
+
69
+ return { blocks, explanation };
70
+ }
71
+
72
+ /**
73
+ * Parse a single code block string
74
+ */
75
+ export function parseSingleCodeBlock(blockStr: string): { language: string; path: string; content: string } | null {
76
+ const match = blockStr.match(/^(\w+)?\s+(.+?)\n([\s\S]*)$/);
77
+ if (!match) return null;
78
+
79
+ return {
80
+ language: match[1] || 'text',
81
+ path: match[2].trim(),
82
+ content: match[3]
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Write parsed code blocks to disk
88
+ */
89
+ export async function writeCodeBlocks(
90
+ blocks: ParsedCodeBlock[],
91
+ baseDir: string,
92
+ options: { dryRun?: boolean } = {}
93
+ ): Promise<{ success: string[]; failed: { path: string; error: string }[] }> {
94
+ const fs = await import('fs/promises');
95
+ const path = await import('path');
96
+
97
+ const success: string[] = [];
98
+ const failed: { path: string; error: string }[] = [];
99
+
100
+ for (const block of blocks) {
101
+ try {
102
+ const fullPath = path.join(baseDir, block.path);
103
+ const dir = path.dirname(fullPath);
104
+
105
+ if (!options.dryRun) {
106
+ // Create directory if it doesn't exist
107
+ await fs.mkdir(dir, { recursive: true });
108
+
109
+ // Write file
110
+ await fs.writeFile(fullPath, block.content, 'utf-8');
111
+ }
112
+
113
+ success.push(block.path);
114
+ } catch (error) {
115
+ failed.push({
116
+ path: block.path,
117
+ error: error instanceof Error ? error.message : String(error)
118
+ });
119
+ }
120
+ }
121
+
122
+ return { success, failed };
123
+ }
124
+
125
+ /**
126
+ * Generate a diff between old and new content
127
+ */
128
+ export function generateDiff(oldContent: string, newContent: string, filePath: string): string {
129
+ const diff = require('diff');
130
+ const changes = diff.diffLines(oldContent, newContent);
131
+
132
+ let result = `--- ${filePath}\n+++ ${filePath}\n`;
133
+
134
+ for (const change of changes) {
135
+ const prefix = change.added ? '+' : change.removed ? '-' : ' ';
136
+ const lines = change.value.split('\n').filter((l: string) => l.length > 0);
137
+ for (const line of lines) {
138
+ result += `${prefix}${line}\n`;
139
+ }
140
+ }
141
+
142
+ return result;
143
+ }
144
+
145
+ export default {
146
+ parseCodeBlocks,
147
+ parseSingleCodeBlock,
148
+ writeCodeBlocks,
149
+ generateDiff
150
+ };
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Vibe-Weaver API Client
3
+ * Communicates with the vibe-ai-api edge function
4
+ */
5
+
6
+ import { getApiKey, getGithubToken } from '../auth/index.js';
7
+ import { getSupabaseUrl } from '../config/index.js';
8
+ import { parseCodeBlocks, ParseResult } from './code-parser.js';
9
+
10
+ export type ModelId = 'fast' | 'balanced' | 'deep_reasoning' | 'refactor';
11
+ export type Platform = 'web' | 'roblox' | 'mobile' | 'desktop' | 'api';
12
+ export type Language = 'typescript' | 'javascript' | 'python' | 'lua' | 'rust';
13
+
14
+ export interface GenerateOptions {
15
+ goal: string;
16
+ platform?: Platform;
17
+ language?: Language;
18
+ modelId?: ModelId;
19
+ context?: string;
20
+ stream?: boolean;
21
+ }
22
+
23
+ export interface GenerateResult {
24
+ code: string;
25
+ explanation: string;
26
+ quality: {
27
+ score: number;
28
+ issues: string[];
29
+ suggestions: string[];
30
+ };
31
+ stats: {
32
+ mode: string;
33
+ parameters: string;
34
+ architecture: string;
35
+ inferenceTimeMs: number;
36
+ };
37
+ parsed: ParseResult;
38
+ }
39
+
40
+ export interface CreditInfo {
41
+ cost: number;
42
+ remaining: number;
43
+ plan: string;
44
+ }
45
+
46
+ /**
47
+ * Get credit cost for a given model
48
+ */
49
+ export function getCreditCost(modelId: ModelId): number {
50
+ const costs: Record<ModelId, number> = {
51
+ 'fast': 0.2,
52
+ 'balanced': 0.5,
53
+ 'deep_reasoning': 1.0,
54
+ 'refactor': 1.0
55
+ };
56
+ return costs[modelId] || 0.5;
57
+ }
58
+
59
+ /**
60
+ * Call the vibe-ai-api to generate code
61
+ */
62
+ export async function generateCode(options: GenerateOptions): Promise<GenerateResult> {
63
+ const apiKey = await getApiKey();
64
+ if (!apiKey) {
65
+ throw new Error('Not logged in. Run "vibe-weaver login" first.');
66
+ }
67
+
68
+ const supabaseUrl = getSupabaseUrl();
69
+ const endpoint = `${supabaseUrl}/functions/v1/vibe-ai-api`;
70
+
71
+ const payload = {
72
+ goal: options.goal,
73
+ platform: options.platform || 'web',
74
+ language: options.language || 'typescript',
75
+ modelId: options.modelId || 'balanced'
76
+ };
77
+
78
+ console.log(`\n${'[Vibe-Weaver]'.padEnd(20)} Calling ${payload.modelId.toUpperCase()} model for: ${options.goal.substring(0, 50)}...`);
79
+
80
+ const response = await fetch(endpoint, {
81
+ method: 'POST',
82
+ headers: {
83
+ 'Content-Type': 'application/json',
84
+ 'Authorization': `Bearer ${apiKey}`
85
+ },
86
+ body: JSON.stringify(payload)
87
+ });
88
+
89
+ if (!response.ok) {
90
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
91
+
92
+ if (response.status === 401) {
93
+ throw new Error('Authentication failed. Please run "vibe-weaver login" again.');
94
+ }
95
+ if (response.status === 402) {
96
+ throw new Error(`Insufficient credits: ${error.message}`);
97
+ }
98
+
99
+ throw new Error(`API error: ${error.error || error.message}`);
100
+ }
101
+
102
+ const result = await response.json();
103
+
104
+ // Parse code blocks from the response
105
+ const parsed = parseCodeBlocks(result.code);
106
+
107
+ return {
108
+ code: result.code,
109
+ explanation: result.explanation || '',
110
+ quality: result.quality || { score: 0, issues: [], suggestions: [] },
111
+ stats: result.stats || { mode: payload.modelId, parameters: 'N/A', architecture: 'N/A', inferenceTimeMs: 0 },
112
+ parsed
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Stream code generation (for future implementation)
118
+ */
119
+ export async function* streamGenerate(options: GenerateOptions): AsyncGenerator<{ delta: string; done: boolean }> {
120
+ const apiKey = await getApiKey();
121
+ if (!apiKey) {
122
+ throw new Error('Not logged in. Run "vibe-weaver login" first.');
123
+ }
124
+
125
+ const supabaseUrl = getSupabaseUrl();
126
+ const endpoint = `${supabaseUrl}/functions/v1/vibe-ai-api`;
127
+
128
+ const payload = {
129
+ goal: options.goal,
130
+ platform: options.platform || 'web',
131
+ language: options.language || 'typescript',
132
+ modelId: options.modelId || 'balanced'
133
+ };
134
+
135
+ const response = await fetch(endpoint, {
136
+ method: 'POST',
137
+ headers: {
138
+ 'Content-Type': 'application/json',
139
+ 'Authorization': `Bearer ${apiKey}`
140
+ },
141
+ body: JSON.stringify(payload)
142
+ });
143
+
144
+ if (!response.ok) {
145
+ throw new Error(`API error: ${response.status}`);
146
+ }
147
+
148
+ // For now, yield the full result
149
+ const result = await response.json();
150
+ yield { delta: result.code, done: true };
151
+ }
152
+
153
+ /**
154
+ * Get remaining credits for the user
155
+ */
156
+ export async function getCredits(): Promise<CreditInfo> {
157
+ const apiKey = await getApiKey();
158
+ if (!apiKey) {
159
+ throw new Error('Not logged in. Run "vibe-weaver login" first.');
160
+ }
161
+
162
+ const supabaseUrl = getSupabaseUrl();
163
+
164
+ try {
165
+ // Try to get profile info
166
+ const response = await fetch(`${supabaseUrl}/rest/v1/profiles?select=credits,plan`, {
167
+ headers: {
168
+ 'Authorization': `Bearer ${apiKey}`,
169
+ 'apikey': `${supabaseUrl}/rest/v1/apikey`
170
+ }
171
+ });
172
+
173
+ if (response.ok) {
174
+ const data = await response.json();
175
+ if (Array.isArray(data) && data.length > 0) {
176
+ return {
177
+ cost: 0,
178
+ remaining: data[0].credits || 0,
179
+ plan: data[0].plan || 'free'
180
+ };
181
+ }
182
+ }
183
+ } catch {
184
+ // Ignore errors
185
+ }
186
+
187
+ return { cost: 0, remaining: 0, plan: 'unknown' };
188
+ }
189
+
190
+ export default {
191
+ generateCode,
192
+ streamGenerate,
193
+ getCredits,
194
+ getCreditCost
195
+ };
196
+
197
+ export type { ModelId, Platform, Language };
@@ -0,0 +1,273 @@
1
+ /**
2
+ * MCP Client for Vibe-Weaver CLI
3
+ * Connects to MCP servers via JSON-RPC
4
+ */
5
+
6
+ import chalk from 'chalk';
7
+ import { getSupabaseUrl } from '../config/index.js';
8
+
9
+ export interface MCPServer {
10
+ name: string;
11
+ url: string;
12
+ enabled: boolean;
13
+ }
14
+
15
+ export interface MCPTool {
16
+ name: string;
17
+ description: string;
18
+ inputSchema: Record<string, any>;
19
+ }
20
+
21
+ export interface MCPInitializeResult {
22
+ protocolVersion: string;
23
+ serverInfo: {
24
+ name: string;
25
+ version: string;
26
+ };
27
+ capabilities: Record<string, any>;
28
+ }
29
+
30
+ export interface MCPToolsResult {
31
+ tools: MCPTool[];
32
+ }
33
+
34
+ /**
35
+ * Default MCP servers provided by Vibe-Weaver
36
+ */
37
+ export function getDefaultMCPServers(): MCPServer[] {
38
+ const supabaseUrl = getSupabaseUrl();
39
+ return [
40
+ { name: 'github-mcp', url: `${supabaseUrl}/functions/v1/github-mcp`, enabled: true },
41
+ { name: 'filesystem-mcp', url: `${supabaseUrl}/functions/v1/filesystem-mcp`, enabled: true },
42
+ { name: 'supabase-mcp', url: `${supabaseUrl}/functions/v1/supabase-mcp`, enabled: true },
43
+ { name: 'roblox-mcp', url: `${supabaseUrl}/functions/v1/roblox-mcp`, enabled: true },
44
+ { name: 'linear-mcp', url: `${supabaseUrl}/functions/v1/linear-mcp`, enabled: false },
45
+ { name: 'notion-mcp', url: `${supabaseUrl}/functions/v1/notion-mcp`, enabled: false },
46
+ { name: 'slack-mcp', url: `${supabaseUrl}/functions/v1/slack-mcp`, enabled: false },
47
+ { name: 'discord-mcp', url: `${supabaseUrl}/functions/v1/discord-mcp`, enabled: false },
48
+ { name: 'vercel-mcp', url: `${supabaseUrl}/functions/v1/vercel-mcp`, enabled: false },
49
+ { name: 'stripe-mcp', url: `${supabaseUrl}/functions/v1/stripe-mcp`, enabled: false },
50
+ ];
51
+ }
52
+
53
+ /**
54
+ * MCP JSON-RPC client
55
+ */
56
+ export class MCPClient {
57
+ private url: string;
58
+ private initialized: boolean = false;
59
+ private serverInfo: MCPInitializeResult['serverInfo'] | null = null;
60
+ private capabilities: Record<string, any> = {};
61
+ private requestId: number = 0;
62
+
63
+ constructor(url: string) {
64
+ this.url = url;
65
+ }
66
+
67
+ /**
68
+ * Send a JSON-RPC request
69
+ */
70
+ private async request(method: string, params?: Record<string, any>): Promise<any> {
71
+ const id = ++this.requestId;
72
+
73
+ const response = await fetch(this.url, {
74
+ method: 'POST',
75
+ headers: {
76
+ 'Content-Type': 'application/json',
77
+ },
78
+ body: JSON.stringify({
79
+ jsonrpc: '2.0',
80
+ id,
81
+ method,
82
+ params: params || {}
83
+ })
84
+ });
85
+
86
+ if (!response.ok) {
87
+ throw new Error(`MCP request failed: ${response.status} ${response.statusText}`);
88
+ }
89
+
90
+ const result = await response.json();
91
+
92
+ if (result.error) {
93
+ throw new Error(`MCP error: ${result.error.message}`);
94
+ }
95
+
96
+ return result.result;
97
+ }
98
+
99
+ /**
100
+ * Initialize the MCP connection
101
+ */
102
+ async initialize(): Promise<MCPInitializeResult> {
103
+ try {
104
+ const result = await this.request('initialize', {
105
+ protocolVersion: '2024-11-05',
106
+ capabilities: {},
107
+ clientInfo: {
108
+ name: 'vibe-weaver-cli',
109
+ version: '1.0.0'
110
+ }
111
+ });
112
+
113
+ this.initialized = true;
114
+ this.serverInfo = result.serverInfo;
115
+ this.capabilities = result.capabilities;
116
+
117
+ return result;
118
+ } catch (error) {
119
+ throw new Error(`Failed to initialize MCP server: ${error instanceof Error ? error.message : error}`);
120
+ }
121
+ }
122
+
123
+ /**
124
+ * List available tools
125
+ */
126
+ async listTools(): Promise<MCPToolsResult> {
127
+ if (!this.initialized) {
128
+ await this.initialize();
129
+ }
130
+
131
+ return this.request('tools/list');
132
+ }
133
+
134
+ /**
135
+ * Call a tool
136
+ */
137
+ async callTool(name: string, args: Record<string, any>): Promise<any> {
138
+ if (!this.initialized) {
139
+ await this.initialize();
140
+ }
141
+
142
+ return this.request('tools/call', {
143
+ name,
144
+ arguments: args
145
+ });
146
+ }
147
+
148
+ /**
149
+ * Check if server is alive
150
+ */
151
+ async ping(): Promise<boolean> {
152
+ try {
153
+ await this.request('ping');
154
+ return true;
155
+ } catch {
156
+ return false;
157
+ }
158
+ }
159
+
160
+ getServerInfo(): MCPInitializeResult['serverInfo'] | null {
161
+ return this.serverInfo;
162
+ }
163
+
164
+ isInitialized(): boolean {
165
+ return this.initialized;
166
+ }
167
+ }
168
+
169
+ /**
170
+ * MCP Manager - handles multiple MCP servers
171
+ */
172
+ export class MCPManager {
173
+ private clients: Map<string, MCPClient> = new Map();
174
+ private servers: MCPServer[] = [];
175
+
176
+ constructor(servers?: MCPServer[]) {
177
+ this.servers = servers || getDefaultMCPServers();
178
+ }
179
+
180
+ /**
181
+ * Initialize all enabled MCP servers
182
+ */
183
+ async initializeAll(): Promise<{ success: string[]; failed: { name: string; error: string }[] }> {
184
+ const success: string[] = [];
185
+ const failed: { name: string; error: string }[] = [];
186
+
187
+ for (const server of this.servers) {
188
+ if (!server.enabled) continue;
189
+
190
+ const client = new MCPClient(server.url);
191
+
192
+ try {
193
+ const result = await client.initialize();
194
+ this.clients.set(server.name, client);
195
+ success.push(`${server.name} (${result.serverInfo.version})`);
196
+ console.log(chalk.green(`✓ Connected to ${server.name}`));
197
+ } catch (error) {
198
+ failed.push({
199
+ name: server.name,
200
+ error: error instanceof Error ? error.message : String(error)
201
+ });
202
+ console.log(chalk.red(`✗ Failed to connect to ${server.name}`));
203
+ }
204
+ }
205
+
206
+ return { success, failed };
207
+ }
208
+
209
+ /**
210
+ * List all available tools from connected servers
211
+ */
212
+ async listAllTools(): Promise<{ server: string; tools: MCPTool[] }[]> {
213
+ const results: { server: string; tools: MCPTool[] }[] = [];
214
+
215
+ for (const [name, client] of this.clients) {
216
+ try {
217
+ const result = await client.listTools();
218
+ results.push({ server: name, tools: result.tools });
219
+ } catch (error) {
220
+ console.warn(chalk.yellow(`Warning: Failed to list tools from ${name}: ${error}`));
221
+ }
222
+ }
223
+
224
+ return results;
225
+ }
226
+
227
+ /**
228
+ * Call a tool on a specific server
229
+ */
230
+ async callTool(serverName: string, toolName: string, args: Record<string, any>): Promise<any> {
231
+ const client = this.clients.get(serverName);
232
+
233
+ if (!client) {
234
+ throw new Error(`MCP server "${serverName}" not connected`);
235
+ }
236
+
237
+ return client.callTool(toolName, args);
238
+ }
239
+
240
+ /**
241
+ * Get status of all servers
242
+ */
243
+ getStatus(): { name: string; connected: boolean; tools: number }[] {
244
+ const status: { name: string; connected: boolean; tools: number }[] = [];
245
+
246
+ for (const server of this.servers) {
247
+ const client = this.clients.get(server.name);
248
+ status.push({
249
+ name: server.name,
250
+ connected: !!client && client.isInitialized(),
251
+ tools: 0 // Would need to cache tool list
252
+ });
253
+ }
254
+
255
+ return status;
256
+ }
257
+
258
+ /**
259
+ * Enable/disable a server
260
+ */
261
+ setServerEnabled(name: string, enabled: boolean): void {
262
+ const server = this.servers.find(s => s.name === name);
263
+ if (server) {
264
+ server.enabled = enabled;
265
+ }
266
+ }
267
+ }
268
+
269
+ export default {
270
+ MCPClient,
271
+ MCPManager,
272
+ getDefaultMCPServers
273
+ };