recoder-code 1.0.113

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 (53) hide show
  1. package/.babelrc +4 -0
  2. package/.claude/commands/commit-push-pr.md +19 -0
  3. package/.claude/commands/dedupe.md +38 -0
  4. package/.devcontainer/Dockerfile +91 -0
  5. package/.devcontainer/devcontainer.json +57 -0
  6. package/.devcontainer/init-firewall.sh +137 -0
  7. package/.gitattributes +2 -0
  8. package/.github/ISSUE_TEMPLATE/bug_report.yml +188 -0
  9. package/.github/ISSUE_TEMPLATE/config.yml +17 -0
  10. package/.github/ISSUE_TEMPLATE/documentation.yml +117 -0
  11. package/.github/ISSUE_TEMPLATE/feature_request.yml +132 -0
  12. package/.github/ISSUE_TEMPLATE/model_behavior.yml +220 -0
  13. package/.github/workflows/auto-close-duplicates.yml +31 -0
  14. package/.github/workflows/backfill-duplicate-comments.yml +44 -0
  15. package/.github/workflows/claude-dedupe-issues.yml +80 -0
  16. package/.github/workflows/claude-issue-triage.yml +106 -0
  17. package/.github/workflows/claude.yml +37 -0
  18. package/.github/workflows/issue-opened-dispatch.yml +28 -0
  19. package/.github/workflows/lock-closed-issues.yml +92 -0
  20. package/.github/workflows/log-issue-events.yml +40 -0
  21. package/CHANGELOG.md +646 -0
  22. package/KILO.md +1273 -0
  23. package/LICENSE.md +21 -0
  24. package/README.md +176 -0
  25. package/SECURITY.md +12 -0
  26. package/Script/run_devcontainer_claude_code.ps1 +152 -0
  27. package/api/githubApi.ts +144 -0
  28. package/babel.config.js +7 -0
  29. package/cli/.gitkeep +0 -0
  30. package/cli/auto-close-duplicates.ts +5 -0
  31. package/cli/configure.js +33 -0
  32. package/cli/list-models.js +48 -0
  33. package/cli/run.js +61 -0
  34. package/cli/set-api-key.js +26 -0
  35. package/config.json +4 -0
  36. package/demo.gif +0 -0
  37. package/examples/gpt-3.5-turbo.js +38 -0
  38. package/examples/gpt-4.js +38 -0
  39. package/examples/hooks/bash_command_validator_example.py +83 -0
  40. package/index.d.ts +3 -0
  41. package/index.js +62 -0
  42. package/jest.config.js +6 -0
  43. package/openapi.yaml +61 -0
  44. package/package.json +47 -0
  45. package/scripts/backfill-duplicate-comments.ts +213 -0
  46. package/tests/api-githubApi.test.ts +30 -0
  47. package/tests/auto-close-duplicates.test.ts +145 -0
  48. package/tests/cli-configure.test.ts +88 -0
  49. package/tests/cli-list-models.test.ts +44 -0
  50. package/tests/cli-run.test.ts +97 -0
  51. package/tests/cli-set-api-key.test.ts +54 -0
  52. package/tests/cli-validate-api-key.test.ts +52 -0
  53. package/tsconfig.json +18 -0
