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.
- package/.babelrc +4 -0
- package/.claude/commands/commit-push-pr.md +19 -0
- package/.claude/commands/dedupe.md +38 -0
- package/.devcontainer/Dockerfile +91 -0
- package/.devcontainer/devcontainer.json +57 -0
- package/.devcontainer/init-firewall.sh +137 -0
- package/.gitattributes +2 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +188 -0
- package/.github/ISSUE_TEMPLATE/config.yml +17 -0
- package/.github/ISSUE_TEMPLATE/documentation.yml +117 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +132 -0
- package/.github/ISSUE_TEMPLATE/model_behavior.yml +220 -0
- package/.github/workflows/auto-close-duplicates.yml +31 -0
- package/.github/workflows/backfill-duplicate-comments.yml +44 -0
- package/.github/workflows/claude-dedupe-issues.yml +80 -0
- package/.github/workflows/claude-issue-triage.yml +106 -0
- package/.github/workflows/claude.yml +37 -0
- package/.github/workflows/issue-opened-dispatch.yml +28 -0
- package/.github/workflows/lock-closed-issues.yml +92 -0
- package/.github/workflows/log-issue-events.yml +40 -0
- package/CHANGELOG.md +646 -0
- package/KILO.md +1273 -0
- package/LICENSE.md +21 -0
- package/README.md +176 -0
- package/SECURITY.md +12 -0
- package/Script/run_devcontainer_claude_code.ps1 +152 -0
- package/api/githubApi.ts +144 -0
- package/babel.config.js +7 -0
- package/cli/.gitkeep +0 -0
- package/cli/auto-close-duplicates.ts +5 -0
- package/cli/configure.js +33 -0
- package/cli/list-models.js +48 -0
- package/cli/run.js +61 -0
- package/cli/set-api-key.js +26 -0
- package/config.json +4 -0
- package/demo.gif +0 -0
- package/examples/gpt-3.5-turbo.js +38 -0
- package/examples/gpt-4.js +38 -0
- package/examples/hooks/bash_command_validator_example.py +83 -0
- package/index.d.ts +3 -0
- package/index.js +62 -0
- package/jest.config.js +6 -0
- package/openapi.yaml +61 -0
- package/package.json +47 -0
- package/scripts/backfill-duplicate-comments.ts +213 -0
- package/tests/api-githubApi.test.ts +30 -0
- package/tests/auto-close-duplicates.test.ts +145 -0
- package/tests/cli-configure.test.ts +88 -0
- package/tests/cli-list-models.test.ts +44 -0
- package/tests/cli-run.test.ts +97 -0
- package/tests/cli-set-api-key.test.ts +54 -0
- package/tests/cli-validate-api-key.test.ts +52 -0
- package/tsconfig.json +18 -0
package/config.json
ADDED
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
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
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
|
+
});
|