kimi-vercel-ai-sdk-provider 0.4.0 → 0.5.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,494 @@
1
+ /**
2
+ * Project scaffolder implementation.
3
+ * @module
4
+ */
5
+
6
+ import type { OutputFormat, ProjectFile, ProjectMetadata, ProjectType, ScaffoldConfig, ScaffoldResult } from './types';
7
+
8
+ // ============================================================================
9
+ // Types
10
+ // ============================================================================
11
+
12
+ /**
13
+ * Function type for generating text.
14
+ */
15
+ export type GenerateTextFunction = (prompt: string) => Promise<{ text: string }>;
16
+
17
+ /**
18
+ * Options for creating a scaffolder.
19
+ */
20
+ export interface ScaffolderOptions {
21
+ /**
22
+ * Function to generate text from the model.
23
+ */
24
+ generateText: GenerateTextFunction;
25
+
26
+ /**
27
+ * Default model ID to use.
28
+ */
29
+ modelId?: string;
30
+ }
31
+
32
+ // ============================================================================
33
+ // ProjectScaffolder Class
34
+ // ============================================================================
35
+
36
+ /**
37
+ * Scaffolder for generating project structures.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * const scaffolder = new ProjectScaffolder({
42
+ * generateText: async (prompt) => {
43
+ * const result = await generateText({ model, prompt });
44
+ * return { text: result.text };
45
+ * },
46
+ * });
47
+ *
48
+ * const project = await scaffolder.scaffold(
49
+ * 'A REST API for todo management',
50
+ * { type: 'express', includeTests: true }
51
+ * );
52
+ * ```
53
+ */
54
+ export class ProjectScaffolder {
55
+ private generateText: GenerateTextFunction;
56
+
57
+ constructor(options: ScaffolderOptions) {
58
+ this.generateText = options.generateText;
59
+ }
60
+
61
+ /**
62
+ * Scaffold a new project based on a description.
63
+ *
64
+ * @param description - Description of the project to create
65
+ * @param config - Scaffold configuration
66
+ * @returns Scaffold result with files and instructions
67
+ */
68
+ async scaffold(description: string, config: ScaffoldConfig = {}): Promise<ScaffoldResult> {
69
+ const {
70
+ type = 'auto',
71
+ includeTests = true,
72
+ includeCI = false,
73
+ includeDocs = true,
74
+ includeDocker = false,
75
+ includeLinting = true,
76
+ outputFormat = 'files',
77
+ useTypeScript = true,
78
+ features = [],
79
+ customTemplate
80
+ } = config;
81
+
82
+ // Build the prompt for project generation
83
+ const prompt = this.buildScaffoldPrompt(description, {
84
+ type,
85
+ includeTests,
86
+ includeCI,
87
+ includeDocs,
88
+ includeDocker,
89
+ includeLinting,
90
+ useTypeScript,
91
+ features,
92
+ customTemplate
93
+ });
94
+
95
+ // Generate the project structure
96
+ const result = await this.generateText(prompt);
97
+
98
+ // Parse the response
99
+ const parsed = this.parseResponse(result.text, type);
100
+
101
+ // Format output based on config
102
+ return this.formatResult(parsed, outputFormat, result.text);
103
+ }
104
+
105
+ /**
106
+ * Build the scaffold prompt.
107
+ */
108
+ private buildScaffoldPrompt(
109
+ description: string,
110
+ config: {
111
+ type: ProjectType;
112
+ includeTests: boolean;
113
+ includeCI: boolean;
114
+ includeDocs: boolean;
115
+ includeDocker: boolean;
116
+ includeLinting: boolean;
117
+ useTypeScript: boolean;
118
+ features: string[];
119
+ customTemplate?: string;
120
+ }
121
+ ): string {
122
+ const parts: string[] = [];
123
+
124
+ parts.push('Generate a complete project structure for the following description:');
125
+ parts.push(`"${description}"`);
126
+ parts.push('');
127
+
128
+ if (config.type !== 'auto') {
129
+ parts.push(`Framework/Type: ${config.type}`);
130
+ }
131
+
132
+ parts.push(`Language: ${config.useTypeScript ? 'TypeScript' : 'JavaScript/Python/Go (as appropriate)'}`);
133
+
134
+ const includes: string[] = [];
135
+ if (config.includeTests) {
136
+ includes.push('comprehensive test files');
137
+ }
138
+ if (config.includeCI) {
139
+ includes.push('GitHub Actions CI/CD workflow');
140
+ }
141
+ if (config.includeDocs) {
142
+ includes.push('README with setup instructions');
143
+ }
144
+ if (config.includeDocker) {
145
+ includes.push('Dockerfile and docker-compose.yml');
146
+ }
147
+ if (config.includeLinting) {
148
+ includes.push('ESLint/linting configuration');
149
+ }
150
+
151
+ if (includes.length > 0) {
152
+ parts.push(`Include: ${includes.join(', ')}`);
153
+ }
154
+
155
+ if (config.features.length > 0) {
156
+ parts.push(`Additional features: ${config.features.join(', ')}`);
157
+ }
158
+
159
+ if (config.customTemplate) {
160
+ parts.push('');
161
+ parts.push('Custom requirements:');
162
+ parts.push(config.customTemplate);
163
+ }
164
+
165
+ parts.push('');
166
+ parts.push('Respond with a JSON object in this exact format:');
167
+ parts.push('```json');
168
+ parts.push('{');
169
+ parts.push(' "projectName": "project-name",');
170
+ parts.push(' "projectType": "detected-type",');
171
+ parts.push(' "technologies": ["tech1", "tech2"],');
172
+ parts.push(' "files": [');
173
+ parts.push(' {');
174
+ parts.push(' "path": "relative/path/to/file.ts",');
175
+ parts.push(' "content": "full file content here",');
176
+ parts.push(' "description": "what this file does"');
177
+ parts.push(' }');
178
+ parts.push(' ],');
179
+ parts.push(' "setupCommands": ["npm install", "npm run dev"],');
180
+ parts.push(' "estimatedSetupTime": "5 minutes"');
181
+ parts.push('}');
182
+ parts.push('```');
183
+ parts.push('');
184
+ parts.push(
185
+ 'Include ALL necessary files for a working project: package.json/requirements.txt, source files, config files, etc.'
186
+ );
187
+ parts.push('Make sure file contents are complete and functional, not placeholders.');
188
+
189
+ return parts.join('\n');
190
+ }
191
+
192
+ /**
193
+ * Parse the model response into structured data.
194
+ */
195
+ private parseResponse(
196
+ text: string,
197
+ defaultType: ProjectType
198
+ ): {
199
+ files: ProjectFile[];
200
+ metadata: ProjectMetadata;
201
+ setupCommands: string[];
202
+ } {
203
+ // Try to extract JSON from the response
204
+ const jsonMatch = text.match(/```json\n?([\s\S]*?)```/) || text.match(/\{[\s\S]*"files"[\s\S]*\}/);
205
+
206
+ if (jsonMatch) {
207
+ try {
208
+ const json = JSON.parse(jsonMatch[1] || jsonMatch[0]);
209
+ const files: ProjectFile[] = (json.files || []).map((f: ProjectFile) => {
210
+ return {
211
+ path: f.path,
212
+ content: f.content,
213
+ description: f.description
214
+ };
215
+ });
216
+
217
+ // If projectType is explicitly provided in JSON, use it
218
+ // If defaultType is 'auto', try to detect from files
219
+ // Otherwise use the defaultType
220
+ let projectType: ProjectType;
221
+ if (json.projectType) {
222
+ projectType = json.projectType as ProjectType;
223
+ } else if (defaultType === 'auto') {
224
+ projectType = this.detectProjectType(files);
225
+ } else {
226
+ projectType = defaultType;
227
+ }
228
+
229
+ return {
230
+ files,
231
+ metadata: {
232
+ projectType,
233
+ projectName: json.projectName || 'my-project',
234
+ fileCount: files.length,
235
+ totalSize: files.reduce((sum: number, f: ProjectFile) => sum + (f.content?.length || 0), 0),
236
+ estimatedSetupTime: json.estimatedSetupTime || 'unknown',
237
+ technologies: json.technologies || [],
238
+ features: []
239
+ },
240
+ setupCommands: json.setupCommands || []
241
+ };
242
+ } catch {
243
+ // JSON parsing failed, try fallback
244
+ }
245
+ }
246
+
247
+ // Fallback: extract files from code blocks
248
+ return this.parseFromCodeBlocks(text, defaultType);
249
+ }
250
+
251
+ /**
252
+ * Parse files from markdown code blocks.
253
+ */
254
+ private parseFromCodeBlocks(
255
+ text: string,
256
+ defaultType: ProjectType
257
+ ): {
258
+ files: ProjectFile[];
259
+ metadata: ProjectMetadata;
260
+ setupCommands: string[];
261
+ } {
262
+ const files: ProjectFile[] = [];
263
+
264
+ // Match code blocks with file paths in the language annotation
265
+ // e.g., ```typescript:src/index.ts or ```path/to/file.ts
266
+ const fileBlockRegex = /```(?:(\w+):)?([\w./-]+)\n([\s\S]*?)```/g;
267
+ let match: RegExpExecArray | null = fileBlockRegex.exec(text);
268
+
269
+ while (match !== null) {
270
+ const path = match[2];
271
+ const content = match[3].trim();
272
+
273
+ // Skip if path doesn't look like a file
274
+ if (path.includes('.')) {
275
+ files.push({
276
+ path,
277
+ content,
278
+ description: undefined
279
+ });
280
+ }
281
+ match = fileBlockRegex.exec(text);
282
+ }
283
+
284
+ // If no files found with paths, try generic code blocks
285
+ if (files.length === 0) {
286
+ const genericBlockRegex = /```(\w*)\n([\s\S]*?)```/g;
287
+ let blockMatch: RegExpExecArray | null = genericBlockRegex.exec(text);
288
+ let index = 0;
289
+
290
+ while (blockMatch !== null) {
291
+ const lang = blockMatch[1] || 'txt';
292
+ const content = blockMatch[2].trim();
293
+
294
+ if (content.length > 10) {
295
+ // Skip trivial blocks
296
+ const ext = this.getExtensionForLanguage(lang);
297
+ files.push({
298
+ path: `file${index}.${ext}`,
299
+ content
300
+ });
301
+ index++;
302
+ }
303
+ blockMatch = genericBlockRegex.exec(text);
304
+ }
305
+ }
306
+
307
+ return {
308
+ files,
309
+ metadata: {
310
+ projectType: defaultType === 'auto' ? this.detectProjectType(files) : defaultType,
311
+ projectName: 'my-project',
312
+ fileCount: files.length,
313
+ totalSize: files.reduce((sum, f) => sum + f.content.length, 0),
314
+ estimatedSetupTime: 'unknown',
315
+ technologies: [],
316
+ features: []
317
+ },
318
+ setupCommands: []
319
+ };
320
+ }
321
+
322
+ /**
323
+ * Detect project type from files.
324
+ */
325
+ private detectProjectType(files: ProjectFile[]): ProjectType {
326
+ const paths = files.map((f) => f.path.toLowerCase());
327
+ const contents = files.map((f) => f.content);
328
+
329
+ if (paths.some((p) => p.includes('next.config'))) {
330
+ return 'nextjs';
331
+ }
332
+ if (contents.some((c) => c.includes('from fastapi'))) {
333
+ return 'fastapi';
334
+ }
335
+ if (contents.some((c) => c.includes('from flask'))) {
336
+ return 'flask';
337
+ }
338
+ if (paths.some((p) => p.includes('package.json'))) {
339
+ const pkgFile = files.find((f) => f.path.includes('package.json'));
340
+ if (pkgFile) {
341
+ if (pkgFile.content.includes('"react"')) {
342
+ return 'react';
343
+ }
344
+ if (pkgFile.content.includes('"vue"')) {
345
+ return 'vue';
346
+ }
347
+ if (pkgFile.content.includes('"express"')) {
348
+ return 'express';
349
+ }
350
+ if (pkgFile.content.includes('"fastify"')) {
351
+ return 'fastify';
352
+ }
353
+ }
354
+ return 'node';
355
+ }
356
+ if (paths.some((p) => p.includes('requirements.txt') || p.endsWith('.py'))) {
357
+ return 'python';
358
+ }
359
+ if (paths.some((p) => p.includes('go.mod') || p.endsWith('.go'))) {
360
+ return 'go';
361
+ }
362
+ if (paths.some((p) => p.includes('cargo.toml') || p.endsWith('.rs'))) {
363
+ return 'rust';
364
+ }
365
+
366
+ return 'node';
367
+ }
368
+
369
+ /**
370
+ * Get file extension for a language.
371
+ */
372
+ private getExtensionForLanguage(lang: string): string {
373
+ const map: Record<string, string> = {
374
+ typescript: 'ts',
375
+ javascript: 'js',
376
+ python: 'py',
377
+ go: 'go',
378
+ rust: 'rs',
379
+ json: 'json',
380
+ yaml: 'yml',
381
+ markdown: 'md',
382
+ html: 'html',
383
+ css: 'css',
384
+ shell: 'sh',
385
+ bash: 'sh',
386
+ dockerfile: 'dockerfile',
387
+ sql: 'sql'
388
+ };
389
+
390
+ return map[lang.toLowerCase()] || 'txt';
391
+ }
392
+
393
+ /**
394
+ * Format the result based on output format.
395
+ */
396
+ private formatResult(
397
+ parsed: {
398
+ files: ProjectFile[];
399
+ metadata: ProjectMetadata;
400
+ setupCommands: string[];
401
+ },
402
+ format: OutputFormat,
403
+ rawResponse: string
404
+ ): ScaffoldResult {
405
+ const instructions = this.generateInstructions(parsed);
406
+
407
+ return {
408
+ files: format === 'instructions' ? [] : parsed.files,
409
+ instructions,
410
+ setupCommands: parsed.setupCommands,
411
+ metadata: parsed.metadata,
412
+ rawResponse: format === 'json' ? rawResponse : undefined
413
+ };
414
+ }
415
+
416
+ /**
417
+ * Generate setup instructions.
418
+ */
419
+ private generateInstructions(parsed: {
420
+ files: ProjectFile[];
421
+ metadata: ProjectMetadata;
422
+ setupCommands: string[];
423
+ }): string {
424
+ const lines: string[] = [];
425
+
426
+ lines.push(`# ${parsed.metadata.projectName} Setup`);
427
+ lines.push('');
428
+ lines.push(`Project type: ${parsed.metadata.projectType}`);
429
+ lines.push(`Files: ${parsed.metadata.fileCount}`);
430
+ lines.push('');
431
+
432
+ if (parsed.metadata.technologies.length > 0) {
433
+ lines.push('## Technologies');
434
+ lines.push(parsed.metadata.technologies.map((t) => `- ${t}`).join('\n'));
435
+ lines.push('');
436
+ }
437
+
438
+ lines.push('## Quick Start');
439
+ lines.push('');
440
+ lines.push('1. Create project directory:');
441
+ lines.push('```bash');
442
+ lines.push(`mkdir ${parsed.metadata.projectName}`);
443
+ lines.push(`cd ${parsed.metadata.projectName}`);
444
+ lines.push('```');
445
+ lines.push('');
446
+
447
+ lines.push('2. Create the following files:');
448
+ for (const file of parsed.files.slice(0, 10)) {
449
+ lines.push(` - \`${file.path}\`${file.description ? `: ${file.description}` : ''}`);
450
+ }
451
+ if (parsed.files.length > 10) {
452
+ lines.push(` - ... and ${parsed.files.length - 10} more files`);
453
+ }
454
+ lines.push('');
455
+
456
+ if (parsed.setupCommands.length > 0) {
457
+ lines.push('3. Run setup commands:');
458
+ lines.push('```bash');
459
+ lines.push(parsed.setupCommands.join('\n'));
460
+ lines.push('```');
461
+ lines.push('');
462
+ }
463
+
464
+ if (parsed.metadata.estimatedSetupTime !== 'unknown') {
465
+ lines.push(`Estimated setup time: ${parsed.metadata.estimatedSetupTime}`);
466
+ }
467
+
468
+ return lines.join('\n');
469
+ }
470
+ }
471
+
472
+ // ============================================================================
473
+ // Utility Functions
474
+ // ============================================================================
475
+
476
+ /**
477
+ * Create an empty scaffold result.
478
+ */
479
+ export function createEmptyScaffoldResult(error: string): ScaffoldResult {
480
+ return {
481
+ files: [],
482
+ instructions: `Error: ${error}`,
483
+ setupCommands: [],
484
+ metadata: {
485
+ projectType: 'auto',
486
+ projectName: 'unknown',
487
+ fileCount: 0,
488
+ totalSize: 0,
489
+ estimatedSetupTime: 'unknown',
490
+ technologies: [],
491
+ features: []
492
+ }
493
+ };
494
+ }