package/config.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "openRouterApiKey": "",
3
+ "openRouterModel": "default-model"
4
+ }
package/demo.gif ADDED
Binary file
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const axios = require('axios');
5
+
6
+ const configPath = path.join(process.cwd(), '.recoderrc');
7
+
8
+ try {
9
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
10
+
11
+ if (!config.openRouterApiKey) {
12
+ console.error('Open Router API key not found. Please set it using: npx recoder-code set-api-key <your-open-router-api-key>');
13
+ process.exit(1);
14
+ }
15
+
16
+ const apiKey = config.openRouterApiKey;
17
+ const modelName = 'gpt-3.5-turbo';
18
+
19
+ // Example API request using the configured API key and model
20
+ const response = await axios.post(
21
+ `https://api.openrouter.com/v1/models/${modelName}/completions`,
22
+ {
23
+ prompt: "Hello, OpenRouter!",
24
+ max_tokens: 50
25
+ },
26
+ {
27
+ headers: {
28
+ 'Authorization': `Bearer ${apiKey}`,
29
+ 'Content-Type': 'application/json'
30
+ }
31
+ }
32
+ );
33
+
34
+ console.log('Response from OpenRouter:', response.data.choices[0].text.trim());
35
+ } catch (err) {
36
+ console.error('Error reading .recoderrc or making API request:', err.message);
37
+ process.exit(1);
38
+ }
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const axios = require('axios');
5
+
6
+ const configPath = path.join(process.cwd(), '.recoderrc');
7
+
8
+ try {
9
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
10
+
11
+ if (!config.openRouterApiKey) {
12
+ console.error('Open Router API key not found. Please set it using: npx recoder-code set-api-key <your-open-router-api-key>');
13
+ process.exit(1);
14
+ }
15
+
16
+ const apiKey = config.openRouterApiKey;
17
+ const modelName = 'gpt-4';
18
+
19
+ // Example API request using the configured API key and model
20
+ const response = await axios.post(
21
+ `https://api.openrouter.com/v1/models/${modelName}/completions`,
22
+ {
23
+ prompt: "Hello, OpenRouter!",
24
+ max_tokens: 50
25
+ },
26
+ {
27
+ headers: {
28
+ 'Authorization': `Bearer ${apiKey}`,
29
+ 'Content-Type': 'application/json'
30
+ }
31
+ }
32
+ );
33
+
34
+ console.log('Response from OpenRouter:', response.data.choices[0].text.trim());
35
+ } catch (err) {
36
+ console.error('Error reading .recoderrc or making API request:', err.message);
37
+ process.exit(1);
38
+ }
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Recoder Code Hook: Bash Command Validator
4
+ =========================================
5
+ This hook runs as a PreToolUse hook for the Bash tool.
6
+ It validates bash commands against a set of rules before execution.
7
+ In this case it changes grep calls to using rg.
8
+
9
+ Read more about hooks here: https://docs.anthropic.com/en/docs/Recoder-code/hooks
10
+
11
+ Make sure to change your path to your actual script.
12
+
13
+ {
14
+ "hooks": {
15
+ "PreToolUse": [
16
+ {
17
+ "matcher": "Bash",
18
+ "hooks": [
19
+ {
20
+ "type": "command",
21
+ "command": "python3 /path/to/Recoder-code/examples/hooks/bash_command_validator_example.py"
22
+ }
23
+ ]
24
+ }
25
+ ]
26
+ }
27
+ }
28
+
29
+ """
30
+
31
+ import json
32
+ import re
33
+ import sys
34
+
35
+ # Define validation rules as a list of (regex pattern, message) tuples
36
+ _VALIDATION_RULES = [
37
+ (
38
+ r"^grep\b(?!.*\|)",
39
+ "Use 'rg' (ripgrep) instead of 'grep' for better performance and features",
40
+ ),
41
+ (
42
+ r"^find\s+\S+\s+-name\b",
43
+ "Use 'rg --files | rg pattern' or 'rg --files -g pattern' instead of 'find -name' for better performance",
44
+ ),
45
+ ]
46
+
47
+
48
+ def _validate_command(command: str) -> list[str]:
49
+ issues = []
50
+ for pattern, message in _VALIDATION_RULES:
51
+ if re.search(pattern, command):
52
+ issues.append(message)
53
+ return issues
54
+
55
+
56
+ def main():
57
+ try:
58
+ input_data = json.load(sys.stdin)
59
+ except json.JSONDecodeError as e:
60
+ print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
61
+ # Exit code 1 shows stderr to the user but not to Recoder
62
+ sys.exit(1)
63
+
64
+ tool_name = input_data.get("tool_name", "")
65
+ if tool_name != "Bash":
66
+ sys.exit(0)
67
+
68
+ tool_input = input_data.get("tool_input", {})
69
+ command = tool_input.get("command", "")
70
+
71
+ if not command:
72
+ sys.exit(0)
73
+
74
+ issues = _validate_command(command)
75
+ if issues:
76
+ for message in issues:
77
+ print(f"• {message}", file=sys.stderr)
78
+ # Exit code 2 blocks tool call and shows stderr to Recoder
79
+ sys.exit(2)
80
+
81
+
82
+ if __name__ == "__main__":
83
+ main()
package/index.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ declare module 'index.js' {
2
+ export const model: any;
3
+ }
package/index.js ADDED
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const readline = require('readline');
5
+
6
+ const configPath = path.join(__dirname, 'config.json');
7
+
8
+ // Check for API key in environment variable
9
+ let apiKey = process.env.OPENROUTER_API_KEY;
10
+ let model = process.env.OPENROUTER_MODEL || 'default-model';
11
+
12
+ if (!apiKey) {
13
+ try {
14
+ // Read the config file if the environment variable is not set
15
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
16
+ apiKey = config.openRouterApiKey;
17
+ model = config.openRouterModel || 'default-model';
18
+ } catch (err) {
19
+ console.error('Error reading config.json:', err.message);
20
+ }
21
+ }
22
+
23
+ if (!apiKey || typeof apiKey !== 'string' || apiKey.trim().length === 0) {
24
+ // Prompt the user for the API key
25
+ const rl = readline.createInterface({
26
+ input: process.stdin,
27
+ output: process.stdout
28
+ });
29
+
30
+ rl.question('Please enter your OpenRouter API key: ', (inputApiKey) => {
31
+ rl.close();
32
+ apiKey = inputApiKey.trim();
33
+
34
+ if (!apiKey || typeof apiKey !== 'string' || apiKey.length === 0) {
35
+ console.error('No valid Open Router API key provided. Exiting.');
36
+ process.exit(1);
37
+ }
38
+
39
+ // Save the API key to the config file
40
+ try {
41
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
42
+ config.openRouterApiKey = apiKey;
43
+ config.openRouterModel = model;
44
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
45
+ console.log('Open Router API key saved to config.json.');
46
+ } catch (err) {
47
+ console.error('Error saving API key to config.json:', err.message);
48
+ process.exit(1);
49
+ }
50
+
51
+ console.log('recoder.xyz');
52
+ console.log('Open Router API key is set and valid.');
53
+ });
54
+ } else {
55
+ console.log('recoder.xyz');
56
+ console.log('Open Router API key is set and valid.');
57
+ }
58
+
59
+ // Export the model for use in other modules
60
+ module.exports = {
61
+ model: model
62
+ };
package/jest.config.js ADDED
@@ -0,0 +1,6 @@
1
+ module.exports = {
2
+ transform: {
3
+ '^.+\\.tsx?$': 'babel-jest',
4
+ },
5
+ transformIgnorePatterns: ['/node_modules/(?!(@babel|@octokit/core|@octokit/rest)/)'],
6
+ };
package/openapi.yaml ADDED
@@ -0,0 +1,61 @@
1
+ openapi: 3.0.0
2
+ info:
3
+ title: Recoder API
4
+ description: API documentation for Recoder
5
+ version: 1.0.0
6
+ servers:
7
+ - url: https://api.recoder.xyz
8
+ description: Production server
9
+ - url: https://engine.recoder.xyz
10
+ description: Engine server
11
+ paths:
12
+ /chat/completions:
13
+ post:
14
+ summary: Create a chat completion
15
+ requestBody:
16
+ required: true
17
+ content:
18
+ application/json:
19
+ schema:
20
+ type: object
21
+ properties:
22
+ model:
23
+ type: string
24
+ description: The model to use for the completion.
25
+ messages:
26
+ type: array
27
+ items:
28
+ type: object
29
+ properties:
30
+ role:
31
+ type: string
32
+ enum: [system, user, assistant]
33
+ content:
34
+ type: string
35
+ description: The content of the message.
36
+ responses:
37
+ '200':
38
+ description: Successful response
39
+ content:
40
+ application/json:
41
+ schema:
42
+ type: object
43
+ properties:
44
+ choices:
45
+ type: array
46
+ items:
47
+ type: object
48
+ properties:
49
+ message:
50
+ type: object
51
+ properties:
52
+ role:
53
+ type: string
54
+ enum: [system, user, assistant]
55
+ content:
56
+ type: string
57
+ description: The content of the message.
58
+ '400':
59
+ description: Bad request
60
+ '500':
61
+ description: Internal server error
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "recoder-code",
3
+ "version": "1.0.113",
4
+ "description": "An npm package that prints 'recoder.xyz' upon installation and allows users to utilize OpenRouter services with a valid API key.",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "recoder-code": "cli/set-api-key.js",
8
+ "recoder-run": "cli/run.js",
9
+ "recoder-configure": "cli/configure.js",
10
+ "recoder-list-models": "cli/list-models.js"
11
+ },
12
+ "scripts": {
13
+ "set-api-key": "node cli/set-api-key.js",
14
+ "postinstall": "echo 'recoder.xyz'",
15
+ "test": "jest"
16
+ },
17
+ "config": {
18
+ "openRouterApiKey": ""
19
+ },
20
+ "dependencies": {
21
+ "@octokit/rest": "^22.0.0",
22
+ "axios": "^1.12.0",
23
+ "openai": "^5.20.1"
24
+ },
25
+ "devDependencies": {
26
+ "@babel/plugin-transform-modules-commonjs": "^7.27.1",
27
+ "@babel/preset-env": "^7.28.3",
28
+ "@babel/preset-typescript": "^7.27.1",
29
+ "@types/jest": "^30.0.0",
30
+ "babel-jest": "^30.1.2",
31
+ "jest": "^29.7.0",
32
+ "supertest": "^7.1.4",
33
+ "ts-jest": "^29.4.1",
34
+ "vitest": "^3.2.4"
35
+ },
36
+ "keywords": [
37
+ "recoder",
38
+ "openrouter",
39
+ "api"
40
+ ],
41
+ "author": "caelum0x",
42
+ "license": "MIT",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "https://github.com/yourusername/recoder-package.git"
46
+ }
47
+ }
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env bun
2
+
3
+ declare global {
4
+ var process: {
5
+ env: Record<string, string | undefined>;
6
+ };
7
+ }
8
+
9
+ interface GitHubIssue {
10
+ number: number;
11
+ title: string;
12
+ state: string;
13
+ state_reason?: string;
14
+ user: { id: number };
15
+ created_at: string;
16
+ closed_at?: string;
17
+ }
18
+
19
+ interface GitHubComment {
20
+ id: number;
21
+ body: string;
22
+ created_at: string;
23
+ user: { type: string; id: number };
24
+ }
25
+
26
+ async function githubRequest<T>(endpoint: string, token: string, method: string = 'GET', body?: any): Promise<T> {
27
+ const response = await fetch(`https://api.github.com${endpoint}`, {
28
+ method,
29
+ headers: {
30
+ Authorization: `Bearer ${token}`,
31
+ Accept: "application/vnd.github.v3+json",
32
+ "User-Agent": "backfill-duplicate-comments-script",
33
+ ...(body && { "Content-Type": "application/json" }),
34
+ },
35
+ ...(body && { body: JSON.stringify(body) }),
36
+ });
37
+
38
+ if (!response.ok) {
39
+ throw new Error(
40
+ `GitHub API request failed: ${response.status} ${response.statusText}`
41
+ );
42
+ }
43
+
44
+ return response.json();
45
+ }
46
+
47
+ async function triggerDedupeWorkflow(
48
+ owner: string,
49
+ repo: string,
50
+ issueNumber: number,
51
+ token: string,
52
+ dryRun: boolean = true
53
+ ): Promise<void> {
54
+ if (dryRun) {
55
+ console.log(`[DRY RUN] Would trigger dedupe workflow for issue #${issueNumber}`);
56
+ return;
57
+ }
58
+
59
+ await githubRequest(
60
+ `/repos/${owner}/${repo}/actions/workflows/claude-dedupe-issues.yml/dispatches`,
61
+ token,
62
+ 'POST',
63
+ {
64
+ ref: 'main',
65
+ inputs: {
66
+ issue_number: issueNumber.toString()
67
+ }
68
+ }
69
+ );
70
+ }
71
+
72
+ async function backfillDuplicateComments(): Promise<void> {
73
+ console.log("[DEBUG] Starting backfill duplicate comments script");
74
+
75
+ const token = process.env.GITHUB_TOKEN;
76
+ if (!token) {
77
+ throw new Error(`GITHUB_TOKEN environment variable is required
78
+
79
+ Usage:
80
+ GITHUB_TOKEN=your_token bun run scripts/backfill-duplicate-comments.ts
81
+
82
+ Environment Variables:
83
+ GITHUB_TOKEN - GitHub personal access token with repo and actions permissions (required)
84
+ DRY_RUN - Set to "false" to actually trigger workflows (default: true for safety)
85
+ MAX_ISSUE_NUMBER - Only process issues with numbers less than this value (default: 4050)`);
86
+ }
87
+ console.log("[DEBUG] GitHub token found");
88
+
89
+ const owner = "anthropics";
90
+ const repo = "claude-code";
91
+ const dryRun = process.env.DRY_RUN !== "false";
92
+ const maxIssueNumber = parseInt(process.env.MAX_ISSUE_NUMBER || "4050", 10);
93
+ const minIssueNumber = parseInt(process.env.MIN_ISSUE_NUMBER || "1", 10);
94
+
95
+ console.log(`[DEBUG] Repository: ${owner}/${repo}`);
96
+ console.log(`[DEBUG] Dry run mode: ${dryRun}`);
97
+ console.log(`[DEBUG] Looking at issues between #${minIssueNumber} and #${maxIssueNumber}`);
98
+
99
+ console.log(`[DEBUG] Fetching issues between #${minIssueNumber} and #${maxIssueNumber}...`);
100
+ const allIssues: GitHubIssue[] = [];
101
+ let page = 1;
102
+ const perPage = 100;
103
+
104
+ while (true) {
105
+ const pageIssues: GitHubIssue[] = await githubRequest(
106
+ `/repos/${owner}/${repo}/issues?state=all&per_page=${perPage}&page=${page}&sort=created&direction=desc`,
107
+ token
108
+ );
109
+
110
+ if (pageIssues.length === 0) break;
111
+
112
+ // Filter to only include issues within the specified range
113
+ const filteredIssues = pageIssues.filter(issue =>
114
+ issue.number >= minIssueNumber && issue.number < maxIssueNumber
115
+ );
116
+ allIssues.push(...filteredIssues);
117
+
118
+ // If the oldest issue in this page is still above our minimum, we need to continue
119
+ // but if the oldest issue is below our minimum, we can stop
120
+ const oldestIssueInPage = pageIssues[pageIssues.length - 1];
121
+ if (oldestIssueInPage && oldestIssueInPage.number >= maxIssueNumber) {
122
+ console.log(`[DEBUG] Oldest issue in page #${page} is #${oldestIssueInPage.number}, continuing...`);
123
+ } else if (oldestIssueInPage && oldestIssueInPage.number < minIssueNumber) {
124
+ console.log(`[DEBUG] Oldest issue in page #${page} is #${oldestIssueInPage.number}, below minimum, stopping`);
125
+ break;
126
+ } else if (filteredIssues.length === 0 && pageIssues.length > 0) {
127
+ console.log(`[DEBUG] No issues in page #${page} are in range #${minIssueNumber}-#${maxIssueNumber}, continuing...`);
128
+ }
129
+
130
+ page++;
131
+
132
+ // Safety limit to avoid infinite loops
133
+ if (page > 200) {
134
+ console.log("[DEBUG] Reached page limit, stopping pagination");
135
+ break;
136
+ }
137
+ }
138
+
139
+ console.log(`[DEBUG] Found ${allIssues.length} issues between #${minIssueNumber} and #${maxIssueNumber}`);
140
+
141
+ let processedCount = 0;
142
+ let candidateCount = 0;
143
+ let triggeredCount = 0;
144
+
145
+ for (const issue of allIssues) {
146
+ processedCount++;
147
+ console.log(
148
+ `[DEBUG] Processing issue #${issue.number} (${processedCount}/${allIssues.length}): ${issue.title}`
149
+ );
150
+
151
+ console.log(`[DEBUG] Fetching comments for issue #${issue.number}...`);
152
+ const comments: GitHubComment[] = await githubRequest(
153
+ `/repos/${owner}/${repo}/issues/${issue.number}/comments`,
154
+ token
155
+ );
156
+ console.log(
157
+ `[DEBUG] Issue #${issue.number} has ${comments.length} comments`
158
+ );
159
+
160
+ // Look for existing duplicate detection comments (from the dedupe bot)
161
+ const dupeDetectionComments = comments.filter(
162
+ (comment) =>
163
+ comment.body.includes("Found") &&
164
+ comment.body.includes("possible duplicate") &&
165
+ comment.user.type === "Bot"
166
+ );
167
+
168
+ console.log(
169
+ `[DEBUG] Issue #${issue.number} has ${dupeDetectionComments.length} duplicate detection comments`
170
+ );
171
+
172
+ // Skip if there's already a duplicate detection comment
173
+ if (dupeDetectionComments.length > 0) {
174
+ console.log(
175
+ `[DEBUG] Issue #${issue.number} already has duplicate detection comment, skipping`
176
+ );
177
+ continue;
178
+ }
179
+
180
+ candidateCount++;
181
+ const issueUrl = `https://github.com/${owner}/${repo}/issues/${issue.number}`;
182
+
183
+ try {
184
+ console.log(
185
+ `[INFO] ${dryRun ? '[DRY RUN] ' : ''}Triggering dedupe workflow for issue #${issue.number}: ${issueUrl}`
186
+ );
187
+ await triggerDedupeWorkflow(owner, repo, issue.number, token, dryRun);
188
+
189
+ if (!dryRun) {
190
+ console.log(
191
+ `[SUCCESS] Successfully triggered dedupe workflow for issue #${issue.number}`
192
+ );
193
+ }
194
+ triggeredCount++;
195
+ } catch (error) {
196
+ console.error(
197
+ `[ERROR] Failed to trigger workflow for issue #${issue.number}: ${error}`
198
+ );
199
+ }
200
+
201
+ // Add a delay between workflow triggers to avoid overwhelming the system
202
+ await new Promise(resolve => setTimeout(resolve, 1000));
203
+ }
204
+
205
+ console.log(
206
+ `[DEBUG] Script completed. Processed ${processedCount} issues, found ${candidateCount} candidates without duplicate comments, ${dryRun ? 'would trigger' : 'triggered'} ${triggeredCount} workflows`
207
+ );
208
+ }
209
+
210
+ backfillDuplicateComments().catch(console.error);
211
+
212
+ // Make it a module
213
+ export {};
@@ -0,0 +1,30 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import { getGitHubData } from '../api/githubApi';
3
+
4
+ describe('GitHub API Functionality', () => {
5
+ it('should fetch data from GitHub API successfully', async () => {
6
+ const result = await getGitHubData('octocat', 'Hello-World');
7
+ expect(result).toHaveProperty('name', 'Hello-World');
8
+ expect(result).toHaveProperty('owner.login', 'octocat');
9
+ });
10
+
11
+ it('should handle invalid repository gracefully', async () => {
12
+ try {
13
+ await getGitHubData('octocat', 'nonexistent-repo');
14
+ } catch (error: unknown) {
15
+ if (error instanceof Error) {
16
+ expect(error.message).toContain('Repository not found');
17
+ }
18
+ }
19
+ });
20
+
21
+ it('should handle invalid user gracefully', async () => {
22
+ try {
23
+ await getGitHubData('nonexistent-user', 'Hello-World');
24
+ } catch (error: unknown) {
25
+ if (error instanceof Error) {
26
+ expect(error.message).toContain('User not found');
27
+ }
28
+ }
29
+ });
30
+ });