@doccov/api 0.3.4 → 0.3.5

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,252 @@
1
+ /**
2
+ * AI-powered build plan generation agent.
3
+ * Uses Claude to analyze repository context and generate execution plans.
4
+ */
5
+
6
+ import { createAnthropic } from '@ai-sdk/anthropic';
7
+ import { generateObject } from 'ai';
8
+ import { z } from 'zod';
9
+ import type {
10
+ BuildPlan,
11
+ BuildPlanEnvironment,
12
+ BuildPlanStep,
13
+ BuildPlanTarget,
14
+ GitHubProjectContext,
15
+ } from '@doccov/sdk';
16
+
17
+ /**
18
+ * Zod schema for build plan validation.
19
+ */
20
+ const BuildPlanStepSchema = z.object({
21
+ id: z.string().describe('Unique identifier (e.g., "install", "build-types")'),
22
+ name: z.string().describe('Human-readable step name'),
23
+ command: z.string().describe('Command to execute'),
24
+ args: z.array(z.string()).describe('Command arguments'),
25
+ cwd: z.string().optional().describe('Working directory relative to repo root'),
26
+ timeout: z.number().optional().describe('Timeout in milliseconds'),
27
+ optional: z.boolean().optional().describe('If true, failure does not stop execution'),
28
+ });
29
+
30
+ const BuildPlanEnvironmentSchema = z.object({
31
+ runtime: z.enum(['node22', 'node24']).describe('Runtime to use (node22 or node24)'),
32
+ packageManager: z.enum(['npm', 'yarn', 'pnpm', 'bun']).describe('Package manager'),
33
+ requiredTools: z.array(z.string()).optional().describe('Additional required tools'),
34
+ });
35
+
36
+ const BuildPlanReasoningSchema = z.object({
37
+ summary: z.string().describe('Brief summary of the approach (1-2 sentences)'),
38
+ rationale: z.string().describe('Why this approach was chosen'),
39
+ concerns: z.array(z.string()).describe('Potential issues or concerns'),
40
+ });
41
+
42
+ const AIBuildPlanSchema = z.object({
43
+ environment: BuildPlanEnvironmentSchema,
44
+ steps: z.array(BuildPlanStepSchema).describe('Steps to execute in order'),
45
+ entryPoints: z.array(z.string()).describe('Entry point files to analyze'),
46
+ reasoning: BuildPlanReasoningSchema,
47
+ confidence: z.enum(['high', 'medium', 'low']).describe('Confidence in this plan'),
48
+ });
49
+
50
+ type AIBuildPlanOutput = z.infer<typeof AIBuildPlanSchema>;
51
+
52
+ /**
53
+ * Options for build plan generation.
54
+ */
55
+ export interface GenerateBuildPlanOptions {
56
+ /** Target a specific package in a monorepo */
57
+ targetPackage?: string;
58
+ /** Override the default model */
59
+ model?: string;
60
+ }
61
+
62
+ /**
63
+ * Format project context for the AI prompt.
64
+ */
65
+ function formatContext(context: GitHubProjectContext): string {
66
+ const sections: string[] = [];
67
+
68
+ // Repository metadata
69
+ sections.push(`=== Repository ===
70
+ Owner: ${context.metadata.owner}
71
+ Repo: ${context.metadata.repo}
72
+ Language: ${context.metadata.language ?? 'unknown'}
73
+ Topics: ${context.metadata.topics.join(', ') || 'none'}
74
+ Description: ${context.metadata.description ?? 'none'}`);
75
+
76
+ // Environment detection
77
+ sections.push(`=== Environment ===
78
+ Package Manager: ${context.packageManager}
79
+ Is Monorepo: ${context.workspace.isMonorepo}
80
+ Workspace Tool: ${context.workspace.tool ?? 'none'}
81
+ Workspace Packages: ${context.workspace.packages?.join(', ') ?? 'none'}`);
82
+
83
+ // Build hints
84
+ sections.push(`=== Build Hints ===
85
+ Has TypeScript: ${context.buildHints.hasTypeScript}
86
+ Has WASM: ${context.buildHints.hasWasm}
87
+ Has Native Modules: ${context.buildHints.hasNativeModules}
88
+ Has Build Script: ${context.buildHints.hasBuildScript}
89
+ Build Script: ${context.buildHints.buildScript ?? 'none'}
90
+ Frameworks: ${context.buildHints.frameworks.join(', ') || 'none'}`);
91
+
92
+ // File contents
93
+ if (context.files.packageJson) {
94
+ const truncated =
95
+ context.files.packageJson.length > 3000
96
+ ? `${context.files.packageJson.slice(0, 3000)}\n... (truncated)`
97
+ : context.files.packageJson;
98
+ sections.push(`=== package.json ===\n${truncated}`);
99
+ }
100
+
101
+ if (context.files.tsconfigJson) {
102
+ sections.push(`=== tsconfig.json ===\n${context.files.tsconfigJson}`);
103
+ }
104
+
105
+ if (context.files.lockfile) {
106
+ // Only include first 500 chars of lockfile
107
+ const preview = context.files.lockfile.content.slice(0, 500);
108
+ sections.push(`=== ${context.files.lockfile.name} (preview) ===\n${preview}\n...`);
109
+ }
110
+
111
+ return sections.join('\n\n');
112
+ }
113
+
114
+ /**
115
+ * System prompt for build plan generation.
116
+ */
117
+ const SYSTEM_PROMPT = `You are a build system expert. Your task is to analyze a GitHub repository and generate a build plan to analyze its TypeScript/JavaScript API.
118
+
119
+ The goal is to:
120
+ 1. Install dependencies
121
+ 2. Build the project (if needed) to generate TypeScript declarations
122
+ 3. Identify entry points for API documentation analysis
123
+
124
+ Package Manager Selection:
125
+ - If a lockfile is detected (package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lockb), use that package manager
126
+ - If Package Manager is "unknown" (no lockfile), default to npm with "npm install" (NOT "npm ci")
127
+ - IMPORTANT: "npm ci" and "--frozen-lockfile" flags ONLY work when a lockfile exists
128
+ - When no lockfile: use "npm install", "yarn install", "pnpm install", or "bun install" (without frozen flags)
129
+
130
+ Install Commands by Package Manager:
131
+ - npm with lockfile: ["npm", "ci"]
132
+ - npm without lockfile: ["npm", "install"]
133
+ - yarn with lockfile: ["yarn", "install", "--frozen-lockfile"]
134
+ - yarn without lockfile: ["yarn", "install"]
135
+ - pnpm with lockfile: ["pnpm", "install", "--frozen-lockfile"]
136
+ - pnpm without lockfile: ["pnpm", "install"]
137
+ - bun with lockfile: ["bun", "install", "--frozen-lockfile"]
138
+ - bun without lockfile: ["bun", "install"]
139
+
140
+ General Guidelines:
141
+ - For TypeScript projects, look for "types" or "exports" fields in package.json
142
+ - For monorepos, focus on the target package if specified
143
+ - Common entry points: src/index.ts, dist/index.d.ts, lib/index.ts, distribution/index.d.ts
144
+ - WASM projects may need build steps before .d.ts files exist
145
+ - Be conservative with timeouts (default 60000ms, increase for builds)
146
+ - Installation is usually required first
147
+ - Build step is needed if package.json has a "build" script and the types are in dist/distribution folder
148
+
149
+ Step ID conventions:
150
+ - "install" - install dependencies
151
+ - "build" - main build step
152
+ - "build-types" - generate type declarations
153
+ - "analyze" - run doccov spec (added automatically)`;
154
+
155
+ /**
156
+ * Generate a build plan prompt.
157
+ */
158
+ function generatePrompt(
159
+ context: GitHubProjectContext,
160
+ options: GenerateBuildPlanOptions,
161
+ ): string {
162
+ let prompt = `Analyze this repository and generate a build plan:\n\n${formatContext(context)}`;
163
+
164
+ if (options.targetPackage) {
165
+ prompt += `\n\nTarget Package: ${options.targetPackage}
166
+ Focus the plan on building and analyzing only this package within the monorepo.`;
167
+ }
168
+
169
+ prompt += `\n\nGenerate a build plan with:
170
+ - environment: Runtime and package manager configuration
171
+ - steps: Ordered build steps (install, build, etc.)
172
+ - entryPoints: TypeScript entry files to analyze (relative paths)
173
+ - reasoning: Explain your approach
174
+ - confidence: How confident you are in this plan`;
175
+
176
+ return prompt;
177
+ }
178
+
179
+ /**
180
+ * Get the Anthropic model for plan generation.
181
+ */
182
+ function getModel() {
183
+ const anthropic = createAnthropic();
184
+ return anthropic('claude-sonnet-4-20250514');
185
+ }
186
+
187
+ /**
188
+ * Generate a build plan for a GitHub repository.
189
+ */
190
+ export async function generateBuildPlan(
191
+ context: GitHubProjectContext,
192
+ options: GenerateBuildPlanOptions = {},
193
+ ): Promise<BuildPlan> {
194
+ const model = getModel();
195
+ const prompt = generatePrompt(context, options);
196
+
197
+ const { object } = await generateObject({
198
+ model,
199
+ schema: AIBuildPlanSchema,
200
+ system: SYSTEM_PROMPT,
201
+ prompt,
202
+ });
203
+
204
+ return transformToBuildPlan(object, context, options);
205
+ }
206
+
207
+ /**
208
+ * Transform AI output to full BuildPlan.
209
+ */
210
+ function transformToBuildPlan(
211
+ output: AIBuildPlanOutput,
212
+ context: GitHubProjectContext,
213
+ options: GenerateBuildPlanOptions,
214
+ ): BuildPlan {
215
+ const target: BuildPlanTarget = {
216
+ type: 'github',
217
+ repoUrl: `https://github.com/${context.metadata.owner}/${context.metadata.repo}`,
218
+ ref: context.ref,
219
+ rootPath: options.targetPackage,
220
+ entryPoints: output.entryPoints,
221
+ };
222
+
223
+ const environment: BuildPlanEnvironment = {
224
+ runtime: output.environment.runtime,
225
+ packageManager: output.environment.packageManager,
226
+ requiredTools: output.environment.requiredTools,
227
+ };
228
+
229
+ const steps: BuildPlanStep[] = output.steps.map((step) => ({
230
+ id: step.id,
231
+ name: step.name,
232
+ command: step.command,
233
+ args: step.args,
234
+ cwd: step.cwd,
235
+ timeout: step.timeout,
236
+ optional: step.optional,
237
+ }));
238
+
239
+ return {
240
+ version: '1.0.0',
241
+ generatedAt: new Date().toISOString(),
242
+ target,
243
+ environment,
244
+ steps,
245
+ reasoning: {
246
+ summary: output.reasoning.summary,
247
+ rationale: output.reasoning.rationale,
248
+ concerns: output.reasoning.concerns,
249
+ },
250
+ confidence: output.confidence,
251
+ };
252
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doccov/api",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "DocCov API - Badge endpoint and coverage services",
5
5
  "keywords": [
6
6
  "doccov",
@@ -18,17 +18,20 @@
18
18
  "license": "MIT",
19
19
  "author": "Ryan Waits",
20
20
  "type": "module",
21
- "main": "./src/index.ts",
22
21
  "scripts": {
22
+ "build": "bunup",
23
23
  "dev": "bun run --hot src/index.ts",
24
24
  "start": "bun run src/index.ts",
25
- "lint": "biome check src/",
26
- "lint:fix": "biome check --write src/",
27
- "format": "biome format --write src/"
25
+ "lint": "biome check src/ functions/ lib/",
26
+ "lint:fix": "biome check --write src/ functions/ lib/",
27
+ "format": "biome format --write src/ functions/ lib/"
28
28
  },
29
29
  "dependencies": {
30
- "@openpkg-ts/spec": "^0.8.0",
30
+ "@ai-sdk/anthropic": "^2.0.55",
31
+ "@doccov/sdk": "^0.13.0",
32
+ "@openpkg-ts/spec": "^0.9.0",
31
33
  "@vercel/sandbox": "^1.0.3",
34
+ "ai": "^5.0.111",
32
35
  "hono": "^4.0.0",
33
36
  "ms": "^2.1.3",
34
37
  "zod": "^3.25.0"
@@ -38,6 +41,7 @@
38
41
  "@types/ms": "^0.7.34",
39
42
  "@types/node": "^20.0.0",
40
43
  "@vercel/node": "^3.0.0",
44
+ "bunup": "latest",
41
45
  "typescript": "^5.0.0"
42
46
  }
43
47
  }
package/src/index.ts CHANGED
@@ -2,20 +2,20 @@ import { Hono } from 'hono';
2
2
  import { cors } from 'hono/cors';
3
3
  import { rateLimit } from './middleware/rate-limit';
4
4
  import { badgeRoute } from './routes/badge';
5
- import { scanRoute } from './routes/scan';
5
+ import { planRoute } from './routes/plan';
6
6
 
7
7
  const app = new Hono();
8
8
 
9
9
  // Middleware
10
10
  app.use('*', cors());
11
11
 
12
- // Rate limit /scan endpoint: 10 requests per minute per IP
12
+ // Rate limit /plan endpoint: 10 requests per minute per IP
13
13
  app.use(
14
- '/scan/*',
14
+ '/plan',
15
15
  rateLimit({
16
16
  windowMs: 60 * 1000,
17
17
  max: 10,
18
- message: 'Too many scan requests. Please try again in a minute.',
18
+ message: 'Too many plan requests. Please try again in a minute.',
19
19
  }),
20
20
  );
21
21
 
@@ -23,10 +23,12 @@ app.use(
23
23
  app.get('/', (c) => {
24
24
  return c.json({
25
25
  name: 'DocCov API',
26
- version: '0.3.0',
26
+ version: '0.4.0',
27
27
  endpoints: {
28
28
  badge: '/badge/:owner/:repo',
29
- scan: '/scan',
29
+ plan: '/plan',
30
+ execute: '/execute',
31
+ 'execute-stream': '/execute-stream',
30
32
  health: '/health',
31
33
  },
32
34
  });
@@ -38,7 +40,7 @@ app.get('/health', (c) => {
38
40
 
39
41
  // Routes
40
42
  app.route('/badge', badgeRoute);
41
- app.route('/scan', scanRoute);
43
+ app.route('/plan', planRoute);
42
44
 
43
45
  // Vercel serverless handler + Bun auto-serves this export
44
46
  export default app;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Plan route for local development (mirrors api/plan.ts)
3
+ * Does NOT use Vercel Sandbox - only GitHub API + Anthropic Claude
4
+ */
5
+
6
+ import { fetchGitHubContext, parseScanGitHubUrl } from '@doccov/sdk';
7
+ import { Hono } from 'hono';
8
+ import { generateBuildPlan } from '../../lib/plan-agent';
9
+
10
+ export const planRoute = new Hono();
11
+
12
+ planRoute.post('/', async (c) => {
13
+ const body = await c.req.json<{ url: string; ref?: string; package?: string }>();
14
+
15
+ if (!body.url) {
16
+ return c.json({ error: 'url is required' }, 400);
17
+ }
18
+
19
+ // Validate URL format
20
+ let repoUrl: string;
21
+ try {
22
+ const parsed = parseScanGitHubUrl(body.url);
23
+ if (!parsed) {
24
+ return c.json({ error: 'Invalid GitHub URL' }, 400);
25
+ }
26
+ repoUrl = `https://github.com/${parsed.owner}/${parsed.repo}`;
27
+ } catch {
28
+ return c.json({ error: 'Invalid GitHub URL' }, 400);
29
+ }
30
+
31
+ try {
32
+ // Fetch project context from GitHub
33
+ const context = await fetchGitHubContext(repoUrl, body.ref);
34
+
35
+ // Check for private repos
36
+ if (context.metadata.isPrivate) {
37
+ return c.json({
38
+ error: 'Private repositories are not supported',
39
+ hint: 'Use a public repository or run doccov locally',
40
+ }, 403);
41
+ }
42
+
43
+ // Generate build plan using AI
44
+ const plan = await generateBuildPlan(context, {
45
+ targetPackage: body.package,
46
+ });
47
+
48
+ return c.json({
49
+ plan,
50
+ context: {
51
+ owner: context.metadata.owner,
52
+ repo: context.metadata.repo,
53
+ ref: context.ref,
54
+ packageManager: context.packageManager,
55
+ isMonorepo: context.workspace.isMonorepo,
56
+ },
57
+ });
58
+ } catch (error) {
59
+ console.error('Plan generation error:', error);
60
+
61
+ if (error instanceof Error) {
62
+ if (error.message.includes('404') || error.message.includes('not found')) {
63
+ return c.json({ error: 'Repository not found' }, 404);
64
+ }
65
+ if (error.message.includes('rate limit')) {
66
+ return c.json({ error: 'GitHub API rate limit exceeded' }, 429);
67
+ }
68
+ }
69
+
70
+ return c.json({
71
+ error: 'Failed to generate build plan',
72
+ message: error instanceof Error ? error.message : 'Unknown error',
73
+ }, 500);
74
+ }
75
+ });
package/tsconfig.json CHANGED
@@ -10,6 +10,6 @@
10
10
  "isolatedModules": true,
11
11
  "noEmit": true
12
12
  },
13
- "include": ["api/**/*", "src/**/*"],
14
- "exclude": ["node_modules"]
13
+ "include": ["api/**/*", "functions/**/*", "lib/**/*", "src/**/*"],
14
+ "exclude": ["node_modules", "dist"]
15
15
  }
package/vercel.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
+ "installCommand": "bun install",
3
+ "buildCommand": "bun run build",
2
4
  "rewrites": [
3
5
  { "source": "/badge/:path*", "destination": "/api" },
4
6
  { "source": "/spec/:path*", "destination": "/api" },
5
- { "source": "/scan-stream", "destination": "/api/scan-stream" },
6
- { "source": "/scan/detect", "destination": "/api/scan/detect" },
7
- { "source": "/scan", "destination": "/api/scan" },
7
+ { "source": "/plan", "destination": "/api/plan" },
8
+ { "source": "/execute", "destination": "/api/execute" },
9
+ { "source": "/execute-stream", "destination": "/api/execute-stream" },
8
10
  { "source": "/(.*)", "destination": "/api" }
9
11
  ]
10
12
  }
@@ -1,118 +0,0 @@
1
- /**
2
- * Detect endpoint - uses SDK detection via SandboxFileSystem.
3
- * Detects monorepo structure and package manager for a GitHub repository.
4
- */
5
-
6
- import {
7
- detectPackageManager,
8
- SandboxFileSystem,
9
- detectMonorepo as sdkDetectMonorepo,
10
- } from '@doccov/sdk';
11
- import type { VercelRequest, VercelResponse } from '@vercel/node';
12
- import { Sandbox } from '@vercel/sandbox';
13
-
14
- export const config = {
15
- runtime: 'nodejs',
16
- maxDuration: 60, // Quick detection, 1 minute max
17
- };
18
-
19
- interface DetectRequestBody {
20
- url: string;
21
- ref?: string;
22
- }
23
-
24
- interface PackageInfo {
25
- name: string;
26
- path: string;
27
- description?: string;
28
- }
29
-
30
- interface DetectResponse {
31
- isMonorepo: boolean;
32
- packageManager: 'npm' | 'pnpm' | 'bun' | 'yarn';
33
- packages?: PackageInfo[];
34
- defaultPackage?: string;
35
- error?: string;
36
- }
37
-
38
- export default async function handler(req: VercelRequest, res: VercelResponse) {
39
- // CORS
40
- res.setHeader('Access-Control-Allow-Origin', '*');
41
- res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
42
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
43
-
44
- if (req.method === 'OPTIONS') {
45
- return res.status(200).end();
46
- }
47
-
48
- if (req.method !== 'POST') {
49
- return res.status(405).json({ error: 'Method not allowed' });
50
- }
51
-
52
- const body = req.body as DetectRequestBody;
53
-
54
- if (!body.url) {
55
- return res.status(400).json({ error: 'url is required' });
56
- }
57
-
58
- try {
59
- const result = await detectRepoStructure(body.url);
60
- return res.status(200).json(result);
61
- } catch (error) {
62
- const message = error instanceof Error ? error.message : String(error);
63
- return res.status(500).json({
64
- isMonorepo: false,
65
- packageManager: 'npm',
66
- error: message,
67
- } as DetectResponse);
68
- }
69
- }
70
-
71
- /**
72
- * Detect repository structure using SDK utilities via SandboxFileSystem.
73
- */
74
- async function detectRepoStructure(url: string): Promise<DetectResponse> {
75
- const sandbox = await Sandbox.create({
76
- source: {
77
- url,
78
- type: 'git',
79
- },
80
- resources: { vcpus: 2 },
81
- timeout: 60 * 1000, // 1 minute
82
- runtime: 'node22',
83
- });
84
-
85
- try {
86
- // Create SDK FileSystem abstraction for sandbox
87
- const fs = new SandboxFileSystem(sandbox);
88
-
89
- // Use SDK detection functions
90
- const [monoInfo, pmInfo] = await Promise.all([sdkDetectMonorepo(fs), detectPackageManager(fs)]);
91
-
92
- if (!monoInfo.isMonorepo) {
93
- return {
94
- isMonorepo: false,
95
- packageManager: pmInfo.name,
96
- };
97
- }
98
-
99
- // Map SDK package info to API response format
100
- const packages: PackageInfo[] = monoInfo.packages
101
- .filter((p) => !p.private)
102
- .map((p) => ({
103
- name: p.name,
104
- path: p.path,
105
- description: p.description,
106
- }))
107
- .sort((a, b) => a.name.localeCompare(b.name));
108
-
109
- return {
110
- isMonorepo: true,
111
- packageManager: pmInfo.name,
112
- packages,
113
- defaultPackage: packages[0]?.name,
114
- };
115
- } finally {
116
- await sandbox.stop();
117
- }
118
- }