skrypt-ai 0.1.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.
Files changed (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +200 -0
  3. package/dist/autofix/index.d.ts +46 -0
  4. package/dist/autofix/index.js +240 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +40 -0
  7. package/dist/commands/autofix.d.ts +2 -0
  8. package/dist/commands/autofix.js +143 -0
  9. package/dist/commands/generate.d.ts +2 -0
  10. package/dist/commands/generate.js +320 -0
  11. package/dist/commands/init.d.ts +2 -0
  12. package/dist/commands/init.js +56 -0
  13. package/dist/commands/review-pr.d.ts +2 -0
  14. package/dist/commands/review-pr.js +117 -0
  15. package/dist/commands/watch.d.ts +2 -0
  16. package/dist/commands/watch.js +142 -0
  17. package/dist/config/index.d.ts +2 -0
  18. package/dist/config/index.js +2 -0
  19. package/dist/config/loader.d.ts +9 -0
  20. package/dist/config/loader.js +82 -0
  21. package/dist/config/types.d.ts +24 -0
  22. package/dist/config/types.js +34 -0
  23. package/dist/generator/generator.d.ts +15 -0
  24. package/dist/generator/generator.js +144 -0
  25. package/dist/generator/index.d.ts +4 -0
  26. package/dist/generator/index.js +4 -0
  27. package/dist/generator/organizer.d.ts +29 -0
  28. package/dist/generator/organizer.js +222 -0
  29. package/dist/generator/types.d.ts +83 -0
  30. package/dist/generator/types.js +1 -0
  31. package/dist/generator/writer.d.ts +28 -0
  32. package/dist/generator/writer.js +320 -0
  33. package/dist/github/pr-comments.d.ts +40 -0
  34. package/dist/github/pr-comments.js +308 -0
  35. package/dist/llm/anthropic-client.d.ts +16 -0
  36. package/dist/llm/anthropic-client.js +92 -0
  37. package/dist/llm/index.d.ts +53 -0
  38. package/dist/llm/index.js +400 -0
  39. package/dist/llm/llm.manual-test.d.ts +1 -0
  40. package/dist/llm/llm.manual-test.js +112 -0
  41. package/dist/llm/llm.mock-test.d.ts +4 -0
  42. package/dist/llm/llm.mock-test.js +79 -0
  43. package/dist/llm/openai-client.d.ts +17 -0
  44. package/dist/llm/openai-client.js +90 -0
  45. package/dist/llm/types.d.ts +60 -0
  46. package/dist/llm/types.js +20 -0
  47. package/dist/scanner/content-type.d.ts +39 -0
  48. package/dist/scanner/content-type.js +194 -0
  49. package/dist/scanner/content-type.test.d.ts +1 -0
  50. package/dist/scanner/content-type.test.js +231 -0
  51. package/dist/scanner/go.d.ts +20 -0
  52. package/dist/scanner/go.js +269 -0
  53. package/dist/scanner/index.d.ts +21 -0
  54. package/dist/scanner/index.js +137 -0
  55. package/dist/scanner/python.d.ts +6 -0
  56. package/dist/scanner/python.js +57 -0
  57. package/dist/scanner/python_parser.py +230 -0
  58. package/dist/scanner/rust.d.ts +23 -0
  59. package/dist/scanner/rust.js +304 -0
  60. package/dist/scanner/scanner.test.d.ts +1 -0
  61. package/dist/scanner/scanner.test.js +210 -0
  62. package/dist/scanner/types.d.ts +50 -0
  63. package/dist/scanner/types.js +1 -0
  64. package/dist/scanner/typescript.d.ts +34 -0
  65. package/dist/scanner/typescript.js +327 -0
  66. package/dist/scanner/typescript.manual-test.d.ts +1 -0
  67. package/dist/scanner/typescript.manual-test.js +112 -0
  68. package/dist/template/docs.json +32 -0
  69. package/dist/template/mdx-components.tsx +62 -0
  70. package/dist/template/next-env.d.ts +6 -0
  71. package/dist/template/next.config.mjs +17 -0
  72. package/dist/template/package.json +39 -0
  73. package/dist/template/postcss.config.mjs +5 -0
  74. package/dist/template/public/search-index.json +1 -0
  75. package/dist/template/scripts/build-search-index.mjs +120 -0
  76. package/dist/template/src/app/api/mock/[...path]/route.ts +224 -0
  77. package/dist/template/src/app/api/openapi/route.ts +48 -0
  78. package/dist/template/src/app/api/rate-limit/route.ts +84 -0
  79. package/dist/template/src/app/docs/[...slug]/page.tsx +81 -0
  80. package/dist/template/src/app/docs/layout.tsx +9 -0
  81. package/dist/template/src/app/docs/page.mdx +67 -0
  82. package/dist/template/src/app/error.tsx +63 -0
  83. package/dist/template/src/app/layout.tsx +71 -0
  84. package/dist/template/src/app/page.tsx +18 -0
  85. package/dist/template/src/app/reference/route.ts +36 -0
  86. package/dist/template/src/app/robots.ts +14 -0
  87. package/dist/template/src/app/sitemap.ts +64 -0
  88. package/dist/template/src/components/breadcrumbs.tsx +41 -0
  89. package/dist/template/src/components/copy-button.tsx +29 -0
  90. package/dist/template/src/components/docs-layout.tsx +35 -0
  91. package/dist/template/src/components/edit-link.tsx +39 -0
  92. package/dist/template/src/components/feedback.tsx +52 -0
  93. package/dist/template/src/components/header.tsx +66 -0
  94. package/dist/template/src/components/mdx/accordion.tsx +48 -0
  95. package/dist/template/src/components/mdx/api-badge.tsx +57 -0
  96. package/dist/template/src/components/mdx/callout.tsx +111 -0
  97. package/dist/template/src/components/mdx/card.tsx +62 -0
  98. package/dist/template/src/components/mdx/changelog.tsx +57 -0
  99. package/dist/template/src/components/mdx/code-block.tsx +42 -0
  100. package/dist/template/src/components/mdx/code-group.tsx +125 -0
  101. package/dist/template/src/components/mdx/code-playground.tsx +322 -0
  102. package/dist/template/src/components/mdx/go-playground.tsx +235 -0
  103. package/dist/template/src/components/mdx/heading.tsx +37 -0
  104. package/dist/template/src/components/mdx/highlighted-code.tsx +89 -0
  105. package/dist/template/src/components/mdx/index.tsx +15 -0
  106. package/dist/template/src/components/mdx/param-table.tsx +71 -0
  107. package/dist/template/src/components/mdx/python-playground.tsx +293 -0
  108. package/dist/template/src/components/mdx/steps.tsx +43 -0
  109. package/dist/template/src/components/mdx/tabs.tsx +81 -0
  110. package/dist/template/src/components/rate-limit-display.tsx +183 -0
  111. package/dist/template/src/components/search-dialog.tsx +178 -0
  112. package/dist/template/src/components/sidebar.tsx +129 -0
  113. package/dist/template/src/components/syntax-theme-selector.tsx +50 -0
  114. package/dist/template/src/components/table-of-contents.tsx +84 -0
  115. package/dist/template/src/components/theme-toggle.tsx +46 -0
  116. package/dist/template/src/components/version-selector.tsx +61 -0
  117. package/dist/template/src/contexts/syntax-theme.tsx +52 -0
  118. package/dist/template/src/lib/highlight.ts +83 -0
  119. package/dist/template/src/lib/search-types.ts +37 -0
  120. package/dist/template/src/lib/search.ts +125 -0
  121. package/dist/template/src/lib/utils.ts +6 -0
  122. package/dist/template/src/styles/globals.css +152 -0
  123. package/dist/template/tsconfig.json +25 -0
  124. package/dist/template/tsconfig.tsbuildinfo +1 -0
  125. package/package.json +72 -0
@@ -0,0 +1,320 @@
1
+ import { mkdir, writeFile } from 'fs/promises';
2
+ import { dirname, join, basename, relative } from 'path';
3
+ import { formatAsMarkdown } from './generator.js';
4
+ import { organizeByTopic, detectCrossReferences, getCrossRefsForElement } from './organizer.js';
5
+ /**
6
+ * Generate llms.txt file (Answer Engine Optimization)
7
+ * Format follows https://llmstxt.org convention
8
+ */
9
+ export async function writeLlmsTxt(docs, outputDir, options = {}) {
10
+ const projectName = options.projectName || 'API';
11
+ const description = options.description || 'API documentation generated by skrypt';
12
+ let content = `# ${projectName}\n\n`;
13
+ content += `> ${description}\n\n`;
14
+ // Group by file for organization
15
+ const byFile = new Map();
16
+ for (const doc of docs) {
17
+ const file = basename(doc.element.filePath).replace(/\.[^.]+$/, '');
18
+ if (!byFile.has(file)) {
19
+ byFile.set(file, []);
20
+ }
21
+ byFile.get(file).push(doc);
22
+ }
23
+ // Summary section
24
+ content += `## Overview\n\n`;
25
+ content += `This project contains ${docs.length} documented API elements across ${byFile.size} modules.\n\n`;
26
+ // Quick reference
27
+ content += `## Quick Reference\n\n`;
28
+ for (const [file, fileDocs] of byFile) {
29
+ content += `### ${file}\n\n`;
30
+ for (const doc of fileDocs) {
31
+ content += `- \`${doc.element.name}\`: ${doc.element.kind}`;
32
+ if (doc.markdown) {
33
+ // Extract first sentence as summary
34
+ const firstSentence = doc.markdown.split(/\.\s/)[0]?.slice(0, 100);
35
+ if (firstSentence) {
36
+ content += ` - ${firstSentence}`;
37
+ }
38
+ }
39
+ content += '\n';
40
+ }
41
+ content += '\n';
42
+ }
43
+ // Detailed API section
44
+ content += `## API Details\n\n`;
45
+ for (const doc of docs) {
46
+ content += `### ${doc.element.name}\n\n`;
47
+ content += `**Type:** ${doc.element.kind}\n`;
48
+ content += `**File:** ${doc.element.filePath}\n\n`;
49
+ content += `\`\`\`\n${doc.element.signature}\n\`\`\`\n\n`;
50
+ if (doc.markdown) {
51
+ content += doc.markdown.slice(0, 500) + '\n\n';
52
+ }
53
+ if (doc.codeExample) {
54
+ content += `**Example:**\n\`\`\`${doc.codeLanguage}\n${doc.codeExample.slice(0, 300)}\n\`\`\`\n\n`;
55
+ }
56
+ }
57
+ // Write both llms.txt and llms-full.md
58
+ await writeFile(join(outputDir, 'llms.txt'), content, 'utf-8');
59
+ // Also write a condensed version (just signatures)
60
+ let condensed = `# ${projectName} API\n\n`;
61
+ condensed += `> ${description}\n\n`;
62
+ for (const doc of docs) {
63
+ condensed += `## ${doc.element.name}\n`;
64
+ condensed += `${doc.element.signature}\n\n`;
65
+ }
66
+ await writeFile(join(outputDir, 'llms-full.md'), condensed, 'utf-8');
67
+ }
68
+ /**
69
+ * Write generated docs to output directory
70
+ */
71
+ export async function writeDocsToDirectory(results, outputDir, sourceDir) {
72
+ let filesWritten = 0;
73
+ let totalDocs = 0;
74
+ // Create output directory
75
+ await mkdir(outputDir, { recursive: true });
76
+ for (const result of results) {
77
+ if (result.docs.length === 0)
78
+ continue;
79
+ // Calculate relative path from source
80
+ const relPath = relative(sourceDir, result.filePath);
81
+ // Validate path traversal - prevent escaping output directory
82
+ if (relPath.startsWith('..') || relPath.startsWith('/')) {
83
+ console.warn(`Skipping file outside source directory: ${result.filePath}`);
84
+ continue;
85
+ }
86
+ const docFileName = relPath.replace(/\.[^.]+$/, '.md');
87
+ const outputPath = join(outputDir, docFileName);
88
+ // Create subdirectories if needed
89
+ await mkdir(dirname(outputPath), { recursive: true });
90
+ // Generate markdown content
91
+ const title = basename(result.filePath, '.py')
92
+ .replace(/^./, c => c.toUpperCase())
93
+ .replace(/_/g, ' ');
94
+ const content = formatAsMarkdown(result.docs, title);
95
+ // Write file
96
+ await writeFile(outputPath, content, 'utf-8');
97
+ filesWritten++;
98
+ totalDocs += result.docs.length;
99
+ }
100
+ // Write index file
101
+ await writeIndexFile(results, outputDir, sourceDir);
102
+ return { filesWritten, totalDocs };
103
+ }
104
+ /**
105
+ * Write an index file linking to all generated docs
106
+ */
107
+ async function writeIndexFile(results, outputDir, sourceDir) {
108
+ let content = '# API Documentation\n\n';
109
+ content += 'Generated by [skrypt](https://github.com/debgotwired/skrypt)\n\n';
110
+ // Summary
111
+ const totalElements = results.reduce((sum, r) => sum + r.docs.length, 0);
112
+ content += '## Summary\n\n';
113
+ content += `- **Total elements:** ${totalElements}\n\n`;
114
+ // File index
115
+ content += '## Files\n\n';
116
+ for (const result of results) {
117
+ if (result.docs.length === 0)
118
+ continue;
119
+ const relPath = relative(sourceDir, result.filePath);
120
+ const docFileName = relPath.replace(/\.[^.]+$/, '.md');
121
+ content += `- [${relPath}](./${docFileName})`;
122
+ content += ` (${result.docs.length} elements)\n`;
123
+ }
124
+ await writeFile(join(outputDir, 'README.md'), content, 'utf-8');
125
+ }
126
+ /**
127
+ * Group generated docs by file
128
+ */
129
+ export function groupDocsByFile(docs) {
130
+ const byFile = new Map();
131
+ for (const doc of docs) {
132
+ const file = doc.element.filePath;
133
+ if (!byFile.has(file)) {
134
+ byFile.set(file, []);
135
+ }
136
+ byFile.get(file).push(doc);
137
+ }
138
+ return Array.from(byFile.entries()).map(([filePath, fileDocs]) => ({
139
+ filePath,
140
+ docs: fileDocs
141
+ }));
142
+ }
143
+ // Topic-organized output
144
+ /**
145
+ * Write docs organized by topic
146
+ */
147
+ export async function writeDocsByTopic(docs, outputDir) {
148
+ let filesWritten = 0;
149
+ // Organize docs by topic
150
+ const topics = organizeByTopic(docs);
151
+ const crossRefs = detectCrossReferences(docs);
152
+ // Create output directory
153
+ await mkdir(outputDir, { recursive: true });
154
+ // Write each topic as a separate file
155
+ for (const topic of topics) {
156
+ const topicPath = join(outputDir, `${topic.id}.md`);
157
+ const content = formatTopicMarkdown(topic, crossRefs);
158
+ await writeFile(topicPath, content, 'utf-8');
159
+ filesWritten++;
160
+ }
161
+ // Write navigation index
162
+ await writeTopicIndexFile(topics, outputDir);
163
+ // Write sidebar config
164
+ await writeSidebarConfig(topics, outputDir);
165
+ return { filesWritten, totalDocs: docs.length, topics };
166
+ }
167
+ /**
168
+ * Format a topic as markdown with cross-references
169
+ */
170
+ function formatTopicMarkdown(topic, allCrossRefs) {
171
+ // MDX frontmatter (standard across doc platforms)
172
+ let content = `---
173
+ title: "${topic.name}"
174
+ description: "${topic.description || `API reference for ${topic.name}`}"
175
+ ---
176
+
177
+ `;
178
+ // Table of contents
179
+ content += '<CardGroup cols={2}>\n';
180
+ for (const doc of topic.docs) {
181
+ const anchor = slugify(doc.element.name);
182
+ const icon = doc.element.kind === 'class' ? 'cube' : doc.element.kind === 'method' ? 'code' : 'function';
183
+ content += ` <Card title="${doc.element.name}" icon="${icon}" href="#${anchor}">\n`;
184
+ content += ` ${doc.element.kind}\n`;
185
+ content += ` </Card>\n`;
186
+ }
187
+ content += '</CardGroup>\n\n';
188
+ // Each element
189
+ for (const doc of topic.docs) {
190
+ content += formatElementWithCrossRefs(doc, allCrossRefs);
191
+ }
192
+ return content;
193
+ }
194
+ /**
195
+ * Format a single element with cross-references
196
+ */
197
+ function formatElementWithCrossRefs(doc, allCrossRefs) {
198
+ const { element } = doc;
199
+ const anchor = slugify(element.name);
200
+ let section = `<a id="${anchor}"></a>\n\n`;
201
+ section += `## \`${element.name}\`\n\n`;
202
+ section += `\`\`\`${doc.codeLanguage}\n${element.signature}\n\`\`\`\n\n`;
203
+ if (doc.markdown) {
204
+ section += doc.markdown + '\n\n';
205
+ }
206
+ // Multi-language examples (MDX CodeGroup)
207
+ const hasMultiLang = doc.typescriptExample && doc.pythonExample;
208
+ if (hasMultiLang) {
209
+ section += '### Examples\n\n';
210
+ section += '<CodeGroup>\n\n';
211
+ section += `\`\`\`typescript example.ts\n${doc.typescriptExample}\n\`\`\`\n\n`;
212
+ section += `\`\`\`python example.py\n${doc.pythonExample}\n\`\`\`\n\n`;
213
+ section += '</CodeGroup>\n\n';
214
+ }
215
+ else if (doc.codeExample) {
216
+ section += '### Example\n\n';
217
+ section += `\`\`\`${doc.codeLanguage} example.${doc.codeLanguage === 'typescript' ? 'ts' : doc.codeLanguage === 'python' ? 'py' : 'js'}\n${doc.codeExample}\n\`\`\`\n\n`;
218
+ }
219
+ // Cross-references
220
+ const refs = getCrossRefsForElement(element.name, allCrossRefs);
221
+ if (refs.length > 0) {
222
+ section += '### Related\n\n';
223
+ section += '<CardGroup cols={3}>\n';
224
+ const uses = refs.filter(r => r.relationship === 'uses');
225
+ const usedBy = refs.filter(r => r.relationship === 'used-by');
226
+ const related = refs.filter(r => r.relationship === 'related' || r.relationship === 'see-also');
227
+ for (const ref of [...uses, ...usedBy, ...related].slice(0, 6)) {
228
+ const label = ref.relationship === 'uses' ? 'Uses' : ref.relationship === 'used-by' ? 'Used by' : 'Related';
229
+ section += ` <Card title="${ref.toElement}" icon="link" href="#${slugify(ref.toElement)}">\n`;
230
+ section += ` ${label}\n`;
231
+ section += ` </Card>\n`;
232
+ }
233
+ section += '</CardGroup>\n\n';
234
+ }
235
+ section += '---\n\n';
236
+ return section;
237
+ }
238
+ /**
239
+ * Write topic-based index file
240
+ */
241
+ async function writeTopicIndexFile(topics, outputDir) {
242
+ // Summary stats
243
+ const totalElements = topics.reduce((sum, t) => sum + t.docs.length, 0);
244
+ // MDX frontmatter
245
+ let content = `---
246
+ title: "API Reference"
247
+ description: "Complete API documentation with code examples"
248
+ ---
249
+
250
+ `;
251
+ content += `<Info>\n`;
252
+ content += ` **${totalElements}** documented elements\n`;
253
+ content += `</Info>\n\n`;
254
+ // Topics as CardGroup
255
+ content += '<CardGroup cols={2}>\n';
256
+ for (const topic of topics) {
257
+ const icon = topic.id === 'memory' ? 'database' : topic.id === 'tools' ? 'wrench' : topic.id === 'core' ? 'cube' : 'folder';
258
+ content += ` <Card title="${topic.name}" icon="${icon}" href="${topic.id}">\n`;
259
+ content += ` ${topic.description || ''} (${topic.docs.length} items)\n`;
260
+ content += ` </Card>\n`;
261
+ }
262
+ content += '</CardGroup>\n\n';
263
+ // Quick reference table per topic
264
+ for (const topic of topics) {
265
+ content += `## ${topic.name}\n\n`;
266
+ if (topic.description) {
267
+ content += `${topic.description}\n\n`;
268
+ }
269
+ content += '| Element | Type |\n';
270
+ content += '|---------|------|\n';
271
+ for (const doc of topic.docs.slice(0, 5)) {
272
+ content += `| [\`${doc.element.name}\`](${topic.id}#${slugify(doc.element.name)}) | ${doc.element.kind} |\n`;
273
+ }
274
+ if (topic.docs.length > 5) {
275
+ content += `| *...and ${topic.docs.length - 5} more* | |\n`;
276
+ }
277
+ content += '\n';
278
+ }
279
+ await writeFile(join(outputDir, 'index.mdx'), content, 'utf-8');
280
+ }
281
+ /**
282
+ * Write sidebar configuration for doc platforms
283
+ */
284
+ async function writeSidebarConfig(topics, outputDir) {
285
+ // Navigation config (compatible with multiple doc platforms)
286
+ const navGroups = topics.map(topic => ({
287
+ group: topic.name,
288
+ pages: [`api-reference/${topic.id}`]
289
+ }));
290
+ const navConfig = {
291
+ navigation: [
292
+ { group: 'API Reference', pages: navGroups.flatMap(g => g.pages) }
293
+ ],
294
+ // Full navigation with nested pages
295
+ groups: navGroups
296
+ };
297
+ await writeFile(join(outputDir, '_navigation.json'), JSON.stringify(navConfig, null, 2), 'utf-8');
298
+ // Sidebar config (Docusaurus-compatible)
299
+ const docusaurusSidebar = {
300
+ apiSidebar: topics.map(topic => ({
301
+ type: 'category',
302
+ label: topic.name,
303
+ items: topic.docs.map(doc => ({
304
+ type: 'doc',
305
+ id: `${topic.id}/${slugify(doc.element.name)}`,
306
+ label: doc.element.name
307
+ }))
308
+ }))
309
+ };
310
+ await writeFile(join(outputDir, '_sidebars.json'), JSON.stringify(docusaurusSidebar, null, 2), 'utf-8');
311
+ }
312
+ /**
313
+ * Convert string to URL-safe slug
314
+ */
315
+ function slugify(str) {
316
+ return str
317
+ .toLowerCase()
318
+ .replace(/[^a-z0-9]+/g, '-')
319
+ .replace(/^-|-$/g, '');
320
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * GitHub PR Comments Integration
3
+ *
4
+ * Posts documentation suggestions and issues as PR comments.
5
+ * Requires: GITHUB_TOKEN environment variable
6
+ */
7
+ export interface PRCommentConfig {
8
+ owner: string;
9
+ repo: string;
10
+ pullNumber: number;
11
+ token?: string;
12
+ }
13
+ export interface DocumentationIssue {
14
+ type: 'missing' | 'outdated' | 'broken_example' | 'suggestion';
15
+ severity: 'error' | 'warning' | 'info';
16
+ file: string;
17
+ line?: number;
18
+ message: string;
19
+ suggestion?: string;
20
+ code?: string;
21
+ }
22
+ export interface CommentResult {
23
+ success: boolean;
24
+ commentId?: number;
25
+ error?: string;
26
+ }
27
+ /**
28
+ * Post a review comment on a PR
29
+ */
30
+ export declare function postPRComment(config: PRCommentConfig, issues: DocumentationIssue[]): Promise<CommentResult>;
31
+ /**
32
+ * Post inline review comments on specific lines
33
+ */
34
+ export declare function postInlineComments(config: PRCommentConfig, issues: DocumentationIssue[]): Promise<CommentResult[]>;
35
+ /**
36
+ * Analyze a PR for documentation issues
37
+ */
38
+ export declare function analyzePRForDocs(config: PRCommentConfig, _options?: {
39
+ checkExamples?: boolean;
40
+ }): Promise<DocumentationIssue[]>;
@@ -0,0 +1,308 @@
1
+ /**
2
+ * GitHub PR Comments Integration
3
+ *
4
+ * Posts documentation suggestions and issues as PR comments.
5
+ * Requires: GITHUB_TOKEN environment variable
6
+ */
7
+ const GITHUB_API = 'https://api.github.com';
8
+ /**
9
+ * Post a review comment on a PR
10
+ */
11
+ export async function postPRComment(config, issues) {
12
+ const token = config.token || process.env.GITHUB_TOKEN;
13
+ if (!token) {
14
+ return {
15
+ success: false,
16
+ error: 'GITHUB_TOKEN environment variable required',
17
+ };
18
+ }
19
+ if (issues.length === 0) {
20
+ return { success: true };
21
+ }
22
+ // Build comment body
23
+ const body = buildCommentBody(issues);
24
+ try {
25
+ const response = await fetch(`${GITHUB_API}/repos/${config.owner}/${config.repo}/issues/${config.pullNumber}/comments`, {
26
+ method: 'POST',
27
+ headers: {
28
+ Authorization: `Bearer ${token}`,
29
+ Accept: 'application/vnd.github.v3+json',
30
+ 'Content-Type': 'application/json',
31
+ 'User-Agent': 'skrypt-cli',
32
+ },
33
+ body: JSON.stringify({ body }),
34
+ });
35
+ if (!response.ok) {
36
+ const error = await response.text();
37
+ return {
38
+ success: false,
39
+ error: `GitHub API error: ${response.status} ${error}`,
40
+ };
41
+ }
42
+ const data = await response.json();
43
+ return {
44
+ success: true,
45
+ commentId: data.id,
46
+ };
47
+ }
48
+ catch (err) {
49
+ const message = err instanceof Error ? err.message : String(err);
50
+ return {
51
+ success: false,
52
+ error: message,
53
+ };
54
+ }
55
+ }
56
+ /**
57
+ * Post inline review comments on specific lines
58
+ */
59
+ export async function postInlineComments(config, issues) {
60
+ const token = config.token || process.env.GITHUB_TOKEN;
61
+ if (!token) {
62
+ return [{
63
+ success: false,
64
+ error: 'GITHUB_TOKEN environment variable required',
65
+ }];
66
+ }
67
+ // First, get the PR diff to find commit SHA
68
+ const prResponse = await fetch(`${GITHUB_API}/repos/${config.owner}/${config.repo}/pulls/${config.pullNumber}`, {
69
+ headers: {
70
+ Authorization: `Bearer ${token}`,
71
+ Accept: 'application/vnd.github.v3+json',
72
+ 'User-Agent': 'skrypt-cli',
73
+ },
74
+ });
75
+ if (!prResponse.ok) {
76
+ return [{
77
+ success: false,
78
+ error: 'Could not fetch PR details',
79
+ }];
80
+ }
81
+ const pr = await prResponse.json();
82
+ const commitId = pr.head.sha;
83
+ const results = [];
84
+ for (const issue of issues) {
85
+ if (!issue.line)
86
+ continue;
87
+ try {
88
+ const response = await fetch(`${GITHUB_API}/repos/${config.owner}/${config.repo}/pulls/${config.pullNumber}/comments`, {
89
+ method: 'POST',
90
+ headers: {
91
+ Authorization: `Bearer ${token}`,
92
+ Accept: 'application/vnd.github.v3+json',
93
+ 'Content-Type': 'application/json',
94
+ 'User-Agent': 'skrypt-cli',
95
+ },
96
+ body: JSON.stringify({
97
+ body: formatInlineComment(issue),
98
+ commit_id: commitId,
99
+ path: issue.file,
100
+ line: issue.line,
101
+ side: 'RIGHT',
102
+ }),
103
+ });
104
+ if (response.ok) {
105
+ const data = await response.json();
106
+ results.push({ success: true, commentId: data.id });
107
+ }
108
+ else {
109
+ const error = await response.text();
110
+ results.push({
111
+ success: false,
112
+ error: `Line ${issue.line}: ${error}`,
113
+ });
114
+ }
115
+ }
116
+ catch (err) {
117
+ results.push({
118
+ success: false,
119
+ error: err instanceof Error ? err.message : String(err),
120
+ });
121
+ }
122
+ }
123
+ return results;
124
+ }
125
+ /**
126
+ * Build the main PR comment body
127
+ */
128
+ function buildCommentBody(issues) {
129
+ const errorCount = issues.filter(i => i.severity === 'error').length;
130
+ const warningCount = issues.filter(i => i.severity === 'warning').length;
131
+ const infoCount = issues.filter(i => i.severity === 'info').length;
132
+ let body = `## 📚 Skrypt Review\n\n`;
133
+ if (errorCount > 0) {
134
+ body += `❌ **${errorCount} error${errorCount > 1 ? 's' : ''}**`;
135
+ }
136
+ if (warningCount > 0) {
137
+ body += ` ⚠️ **${warningCount} warning${warningCount > 1 ? 's' : ''}**`;
138
+ }
139
+ if (infoCount > 0) {
140
+ body += ` ℹ️ **${infoCount} suggestion${infoCount > 1 ? 's' : ''}**`;
141
+ }
142
+ body += '\n\n---\n\n';
143
+ // Group by type
144
+ const byType = new Map();
145
+ for (const issue of issues) {
146
+ const existing = byType.get(issue.type) || [];
147
+ existing.push(issue);
148
+ byType.set(issue.type, existing);
149
+ }
150
+ const typeLabels = {
151
+ missing: '📝 Missing Documentation',
152
+ outdated: '🔄 Outdated Documentation',
153
+ broken_example: '🐛 Broken Code Examples',
154
+ suggestion: '💡 Suggestions',
155
+ };
156
+ for (const [type, typeIssues] of byType) {
157
+ body += `### ${typeLabels[type] || type}\n\n`;
158
+ for (const issue of typeIssues) {
159
+ const icon = issue.severity === 'error' ? '❌' : issue.severity === 'warning' ? '⚠️' : 'ℹ️';
160
+ body += `${icon} **${issue.file}**`;
161
+ if (issue.line) {
162
+ body += ` (line ${issue.line})`;
163
+ }
164
+ body += `\n`;
165
+ body += `> ${issue.message}\n`;
166
+ if (issue.suggestion) {
167
+ body += `\n**Suggestion:** ${issue.suggestion}\n`;
168
+ }
169
+ if (issue.code) {
170
+ body += `\n\`\`\`diff\n${issue.code}\n\`\`\`\n`;
171
+ }
172
+ body += '\n';
173
+ }
174
+ }
175
+ body += '\n---\n';
176
+ body += '*Generated by [skrypt](https://skrypt.sh)*';
177
+ return body;
178
+ }
179
+ /**
180
+ * Format an inline review comment
181
+ */
182
+ function formatInlineComment(issue) {
183
+ const icon = issue.severity === 'error' ? '❌' : issue.severity === 'warning' ? '⚠️' : '💡';
184
+ let comment = `${icon} **${issue.type.replace('_', ' ')}:** ${issue.message}`;
185
+ if (issue.suggestion) {
186
+ comment += `\n\n**Suggestion:** ${issue.suggestion}`;
187
+ }
188
+ if (issue.code) {
189
+ comment += `\n\n\`\`\`suggestion\n${issue.code}\n\`\`\``;
190
+ }
191
+ return comment;
192
+ }
193
+ /**
194
+ * Analyze a PR for documentation issues
195
+ */
196
+ export async function analyzePRForDocs(config, _options = {}) {
197
+ const token = config.token || process.env.GITHUB_TOKEN;
198
+ if (!token) {
199
+ throw new Error('GITHUB_TOKEN required');
200
+ }
201
+ // Get PR files
202
+ const response = await fetch(`${GITHUB_API}/repos/${config.owner}/${config.repo}/pulls/${config.pullNumber}/files`, {
203
+ headers: {
204
+ Authorization: `Bearer ${token}`,
205
+ Accept: 'application/vnd.github.v3+json',
206
+ 'User-Agent': 'skrypt-cli',
207
+ },
208
+ });
209
+ if (!response.ok) {
210
+ throw new Error('Could not fetch PR files');
211
+ }
212
+ const files = await response.json();
213
+ const issues = [];
214
+ // Analyze each changed file
215
+ for (const file of files) {
216
+ // Check for source code files that might need doc updates
217
+ if (/\.(ts|js|py|go|rs)$/.test(file.filename)) {
218
+ // Check if there's a corresponding doc file
219
+ const docFile = file.filename
220
+ .replace(/\.(ts|js|py|go|rs)$/, '.md')
221
+ .replace(/^src\//, 'docs/');
222
+ const hasDocChanges = files.some((f) => f.filename === docFile);
223
+ if (!hasDocChanges && file.status === 'modified') {
224
+ // Check if the diff contains new exports/functions
225
+ const patch = file.patch || '';
226
+ const hasNewExports = /^\+.*export\s+(function|const|class|interface)/m.test(patch);
227
+ const hasNewPubFn = /^\+.*pub\s+fn/m.test(patch); // Rust
228
+ const hasNewDefPy = /^\+.*def\s+[a-z]/m.test(patch); // Python
229
+ if (hasNewExports || hasNewPubFn || hasNewDefPy) {
230
+ issues.push({
231
+ type: 'missing',
232
+ severity: 'warning',
233
+ file: file.filename,
234
+ message: 'New API added but documentation may not be updated',
235
+ suggestion: `Run \`skrypt generate\` to update documentation for ${file.filename}`,
236
+ });
237
+ }
238
+ }
239
+ }
240
+ // Check markdown files for potential issues
241
+ if (/\.mdx?$/.test(file.filename)) {
242
+ const patch = file.patch || '';
243
+ // Check for broken code blocks
244
+ const codeBlockIssues = checkCodeBlocks(patch, file.filename);
245
+ issues.push(...codeBlockIssues);
246
+ }
247
+ }
248
+ return issues;
249
+ }
250
+ /**
251
+ * Check code blocks in markdown for potential issues
252
+ */
253
+ function checkCodeBlocks(patch, filename) {
254
+ const issues = [];
255
+ // Find code blocks being added
256
+ const addedLines = patch
257
+ .split('\n')
258
+ .filter(line => line.startsWith('+'))
259
+ .map(line => line.slice(1))
260
+ .join('\n');
261
+ const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
262
+ let match;
263
+ while ((match = codeBlockRegex.exec(addedLines)) !== null) {
264
+ const language = match[1] || 'text';
265
+ const code = match[2];
266
+ if (!code)
267
+ continue;
268
+ // Check for common issues
269
+ if (code.includes('TODO') || code.includes('FIXME')) {
270
+ issues.push({
271
+ type: 'broken_example',
272
+ severity: 'warning',
273
+ file: filename,
274
+ message: 'Code example contains TODO/FIXME marker',
275
+ suggestion: 'Complete the code example before merging',
276
+ });
277
+ }
278
+ if (code.includes('...') && !code.includes('...args')) {
279
+ issues.push({
280
+ type: 'broken_example',
281
+ severity: 'info',
282
+ file: filename,
283
+ message: 'Code example contains ellipsis (...) placeholder',
284
+ suggestion: 'Consider providing complete, runnable code',
285
+ });
286
+ }
287
+ // Check for obviously broken syntax
288
+ if (language === 'javascript' || language === 'typescript') {
289
+ try {
290
+ new Function(code);
291
+ }
292
+ catch (err) {
293
+ const errMsg = err instanceof Error ? err.message : String(err);
294
+ // Only flag if it's clearly meant to be runnable
295
+ if (!code.includes('// ...') && code.length > 20) {
296
+ issues.push({
297
+ type: 'broken_example',
298
+ severity: 'error',
299
+ file: filename,
300
+ message: `JavaScript/TypeScript syntax error: ${errMsg.split('\n')[0]}`,
301
+ suggestion: 'Fix the code example or mark it as pseudo-code',
302
+ });
303
+ }
304
+ }
305
+ }
306
+ }
307
+ return issues;
308
+ }
@@ -0,0 +1,16 @@
1
+ import { LLMClient, LLMClientConfig, CompletionRequest, CompletionResponse } from './types.js';
2
+ import { LLMProvider } from '../config/types.js';
3
+ /**
4
+ * Anthropic Claude client
5
+ */
6
+ export declare class AnthropicClient implements LLMClient {
7
+ provider: LLMProvider;
8
+ private client;
9
+ private model;
10
+ private maxRetries;
11
+ constructor(config: LLMClientConfig);
12
+ isConfigured(): boolean;
13
+ complete(request: CompletionRequest): Promise<CompletionResponse>;
14
+ private mapStopReason;
15
+ private handleError;
16
+ }