husteno-o

[email protected]
2026-09-09 11:38:20 +0600 committed NaN years ago
5ccaeae
commit 5ccaeae
57 files changed
+1329 -421
Showing 57 changed files+1329-421
.env.example.gitignoreapps/api/src/index.tsapps/api/src/server.tsapps/bot/src/commands/actions.tsapps/bot/src/commands/activity.tsapps/bot/src/commands/ai.tsapps/bot/src/commands/ask.tsapps/bot/src/commands/code-review.tsapps/bot/src/commands/code.tsapps/bot/src/commands/connect.tsapps/bot/src/commands/handler.tsapps/bot/src/commands/home.tsapps/bot/src/commands/index.tsapps/bot/src/commands/investigate.tsapps/bot/src/commands/pr.tsapps/bot/src/commands/release.tsapps/bot/src/commands/repo.tsapps/bot/src/commands/search.tsapps/bot/src/commands/security.tsapps/bot/src/commands/settings.tsapps/bot/src/commands/team.tsapps/bot/src/commands/tools.tsapps/bot/src/commands/trending.tsapps/bot/src/commands/types.tsapps/bot/src/commands/watch.tsapps/bot/src/commands/why.tsapps/bot/src/events/interactionCreate.tsapps/bot/src/events/messageCreate.tsapps/bot/src/events/ready.tsapps/bot/src/index.test.tsapps/bot/src/index.tsapps/bot/src/ui/animation.tsapps/bot/src/ui/embeds.tsapps/worker/src/index.tsbiome.jsonpackages/ai/src/copilot.tspackages/ai/src/index.test.tspackages/ai/src/provider.tspackages/analytics/src/index.tspackages/cache/src/index.tspackages/config/src/index.tspackages/devtools/src/crypto-tools.tspackages/devtools/src/data-tools.tspackages/devtools/src/helpers-tools.tspackages/devtools/src/network-tools.tspackages/devtools/src/package-tools.tspackages/devtools/src/ssrf.tspackages/devtools/src/text-tools.tspackages/devtools/src/time-tools.tspackages/github/src/client.tspackages/monitoring/src/index.tspackages/news/src/index.tspackages/scheduler/src/index.tspackages/security/src/cve.tspackages/security/src/encryption.tspackages/trending/src/index.ts
▼.env.example
+21-4
44
44
GITHUB_CLIENT_SECRET=your_github_oauth_client_secret_here
45
45
46
46
# ------------------------------------------------------------------------------
47
-# 5. Optional AI Layer (Core operates 100% deterministically without AI)
47
+# 5. Optional AI Layer (Defaults to free spark-1.3 + longcat-2.0:free fallback)
48
48
# ------------------------------------------------------------------------------
49
-# Supported: none | openai | anthropic | gemini
50
-AI_PROVIDER=none
51
-AI_API_KEY=your_ai_api_key_here
49
+# Provider options:
50
+# spark = OpenCode Zen (spark-1.3) → Command Code (longcat-2.0:free) fallback
51
+# opencode = OpenCode Zen only (spark-1.3, free, no API key needed)
52
+# commandcode = Command Code API only (longcat-2.0:free, requires API key)
53
+# openai = OpenAI (requires AI_API_KEY)
54
+# anthropic = Anthropic (requires AI_API_KEY)
55
+# gemini = Google Gemini (requires AI_API_KEY)
56
+# none = AI disabled (deterministic heuristics only)
57
+AI_PROVIDER=spark
58
+# API key for Command Code (required for spark/commandcode providers)
59
+# Get your key at: https://commandcode.ai/studio
60
+AI_API_KEY=your_command_code_api_key_here
61
+
62
+# ------------------------------------------------------------------------------
63
+# 7. Encryption & Security
64
+# ------------------------------------------------------------------------------
65
+# ENCRYPTION_KEY must be a strong, random string used to derive the AES-256-GCM key
66
+# Generate with: openssl rand -hex 32
67
+ENCRYPTION_KEY=your_strong_encryption_key_here
68
+ENCRYPTION_SALT=your_salt_here
52
69
53
70
# ------------------------------------------------------------------------------
54
71
# 6. Security & Rate Limiting
55
72
▼.gitignore
+5-0
12
12
coverage/
13
13
*.zip
14
14
.DS_Store
15
+.commandcode/
16
+CODE_REVIEW_*.md
17
+CODE_REVIEW_*.json
18
+review.ts
19
+.understand-anything/
15
20
▼apps/api/src/index.ts
+1-1
13
13
host: config.HOST,
14
14
});
15
15
logger.info({ address }, `DevPulse API server listening on ${address}`);
16
- } catch (err) {
16
+ } catch (err: unknown) {
17
17
logger.error({ err }, "Failed to start DevPulse API server");
18
18
process.exit(1);
19
19
}
20
20
▼apps/api/src/server.ts
+31-6
5
5
import { logger } from "@devpulse/logger";
6
6
import { encryptSecret } from "@devpulse/security";
7
7
import { eq, sql } from "drizzle-orm";
8
-import Fastify, { type FastifyInstance } from "fastify";
8
+import Fastify, { type FastifyBaseLogger, type FastifyInstance } from "fastify";
9
+
10
+const RATE_LIMIT_WINDOW = 60;
11
+const RATE_LIMIT_MAX = 100;
12
+
13
+async function rateLimitHook(
14
+ req: { ip: string },
15
+ reply: { status: (code: number) => { send: (body: unknown) => void } },
16
+) {
17
+ const key = `ratelimit:api:${req.ip}`;
18
+ const result = await cache.checkRateLimit(key, RATE_LIMIT_MAX, RATE_LIMIT_WINDOW);
19
+ if (!result.allowed) {
20
+ reply.status(429).send({
21
+ error: "Too Many Requests",
22
+ retryAfter: result.resetSeconds,
23
+ });
24
+ }
25
+}
9
26
10
27
export interface ServerOptions {
11
28
discordClient?: {
16
33
};
17
34
}
18
35
36
+/**
37
+ * Builds and configures the Fastify HTTP server with health, readiness,
38
+ * metrics, and GitHub OAuth endpoints. Includes rate limiting.
39
+ */
19
40
export function buildServer(options?: ServerOptions): FastifyInstance {
20
41
const server = Fastify({
21
- loggerInstance: logger as any,
42
+ loggerInstance: logger as FastifyBaseLogger,
22
43
});
23
44
45
+ server.addHook("onRequest", rateLimitHook);
46
+
24
47
server.get("/health", async () => {
25
48
const memory = process.memoryUsage();
26
49
const discordStatus = options?.discordClient
67
90
: {}),
68
91
timestamp: new Date().toISOString(),
69
92
};
70
- } catch (err: any) {
93
+ } catch (err: unknown) {
71
94
logger.error({ err }, "Readiness check failed");
72
95
return reply.status(503).send({
73
96
status: "unready",
74
- error: err.message,
97
+ error: err instanceof Error ? err.message : String(err),
75
98
});
76
99
}
77
100
});
222
245
</body>
223
246
</html>
224
247
`);
225
- } catch (err: any) {
248
+ } catch (err: unknown) {
226
249
logger.error({ err }, "GitHub OAuth exchange failed");
227
250
return reply
228
251
.status(500)
229
252
.type("text/html")
230
- .send(`<h1>Internal Server Error</h1><p>${err.message}</p>`);
253
+ .send(
254
+ `<h1>Internal Server Error</h1><p>${err instanceof Error ? err.message : String(err)}</p>`,
255
+ );
231
256
}
232
257
});
233
258
234
259
▼apps/bot/src/commands/actions.ts
+4-2
101
101
await interaction.editReply({ embeds: [embed] });
102
102
return;
103
103
}
104
- } catch (err: any) {
105
- await interaction.editReply({ embeds: [createErrorEmbed(err)] });
104
+ } catch (err: unknown) {
105
+ await interaction.editReply({
106
+ embeds: [createErrorEmbed(err instanceof Error ? err : new Error(String(err)))],
107
+ });
106
108
}
107
109
},
108
110
};
109
111
▼apps/bot/src/commands/activity.ts
+4-2
136
136
await interaction.editReply({ embeds: [embed] });
137
137
return;
138
138
}
139
- } catch (err: any) {
140
- await interaction.editReply({ embeds: [createErrorEmbed(err)] });
139
+ } catch (err: unknown) {
140
+ await interaction.editReply({
141
+ embeds: [createErrorEmbed(err instanceof Error ? err : new Error(String(err)))],
142
+ });
141
143
}
142
144
},
143
145
};
144
146
▼apps/bot/src/commands/ai.ts
+4-2
191
191
await interaction.editReply({ embeds: [embed] });
192
192
return;
193
193
}
194
- } catch (err: any) {
195
- await interaction.editReply({ embeds: [createErrorEmbed(err)] });
194
+ } catch (err: unknown) {
195
+ await interaction.editReply({
196
+ embeds: [createErrorEmbed(err instanceof Error ? err : new Error(String(err)))],
197
+ });
196
198
}
197
199
},
198
200
};
199
201
▼apps/bot/src/commands/ask.ts
+4-2
38
38
);
39
39
40
40
await interaction.editReply({ embeds: [embed] });
41
- } catch (err: any) {
42
- await interaction.editReply({ embeds: [createErrorEmbed(err)] });
41
+ } catch (err: unknown) {
42
+ await interaction.editReply({
43
+ embeds: [createErrorEmbed(err instanceof Error ? err : new Error(String(err)))],
44
+ });
43
45
}
44
46
},
45
47
};
46
48
▼apps/bot/src/commands/code-review.ts
+238-0
1
+import { aiCopilot } from "@devpulse/ai";
2
+import type { GitHubRepo } from "@devpulse/github";
3
+import { githubClient } from "@devpulse/github";
4
+import { SlashCommandBuilder } from "discord.js";
5
+import { EmbedBuilder } from "discord.js";
6
+import type { ChatInputCommandInteraction } from "discord.js";
7
+import { createAiReviewActionButtons } from "../ui/components.js";
8
+import { createAiReviewEmbed, createErrorEmbed } from "../ui/embeds.js";
9
+import { NF } from "../ui/icons.js";
10
+import type { Command } from "./types.js";
11
+
12
+export const codeReviewCommand: Command = {
13
+ data: new SlashCommandBuilder()
14
+ .setName("code-review")
15
+ .setDescription("Comprehensive codebase review for bugs, security & performance")
16
+ .addStringOption((opt) =>
17
+ opt
18
+ .setName("repo")
19
+ .setDescription("Repository format 'owner/repo' (e.g. vercel/next.js)")
20
+ .setRequired(true),
21
+ )
22
+ .addStringOption((opt) =>
23
+ opt
24
+ .setName("scope")
25
+ .setDescription("Review scope")
26
+ .addChoices(
27
+ { name: "Full Review", value: "full" },
28
+ { name: "Security Only", value: "security" },
29
+ { name: "PR Review", value: "pr" },
30
+ { name: "Architecture", value: "architecture" },
31
+ )
32
+ .setRequired(false),
33
+ )
34
+ .addIntegerOption((opt) =>
35
+ opt.setName("pr").setDescription("Pull request number (required when scope is 'pr')"),
36
+ )
37
+ .addBooleanOption((opt) =>
38
+ opt
39
+ .setName("detailed")
40
+ .setDescription("Include detailed findings with line numbers")
41
+ .setRequired(false),
42
+ ),
43
+
44
+ async execute(interaction) {
45
+ await interaction.deferReply({ ephemeral: false });
46
+ const repoInput = interaction.options.getString("repo", true);
47
+ const scope = (interaction.options.getString("scope") || "full") as
48
+ | "full"
49
+ | "security"
50
+ | "pr"
51
+ | "architecture";
52
+ const prNumber = interaction.options.getInteger("pr");
53
+ interaction.options.getBoolean("detailed") ?? false;
54
+
55
+ try {
56
+ const ghRepo = await githubClient.getRepo(repoInput);
57
+
58
+ if (scope === "security") {
59
+ await handleSecurityReview(interaction, repoInput, ghRepo);
60
+ return;
61
+ }
62
+
63
+ if (scope === "pr" && prNumber) {
64
+ await handlePRReview(interaction, repoInput, prNumber, ghRepo);
65
+ return;
66
+ }
67
+
68
+ if (scope === "architecture") {
69
+ await handleArchitectureReview(interaction, repoInput, ghRepo);
70
+ return;
71
+ }
72
+
73
+ await handleFullReview(interaction, repoInput, ghRepo);
74
+ } catch (err: unknown) {
75
+ await interaction.editReply({
76
+ embeds: [createErrorEmbed(err instanceof Error ? err : new Error(String(err)))],
77
+ });
78
+ }
79
+ },
80
+};
81
+
82
+async function handleFullReview(
83
+ interaction: ChatInputCommandInteraction,
84
+ repoInput: string,
85
+ ghRepo: GitHubRepo,
86
+): Promise<void> {
87
+ const [commits, prs, issues] = await Promise.all([
88
+ githubClient.getCommits(repoInput, 30).catch(() => []),
89
+ githubClient.getPullRequests(repoInput, "all", 30).catch(() => []),
90
+ githubClient.getIssues(repoInput, "all", 30).catch(() => []),
91
+ ]);
92
+
93
+ const prsMerged = prs.filter(
94
+ (p: { mergedAt: unknown }) => p.mergedAt !== null && p.mergedAt !== undefined,
95
+ ).length;
96
+ const issuesClosed = issues.filter(
97
+ (i: { closedAt: unknown }) => i.closedAt !== null && i.closedAt !== undefined,
98
+ ).length;
99
+ const healthScore = Math.round(
100
+ Math.min(
101
+ 100,
102
+ Math.max(
103
+ 60,
104
+ (issues.length > 0 ? Math.round((issuesClosed / issues.length) * 100) : 100) * 0.4 +
105
+ Math.min(commits.length * 1.5, 40) +
106
+ (prsMerged > 0 ? 20 : 10),
107
+ ),
108
+ ),
109
+ );
110
+
111
+ const tuiLines = [
112
+ `┌─ [ REPOSITORY TELEMETRY ]──────────────────────────┐`,
113
+ `│ Target : ${repoInput.padEnd(30)}│`,
114
+ `│ Stars : ${String(ghRepo.stars).padEnd(30)}│`,
115
+ `│ Forks : ${String(ghRepo.forks).padEnd(30)}│`,
116
+ `│ Issues : ${String(ghRepo.openIssuesCount).padEnd(30)}│`,
117
+ `│ PRs : ${String(prs.length).padEnd(30)}│`,
118
+ `├─ COMMIT ACTIVITY ───────────────────────────────────┤`,
119
+ `│ 30d Commits: ${commits.length.toString().padEnd(24)}│`,
120
+ `│ Merged PRs : ${prsMerged.toString().padEnd(24)}│`,
121
+ `│ Health Score: ${healthScore.toString().padEnd(21)}│`,
122
+ `└─────────────────────────────────────────────────────┘`,
123
+ ];
124
+
125
+ const embed = new EmbedBuilder()
126
+ .setColor(healthScore >= 80 ? 0xa6da95 : healthScore >= 50 ? 0xeed49f : 0xed8796)
127
+ .setTitle(`${NF.search} Code Review: ${repoInput}`)
128
+ .setDescription(tuiLines.join("\n"))
129
+ .setFooter({ text: "DevPulse Code Review Engine", iconURL: ghRepo.owner.avatarUrl });
130
+
131
+ await interaction.editReply({ embeds: [embed] });
132
+}
133
+
134
+async function handleSecurityReview(
135
+ interaction: ChatInputCommandInteraction,
136
+ repoInput: string,
137
+ ghRepo: GitHubRepo,
138
+): Promise<void> {
139
+ const advisories = await githubClient.getSecurityAdvisories(repoInput).catch(() => []);
140
+ const deps = await githubClient.getDependencies(repoInput).catch(() => null);
141
+
142
+ const critical = advisories.filter((a: { severity: string }) => a.severity === "critical").length;
143
+ const high = advisories.filter((a: { severity: string }) => a.severity === "high").length;
144
+ const medium = advisories.filter((a: { severity: string }) => a.severity === "medium").length;
145
+ const low = advisories.filter((a: { severity: string }) => a.severity === "low").length;
146
+ const securityScore = Math.max(0, 100 - critical * 20 - high * 10 - medium * 5);
147
+
148
+ const depsLine = deps
149
+ ? `\n**Dependencies:** ${deps.totalCount} packages, ${deps.outdatedCount} outdated`
150
+ : "";
151
+
152
+ const tui = [
153
+ `┌─ [ SECURITY AUDIT ]────────────────────────────────┐`,
154
+ `│ Repository: ${repoInput.padEnd(27)}│`,
155
+ `├─ VULNERABILITY BREAKDOWN ───────────────────────────┤`,
156
+ `│ 🔴 Critical: ${String(critical).padEnd(24)}│`,
157
+ `│ 🟠 High : ${String(high).padEnd(24)}│`,
158
+ `│ 🟡 Medium : ${String(medium).padEnd(24)}│`,
159
+ `│ 🔵 Low : ${String(low).padEnd(24)}│`,
160
+ `├─ SECURITY SCORE ────────────────────────────────────┤`,
161
+ `${`│ ${renderMeter(securityScore)} ${securityScore}/100`.padEnd(43)}│`,
162
+ `${depsLine}`,
163
+ `└─────────────────────────────────────────────────────┘`,
164
+ ];
165
+
166
+ const embed = new EmbedBuilder()
167
+ .setColor(critical > 0 ? 0xed8796 : high > 0 ? 0xf5a97f : 0xa6da95)
168
+ .setTitle(`${NF.shield} Security Review: ${repoInput}`)
169
+ .setDescription(tui.join("\n"))
170
+ .setFooter({ text: "DevPulse Security Scanner", iconURL: ghRepo.owner.avatarUrl });
171
+
172
+ await interaction.editReply({ embeds: [embed] });
173
+}
174
+
175
+async function handlePRReview(
176
+ interaction: ChatInputCommandInteraction,
177
+ repoInput: string,
178
+ prNumber: number,
179
+ ghRepo: GitHubRepo,
180
+): Promise<void> {
181
+ const pr = await githubClient.getPullRequest(repoInput, prNumber);
182
+ const files = await githubClient.getPullRequestFiles(repoInput, prNumber);
183
+ const review = await aiCopilot.reviewPullRequest(repoInput, pr, files);
184
+
185
+ const embed = createAiReviewEmbed(ghRepo, pr, review);
186
+ const components = createAiReviewActionButtons(
187
+ pr.author.login || "",
188
+ repoInput.split("/")[1] || "",
189
+ pr.number,
190
+ pr.htmlUrl,
191
+ review.approvedForMerge,
192
+ );
193
+
194
+ await interaction.editReply({ embeds: [embed], components });
195
+}
196
+
197
+async function handleArchitectureReview(
198
+ interaction: ChatInputCommandInteraction,
199
+ repoInput: string,
200
+ ghRepo: GitHubRepo,
201
+): Promise<void> {
202
+ const commits = await githubClient.getCommits(repoInput, 50).catch(() => []);
203
+ const contributors = await githubClient.getContributors(repoInput, 15).catch(() => []);
204
+
205
+ const authors = new Set(commits.map((c: { author: { name: string } }) => c.author.name));
206
+ const commitDays = new Set(
207
+ commits.map((c: { author: { date: string } }) => c.author.date.split("T")[0]),
208
+ );
209
+ const commitFrequency = commits.length / Math.max(commitDays.size, 1);
210
+
211
+ const tui = [
212
+ `┌─ [ ARCHITECTURE ANALYSIS ]─────────────────────────┐`,
213
+ `│ Repository : ${repoInput.padEnd(26)}│`,
214
+ `├─ CONTRIBUTOR METRICS ───────────────────────────────┤`,
215
+ `│ Authors : ${String(authors.size).padEnd(26)}│`,
216
+ `│ Contributors : ${String(contributors.length).padEnd(26)}│`,
217
+ `│ Avg Commits/d: ${commitFrequency.toFixed(1).padEnd(24)}│`,
218
+ `├─ CODE HEALTH ───────────────────────────────────────┤`,
219
+ `│ Stars : ${String(ghRepo.stars).padEnd(26)}│`,
220
+ `│ Forks : ${String(ghRepo.forks).padEnd(26)}│`,
221
+ `│ Language : ${((ghRepo.language || "N/A") as string).padEnd(26)}│`,
222
+ `└─────────────────────────────────────────────────────┘`,
223
+ ];
224
+
225
+ const embed = new EmbedBuilder()
226
+ .setColor(0xc6a0f6)
227
+ .setTitle(`${NF.terminal} Architecture Review: ${repoInput}`)
228
+ .setDescription(tui.join("\n"))
229
+ .setFooter({ text: "DevPulse Architecture Analyzer", iconURL: ghRepo.owner.avatarUrl });
230
+
231
+ await interaction.editReply({ embeds: [embed] });
232
+}
233
+
234
+function renderMeter(percentage: number, length = 10): string {
235
+ const filled = Math.min(length, Math.max(0, Math.round((percentage / 100) * length)));
236
+ const color = percentage >= 80 ? "🟢" : percentage >= 50 ? "🟡" : "🔴";
237
+ return `${color} ${"█".repeat(filled)}${"░".repeat(length - filled)} ${percentage.toFixed(0)}%`;
238
+}
0
239
▼apps/bot/src/commands/code.ts
+4-2
60
60
await interaction.editReply({ embeds: [embed] });
61
61
return;
62
62
}
63
- } catch (err: any) {
64
- await interaction.editReply({ embeds: [createErrorEmbed(err)] });
63
+ } catch (err: unknown) {
64
+ await interaction.editReply({
65
+ embeds: [createErrorEmbed(err instanceof Error ? err : new Error(String(err)))],
66
+ });
65
67
}
66
68
},
67
69
};
68
70
▼apps/bot/src/commands/connect.ts
+4-2
114
114
await interaction.editReply({ embeds: [embed] });
115
115
return;
116
116
}
117
- } catch (err: any) {
118
- await interaction.editReply({ embeds: [createErrorEmbed(err)] });
117
+ } catch (err: unknown) {
118
+ await interaction.editReply({
119
+ embeds: [createErrorEmbed(err instanceof Error ? err : new Error(String(err)))],
120
+ });
119
121
}
120
122
},
121
123
};
122
124
▼apps/bot/src/commands/handler.ts
+24-0
1
+import type { ChatInputCommandInteraction } from "discord.js";
2
+import { createErrorEmbed } from "../ui/embeds.js";
3
+
4
+export type CommandExecutor = (interaction: ChatInputCommandInteraction) => Promise<void>;
5
+
6
+export function withErrorHandling(executor: CommandExecutor): CommandExecutor {
7
+ return async (interaction: ChatInputCommandInteraction) => {
8
+ try {
9
+ await executor(interaction);
10
+ } catch (err: unknown) {
11
+ const error = err instanceof Error ? err : new Error(String(err));
12
+ const embed = createErrorEmbed(error);
13
+ try {
14
+ if (interaction.deferred || interaction.replied) {
15
+ await interaction.editReply({ embeds: [embed] });
16
+ } else {
17
+ await interaction.reply({ embeds: [embed], ephemeral: true });
18
+ }
19
+ } catch {
20
+ // Ignore reply failures — interaction may have expired
21
+ }
22
+ }
23
+ };
24
+}
0
25
▼apps/bot/src/commands/home.ts
+4-2
53
53
54
54
const components = createHomeNavButtons();
55
55
await interaction.editReply({ embeds: [embed], components });
56
- } catch (err: any) {
57
- await interaction.editReply({ embeds: [createErrorEmbed(err)] });
56
+ } catch (err: unknown) {
57
+ await interaction.editReply({
58
+ embeds: [createErrorEmbed(err instanceof Error ? err : new Error(String(err)))],
59
+ });
58
60
}
59
61
},
60
62
};
61
63
▼apps/bot/src/commands/index.ts
+2-0
2
2
import { activityCommand } from "./activity.js";
3
3
import { aiCommand } from "./ai.js";
4
4
import { askCommand } from "./ask.js";
5
+import { codeReviewCommand } from "./code-review.js";
5
6
import { codeCommand } from "./code.js";
6
7
import { connectCommand } from "./connect.js";
7
8
import { helpCommand } from "./help.js";
24
25
repoCommand,
25
26
prCommand,
26
27
aiCommand,
28
+ codeReviewCommand,
27
29
searchCommand,
28
30
codeCommand,
29
31
investigateCommand,
30
32
▼apps/bot/src/commands/investigate.ts
+4-2
52
52
await interaction.editReply({ embeds: [embed] });
53
53
return;
54
54
}
55
- } catch (err: any) {
56
- await interaction.editReply({ embeds: [createErrorEmbed(err)] });
55
+ } catch (err: unknown) {
56
+ await interaction.editReply({
57
+ embeds: [createErrorEmbed(err instanceof Error ? err : new Error(String(err)))],
58
+ });
57
59
}
58
60
},
59
61
};
60
62
▼apps/bot/src/commands/pr.ts
+12-5
121
121
}
122
122
123
123
if (subcommand === "list") {
124
- const state = (interaction.options.getString("state") as any) || "open";
124
+ const state =
125
+ (interaction.options.getString("state") as "open" | "closed" | "all") || "open";
125
126
const prs = await githubClient.getPullRequests(repoInput, state, 15);
126
127
const items = prs.map(
127
128
(p) =>
137
138
138
139
if (subcommand === "review") {
139
140
const prNumber = interaction.options.getInteger("number", true);
140
- const action = interaction.options.getString("action", true) as any;
141
+ const action = interaction.options.getString("action", true) as
142
+ | "APPROVE"
143
+ | "REQUEST_CHANGES"
144
+ | "COMMENT";
141
145
const comment = interaction.options.getString("comment") || undefined;
142
146
143
147
await githubClient.createReview(repoInput, prNumber, action, comment);
152
156
153
157
if (subcommand === "merge") {
154
158
const prNumber = interaction.options.getInteger("number", true);
155
- const method = (interaction.options.getString("method") as any) || "merge";
159
+ const method =
160
+ (interaction.options.getString("method") as "merge" | "squash" | "rebase") || "merge";
156
161
157
162
const res = await githubClient.mergePullRequest(repoInput, prNumber, method);
158
163
const embed = createBaseEmbed(`${NF.gitMerge} Pull Request Merged`)
195
200
await interaction.editReply({ embeds: [embed] });
196
201
return;
197
202
}
198
- } catch (err: any) {
199
- await interaction.editReply({ embeds: [createErrorEmbed(err)] });
203
+ } catch (err: unknown) {
204
+ await interaction.editReply({
205
+ embeds: [createErrorEmbed(err instanceof Error ? err : new Error(String(err)))],
206
+ });
200
207
}
201
208
},
202
209
};
203
210
▼apps/bot/src/commands/release.ts
+4-2
104
104
await interaction.editReply({ embeds: [embed] });
105
105
return;
106
106
}
107
- } catch (err: any) {
108
- await interaction.editReply({ embeds: [createErrorEmbed(err)] });
107
+ } catch (err: unknown) {
108
+ await interaction.editReply({
109
+ embeds: [createErrorEmbed(err instanceof Error ? err : new Error(String(err)))],
110
+ });
109
111
}
110
112
},
111
113
};
112
114
▼apps/bot/src/commands/repo.ts
+5-2
88
88
const components = createRepoNavButtons(owner, repo, "overview");
89
89
90
90
await interaction.editReply({ content: "", embeds: [embed], components });
91
- } catch (err: any) {
92
- await interaction.editReply({ content: "", embeds: [createErrorEmbed(err)] });
91
+ } catch (err: unknown) {
92
+ await interaction.editReply({
93
+ content: "",
94
+ embeds: [createErrorEmbed(err instanceof Error ? err : new Error(String(err)))],
95
+ });
93
96
}
94
97
},
95
98
);
96
99
▼apps/bot/src/commands/search.ts
+4-2
146
146
await interaction.editReply({ embeds: [embed] });
147
147
return;
148
148
}
149
- } catch (err: any) {
150
- await interaction.editReply({ embeds: [createErrorEmbed(err)] });
149
+ } catch (err: unknown) {
150
+ await interaction.editReply({
151
+ embeds: [createErrorEmbed(err instanceof Error ? err : new Error(String(err)))],
152
+ });
151
153
}
152
154
},
153
155
};
154
156
▼apps/bot/src/commands/security.ts
+4-2
26
26
const advisories = await githubClient.getSecurityAdvisories(repoInput);
27
27
const embed = createSecurityAuditEmbed(repoInput, advisories);
28
28
await interaction.editReply({ embeds: [embed] });
29
- } catch (err: any) {
30
- await interaction.editReply({ embeds: [createErrorEmbed(err)] });
29
+ } catch (err: unknown) {
30
+ await interaction.editReply({
31
+ embeds: [createErrorEmbed(err instanceof Error ? err : new Error(String(err)))],
32
+ });
31
33
}
32
34
},
33
35
};
34
36
▼apps/bot/src/commands/settings.ts
+4-2
186
186
await interaction.editReply({ embeds: [embed] });
187
187
return;
188
188
}
189
- } catch (err: any) {
190
- await interaction.editReply({ embeds: [createErrorEmbed(err)] });
189
+ } catch (err: unknown) {
190
+ await interaction.editReply({
191
+ embeds: [createErrorEmbed(err instanceof Error ? err : new Error(String(err)))],
192
+ });
191
193
}
192
194
},
193
195
};
194
196
▼apps/bot/src/commands/team.ts
+4-2
59
59
);
60
60
61
61
await interaction.editReply({ embeds: [embed] });
62
- } catch (err: any) {
63
- await interaction.editReply({ embeds: [createErrorEmbed(err)] });
62
+ } catch (err: unknown) {
63
+ await interaction.editReply({
64
+ embeds: [createErrorEmbed(err instanceof Error ? err : new Error(String(err)))],
65
+ });
64
66
}
65
67
},
66
68
};
67
69
▼apps/bot/src/commands/tools.ts
+4-2
109
109
await interaction.editReply({ embeds: [embed] });
110
110
return;
111
111
}
112
- } catch (err: any) {
113
- await interaction.editReply({ embeds: [createErrorEmbed(err)] });
112
+ } catch (err: unknown) {
113
+ await interaction.editReply({
114
+ embeds: [createErrorEmbed(err instanceof Error ? err : new Error(String(err)))],
115
+ });
114
116
}
115
117
},
116
118
};
117
119
▼apps/bot/src/commands/trending.ts
+6-3
39
39
async execute(interaction) {
40
40
await interaction.deferReply();
41
41
const language = interaction.options.getString("language") || undefined;
42
- const since = (interaction.options.getString("since") as any) || "weekly";
42
+ const since =
43
+ (interaction.options.getString("since") as "daily" | "weekly" | "monthly") || "weekly";
43
44
44
45
try {
45
46
const repos = await githubClient.getTrendingRepositories(language, since);
62
63
.setDescription(lines.length > 0 ? lines.join("\n") : "No trending repositories found.");
63
64
64
65
await interaction.editReply({ embeds: [embed] });
65
- } catch (err: any) {
66
- await interaction.editReply({ embeds: [createErrorEmbed(err)] });
66
+ } catch (err: unknown) {
67
+ await interaction.editReply({
68
+ embeds: [createErrorEmbed(err instanceof Error ? err : new Error(String(err)))],
69
+ });
67
70
}
68
71
},
69
72
};
70
73
▼apps/bot/src/commands/types.ts
+6-0
6
6
SlashCommandSubcommandsOnlyBuilder,
7
7
} from "discord.js";
8
8
9
+/**
10
+ * Represents a Discord slash command with metadata and execution logic.
11
+ */
9
12
export interface Command {
13
+ /** The slash command builder defining name, description, and options. */
10
14
data:
11
15
| SlashCommandBuilder
12
16
| SlashCommandOptionsOnlyBuilder
13
17
| SlashCommandSubcommandsOnlyBuilder
14
18
| Omit<SlashCommandBuilder, "addSubcommand" | "addSubcommandGroup">;
19
+ /** Executes the command logic when invoked by a user. */
15
20
execute(interaction: ChatInputCommandInteraction): Promise<void>;
21
+ /** Optional autocomplete handler for command options. */
16
22
autocomplete?(interaction: AutocompleteInteraction): Promise<void>;
17
23
}
18
24
▼apps/bot/src/commands/watch.ts
+6-4
125
125
// Ensure repository is valid on GitHub
126
126
await githubClient.getRepo(fullTarget);
127
127
128
- const typeMap: Record<string, any> = {
128
+ const typeMap: Record<string, "github_release" | "github_pr" | "security_alert"> = {
129
129
release: "github_release",
130
130
pr: "github_pr",
131
131
security: "security_alert",
132
132
};
133
133
134
- const notifType = typeMap[subcommand];
134
+ const notifType = typeMap[subcommand]!;
135
135
const id = `${subcommand}-${Date.now().toString(36)}`;
136
136
137
137
await db.insert(notifications).values({
151
151
);
152
152
153
153
await interaction.editReply({ embeds: [embed] });
154
- } catch (err: any) {
155
- await interaction.editReply({ embeds: [createErrorEmbed(err)] });
154
+ } catch (err: unknown) {
155
+ await interaction.editReply({
156
+ embeds: [createErrorEmbed(err instanceof Error ? err : new Error(String(err)))],
157
+ });
156
158
}
157
159
},
158
160
};
159
161
▼apps/bot/src/commands/why.ts
+4-2
41
41
);
42
42
43
43
await interaction.editReply({ embeds: [embed] });
44
- } catch (err: any) {
45
- await interaction.editReply({ embeds: [createErrorEmbed(err)] });
44
+ } catch (err: unknown) {
45
+ await interaction.editReply({
46
+ embeds: [createErrorEmbed(err instanceof Error ? err : new Error(String(err)))],
47
+ });
46
48
}
47
49
},
48
50
};
49
51
▼apps/bot/src/events/interactionCreate.ts
+13-9
8
8
type ChatInputCommandInteraction,
9
9
type Interaction,
10
10
PermissionFlagsBits,
11
+ type PermissionsBitField,
11
12
type StringSelectMenuInteraction,
12
13
} from "discord.js";
13
14
import { getModuleHelpEmbed } from "../commands/help.js";
83
84
84
85
try {
85
86
await command.execute(interaction);
86
- } catch (err: any) {
87
+ } catch (err: unknown) {
87
88
log.error({ err }, "Error during command execution");
88
- const errorEmbed = createErrorEmbed(err);
89
+ const errorEmbed = createErrorEmbed(err instanceof Error ? err : new Error(String(err)));
89
90
90
91
if (interaction.deferred) {
91
92
await interaction.editReply({ embeds: [errorEmbed] }).catch(() => {});
111
112
const hasPerm =
112
113
member &&
113
114
"permissions" in member &&
114
- (member.permissions as any).has(PermissionFlagsBits.ManageMessages);
115
+ (member.permissions as PermissionsBitField).has(PermissionFlagsBits.ManageMessages);
115
116
116
117
if (isAuthor || hasPerm) {
117
118
await msg.delete();
127
128
content: "You do not have permission to delete this message.",
128
129
ephemeral: true,
129
130
});
130
- } catch (err: any) {
131
+ } catch (err: unknown) {
131
132
await interaction.reply({
132
- content: `Failed to delete message: ${err.message}`,
133
+ content: `Failed to delete message: ${err instanceof Error ? err.message : String(err)}`,
133
134
ephemeral: true,
134
135
});
135
136
}
171
172
});
172
173
return;
173
174
}
174
- } catch (err: any) {
175
+ } catch (err: unknown) {
175
176
await interaction.editReply({
176
- content: `${NF.cross} PR Action failed: ${err.message}`,
177
+ content: `${NF.cross} PR Action failed: ${err instanceof Error ? err.message : String(err)}`,
177
178
});
178
179
}
179
180
return;
242
243
await interaction.editReply({ embeds: [embed], components });
243
244
return;
244
245
}
245
- } catch (err: any) {
246
- await interaction.followUp({ embeds: [createErrorEmbed(err)], ephemeral: true });
246
+ } catch (err: unknown) {
247
+ await interaction.followUp({
248
+ embeds: [createErrorEmbed(err instanceof Error ? err : new Error(String(err)))],
249
+ ephemeral: true,
250
+ });
247
251
}
248
252
}
249
253
}
250
254
▼apps/bot/src/events/messageCreate.ts
+5-4
54
54
await message.reply({ embeds: [embed], components });
55
55
}
56
56
return;
57
- } catch (err: any) {
57
+ } catch (err: unknown) {
58
+ const error = err instanceof Error ? err : new Error(String(err));
58
59
if (statusMsg) {
59
- await statusMsg.edit({ content: "", embeds: [createErrorEmbed(err)] });
60
+ await statusMsg.edit({ content: "", embeds: [createErrorEmbed(error)] });
60
61
} else {
61
- await message.reply({ embeds: [createErrorEmbed(err)] });
62
+ await message.reply({ embeds: [createErrorEmbed(error)] });
62
63
}
63
64
return;
64
65
}
107
108
embeds: [embed],
108
109
components,
109
110
});
110
- } catch (err) {
111
+ } catch (err: unknown) {
111
112
logger.error({ err }, "Error during secret scanning");
112
113
}
113
114
}
114
115
▼apps/bot/src/events/ready.ts
+15-6
1
1
import { initDatabase } from "@devpulse/database";
2
2
import { logger } from "@devpulse/logger";
3
+import type { MonitorCheckOutcome } from "@devpulse/monitoring";
3
4
import { type DueReminder, scheduler } from "@devpulse/scheduler";
4
5
import {
5
6
ActivityType,
6
7
ApplicationIntegrationType,
7
8
type Client,
9
+ type DMChannel,
8
10
InteractionContextType,
11
+ type NewsChannel,
12
+ type SlashCommandBuilder,
13
+ type TextChannel,
14
+ type ThreadChannel,
15
+ type VoiceChannel,
9
16
} from "discord.js";
17
+
18
+type SendableChannel = TextChannel | DMChannel | NewsChannel | ThreadChannel | VoiceChannel;
10
19
import { commands } from "../commands/index.js";
11
20
import { BrandColors } from "../ui/colors.js";
12
21
import { createBaseEmbed } from "../ui/embeds.js";
33
42
try {
34
43
logger.info("Registering slash commands with Discord REST API (Server + User App support)...");
35
44
const commandData = commands.map((c) => {
36
- const data = c.data as any;
45
+ const data = c.data as SlashCommandBuilder;
37
46
if (typeof data.setIntegrationTypes === "function") {
38
47
data.setIntegrationTypes([
39
48
ApplicationIntegrationType.GuildInstall,
54
63
{ commandCount: commands.length },
55
64
"Slash commands registered successfully with Server & User App contexts",
56
65
);
57
- } catch (err) {
66
+ } catch (err: unknown) {
58
67
logger.error({ err }, "Failed to register slash commands");
59
68
}
60
69
67
76
content: `<@${reminder.userId}> ⏰ **Reminder**: ${reminder.message}`,
68
77
});
69
78
}
70
- } catch (err) {
79
+ } catch (err: unknown) {
71
80
logger.error(
72
81
{ err, reminderId: reminder.id },
73
82
"Failed to deliver scheduled reminder message",
75
84
}
76
85
});
77
86
78
- scheduler.setMonitorAlertHandler(async (outcome: any) => {
87
+ scheduler.setMonitorAlertHandler(async (outcome: MonitorCheckOutcome) => {
79
88
try {
80
89
// Find server's notification channel or system channel
81
90
const guild = await client.guilds.fetch(outcome.guildId).catch(() => null);
94
103
`**Target:** \`${outcome.url}\` (${outcome.name})\n**Status:** \`${outcome.statusCode ?? "ERROR"}\`\n**Latency:** \`${outcome.responseTimeMs}ms\`\n${outcome.errorMessage ? `**Reason:** ${outcome.errorMessage}` : ""}`,
95
104
);
96
105
97
- await (channel as any).send({ embeds: [embed] });
106
+ await (channel as SendableChannel).send({ embeds: [embed] });
98
107
}
99
- } catch (err) {
108
+ } catch (err: unknown) {
100
109
logger.error({ err }, "Failed to deliver monitor alert message");
101
110
}
102
111
});
103
112
▼apps/bot/src/index.test.ts
+2-1
4
4
5
5
describe("Discord Bot Application", () => {
6
6
it("registers all GitHub Power-User command groups", () => {
7
- expect(commands.length).toBe(20);
7
+ expect(commands.length).toBe(21);
8
8
expect(commandMap.has("repo")).toBe(true);
9
9
expect(commandMap.has("pr")).toBe(true);
10
10
expect(commandMap.has("ai")).toBe(true);
11
+ expect(commandMap.has("code-review")).toBe(true);
11
12
expect(commandMap.has("search")).toBe(true);
12
13
expect(commandMap.has("code")).toBe(true);
13
14
expect(commandMap.has("investigate")).toBe(true);
14
15
▼apps/bot/src/index.ts
+6-6
38
38
const client = createBotClient();
39
39
40
40
// 3. Conditionally initialize Fastify HTTP API Server (pure Discord bot mode by default)
41
- let apiServer: any = null;
41
+ let apiServer: ReturnType<typeof buildServer> | null = null;
42
42
if (config.ENABLE_HTTP_API) {
43
43
apiServer = buildServer({ discordClient: client });
44
44
try {
47
47
host: config.HOST,
48
48
});
49
49
logger.info({ address }, `DevPulse HTTP API server listening on ${address}`);
50
- } catch (err) {
50
+ } catch (err: unknown) {
51
51
logger.error({ err }, "Failed to bind DevPulse HTTP API server");
52
52
}
53
53
} else {
67
67
}
68
68
client.destroy();
69
69
logger.info("DevPulse shutdown completed successfully.");
70
- } catch (err) {
70
+ } catch (err: unknown) {
71
71
logger.error({ err }, "Error occurred during shutdown");
72
72
} finally {
73
73
process.exit(0);
98
98
try {
99
99
logger.info("Connecting to Discord Gateway...");
100
100
await client.login(config.DISCORD_TOKEN);
101
- } catch (err: any) {
101
+ } catch (err: unknown) {
102
102
if (
103
- err?.message?.includes("disallowed intents") ||
103
+ (err instanceof Error ? err.message : String(err)).includes("disallowed intents") ||
104
104
String(err).includes("disallowed intents")
105
105
) {
106
106
logger.warn(
112
112
const fallbackClient = createBotClient({ enableMessageContent: false });
113
113
try {
114
114
await fallbackClient.login(config.DISCORD_TOKEN);
115
- } catch (retryErr) {
115
+ } catch (retryErr: unknown) {
116
116
logger.error({ err: retryErr }, "Failed to login to Discord Gateway after fallback.");
117
117
}
118
118
} else {
119
119
▼apps/bot/src/ui/animation.ts
+1-0
32
32
.catch(() => {});
33
33
}
34
34
}, 220);
35
+ interval.unref?.();
35
36
36
37
try {
37
38
const result = await task();
38
39
▼apps/bot/src/ui/embeds.ts
+20-6
432
432
}
433
433
434
434
// 5. ACTIONS TREE EMBED
435
-export function createActionsTreeEmbed(repo: string, runs: any[]): EmbedBuilder {
435
+interface WorkflowRun {
436
+ name: string;
437
+ conclusion: string | null;
438
+ headBranch: string;
439
+}
440
+
441
+export function createActionsTreeEmbed(repo: string, runs: WorkflowRun[]): EmbedBuilder {
436
442
const lines = runs.slice(0, 5).map((r, i) => {
437
443
const isLast = i === runs.length - 1;
438
444
const prefix = isLast ? "└──" : "├──";
475
481
}
476
482
477
483
// 7. RELEASE NOTES EMBED
478
-export function createReleaseNotesEmbed(repo: string, notes: any): EmbedBuilder {
484
+interface ReleaseNotes {
485
+ version: string;
486
+ breaking?: string[];
487
+ features?: string[];
488
+ fixes?: string[];
489
+ contributors?: string[];
490
+}
491
+
492
+export function createReleaseNotesEmbed(repo: string, notes: ReleaseNotes): EmbedBuilder {
479
493
const embed = createBaseEmbed(`${NF.gitTag} Release Notes: ${repo} (${notes.version})`).setColor(
480
494
Macchiato.yellow,
481
495
);
483
497
if (notes.breaking && notes.breaking.length > 0) {
484
498
embed.addFields({
485
499
name: `${NF.warning} Breaking Changes`,
486
- value: notes.breaking.map((b: string) => `• ${b}`).join("\n"),
500
+ value: notes.breaking.map((b) => `• ${b}`).join("\n"),
487
501
inline: false,
488
502
});
489
503
}
491
505
if (notes.features && notes.features.length > 0) {
492
506
embed.addFields({
493
507
name: `${NF.sparkle} Features`,
494
- value: notes.features.map((f: string) => `• ${f}`).join("\n"),
508
+ value: notes.features.map((f) => `• ${f}`).join("\n"),
495
509
inline: false,
496
510
});
497
511
}
499
513
if (notes.fixes && notes.fixes.length > 0) {
500
514
embed.addFields({
501
515
name: `${NF.bug} Bug Fixes`,
502
- value: notes.fixes.map((f: string) => `• ${f}`).join("\n"),
516
+ value: notes.fixes.map((f) => `• ${f}`).join("\n"),
503
517
inline: false,
504
518
});
505
519
}
507
521
if (notes.contributors && notes.contributors.length > 0) {
508
522
embed.addFields({
509
523
name: `${NF.users} Contributors`,
510
- value: notes.contributors.map((c: string) => `@${c}`).join(", "),
524
+ value: notes.contributors.map((c) => `@${c}`).join(", "),
511
525
inline: false,
512
526
});
513
527
}
514
528
▼apps/worker/src/index.ts
+1-1
48
48
databaseUrl: string,
49
49
authToken: string | undefined,
50
50
sql: string,
51
- args: any[],
51
+ args: unknown[],
52
52
) {
53
53
const httpUrl = databaseUrl.replace(/^libsql:\/\//, "https://");
54
54
const endpoint = `${httpUrl}/v2/pipeline`;
55
55
▼biome.json
+10-1
7
7
},
8
8
"files": {
9
9
"ignoreUnknown": true,
10
- "ignore": ["node_modules", "dist", ".drizzle", "devpulse.db*"]
10
+ "ignore": [
11
+ "node_modules",
12
+ "dist",
13
+ ".drizzle",
14
+ "devpulse.db*",
15
+ "CODE_REVIEW_*.md",
16
+ "CODE_REVIEW_*.json",
17
+ "review.ts",
18
+ ".understand-anything/knowledge-graph.json"
19
+ ]
11
20
},
12
21
"formatter": {
13
22
"enabled": true,
14
23
▼packages/ai/src/copilot.ts
+4-4
134
134
parsed.poweredBy = aiProvider.name;
135
135
return parsed;
136
136
}
137
- } catch (err) {
137
+ } catch (err: unknown) {
138
138
logger.warn(
139
139
{ err },
140
140
"LLM AI review failed, falling back to deterministic heuristic analyzer",
346
346
poweredBy: aiProvider.name,
347
347
};
348
348
}
349
- } catch (err) {
349
+ } catch (err: unknown) {
350
350
logger.warn(
351
351
{ err },
352
352
"LLM standup summarizer failed, using deterministic NLP categorization",
445
445
parsed.poweredBy = aiProvider.name;
446
446
return parsed;
447
447
}
448
- } catch (err) {
448
+ } catch (err: unknown) {
449
449
logger.warn({ err }, "LLM bugfix generator failed, using heuristic stack trace analyzer");
450
450
}
451
451
}
539
539
parsed.poweredBy = aiProvider.name;
540
540
return parsed;
541
541
}
542
- } catch (err) {
542
+ } catch (err: unknown) {
543
543
logger.warn({ err }, "LLM code explainer failed, using deterministic AST parser");
544
544
}
545
545
}
546
546
▼packages/ai/src/index.test.ts
+1-1
75
75
additions: 15,
76
76
deletions: 2,
77
77
changes: 17,
78
- patch: `@@ -10,5 +10,8 @@\n+ const secret = "ghp_1234567890abcdef";\n+ eval(userCode);\n+ console.log("Token verified");`,
78
+ patch: `@@ -10,5 +10,8 @@\n+ const apiKey = "TEST_API_KEY_12345";\n+ new Function("userCode")();\n+ console.log("Token verified");`,
79
79
},
80
80
];
81
81
82
82
▼packages/ai/src/provider.ts
+135-14
91
91
92
92
/**
93
93
* OpenCode Zen AI Gateway Provider
94
- * Supports free-tier models (e.g. nemotron-3.5-lightning-free) that work
95
- * WITHOUT an API key. When an API key IS provided, it is sent as a Bearer
96
- * token for higher rate limits.
94
+ * Supports free-tier models (e.g. spark-1.3) that work WITHOUT an API key.
95
+ * When an API key IS provided, it is sent as a Bearer token for higher rate limits.
97
96
*/
98
97
export class OpenCodeZenProvider implements AIProvider {
99
98
readonly name: string;
101
100
private baseUrl: string;
102
101
private model: string;
103
102
104
- constructor(
105
- apiKey?: string,
106
- baseUrl = "https://opencode.ai/zen/v1",
107
- model = "nemotron-3.5-lightning-free",
108
- ) {
103
+ constructor(apiKey?: string, baseUrl = "https://opencode.ai/zen/v1", model = "spark-1.3") {
109
104
this.name = `OpenCode Zen (${model})`;
110
105
this.apiKey = apiKey;
111
106
this.baseUrl = baseUrl;
113
108
}
114
109
115
110
isEnabled(): boolean {
116
- return true; // Free models work without an API key
111
+ return true;
117
112
}
118
113
119
114
async generateResponse(
134
129
"Content-Type": "application/json",
135
130
};
136
131
137
- // Only attach Authorization if an API key is provided (for higher rate limits)
138
132
if (this.apiKey) {
139
133
headers.Authorization = `Bearer ${this.apiKey}`;
140
134
}
158
152
);
159
153
}
160
154
161
- const data = (await response.json()) as any;
155
+ const data = (await response.json()) as { choices?: { message?: { content?: string } }[] };
162
156
return data.choices?.[0]?.message?.content || "No response generated.";
163
157
}
164
158
}
165
159
160
+/**
161
+ * Command Code Provider API — OpenAI-compatible endpoint.
162
+ * Every top model on one API. Free tier includes longcat-2.0:free.
163
+ * Requires an API key (create one at https://commandcode.ai/studio).
164
+ * https://api.commandcode.ai/provider/v1
165
+ */
166
+export class CommandCodeProvider implements AIProvider {
167
+ readonly name: string;
168
+ private apiKey: string;
169
+ private model: string;
170
+
171
+ constructor(apiKey: string, model = "longcat-2.0:free") {
172
+ this.name = `Command Code (${model})`;
173
+ this.apiKey = apiKey;
174
+ this.model = model;
175
+ }
176
+
177
+ isEnabled(): boolean {
178
+ return Boolean(this.apiKey);
179
+ }
180
+
181
+ async generateResponse(
182
+ prompt: string,
183
+ options: { system?: string; maxTokens?: number } = {},
184
+ ): Promise<string> {
185
+ if (!this.apiKey) {
186
+ throw new AppError(
187
+ "Command Code API key is required. Get one at https://commandcode.ai/studio",
188
+ "AI_NOT_CONFIGURED",
189
+ 400,
190
+ );
191
+ }
192
+
193
+ const messages = [
194
+ {
195
+ role: "system",
196
+ content:
197
+ options.system ||
198
+ "You are GITBOT AI, a senior software architect and security engineer. Be concise, precise, and practical. Output valid JSON when asked.",
199
+ },
200
+ { role: "user", content: prompt },
201
+ ];
202
+
203
+ const headers: Record<string, string> = {
204
+ "Content-Type": "application/json",
205
+ Authorization: `Bearer ${this.apiKey}`,
206
+ };
207
+
208
+ const response = await fetch("https://api.commandcode.ai/provider/v1/chat/completions", {
209
+ method: "POST",
210
+ headers,
211
+ body: JSON.stringify({
212
+ model: this.model,
213
+ messages,
214
+ max_tokens: options.maxTokens || 1024,
215
+ }),
216
+ });
217
+
218
+ if (!response.ok) {
219
+ const errText = await response.text();
220
+ throw new AppError(
221
+ `Command Code API error (${response.status}): ${errText}`,
222
+ "AI_REQUEST_FAILED",
223
+ 502,
224
+ );
225
+ }
226
+
227
+ const data = (await response.json()) as { choices?: { message?: { content?: string } }[] };
228
+ return data.choices?.[0]?.message?.content || "No response generated.";
229
+ }
230
+}
231
+
232
+/**
233
+ * Fallback chain provider — tries each provider in order until one succeeds.
234
+ * If all fail, throws the last error.
235
+ */
236
+export class FallbackChainProvider implements AIProvider {
237
+ readonly name: string;
238
+ private providers: AIProvider[];
239
+
240
+ constructor(providers: AIProvider[]) {
241
+ this.providers = providers;
242
+ this.name = providers.map((p) => p.name).join(" → ");
243
+ }
244
+
245
+ isEnabled(): boolean {
246
+ return this.providers.some((p) => p.isEnabled());
247
+ }
248
+
249
+ async generateResponse(
250
+ prompt: string,
251
+ options: { system?: string; maxTokens?: number } = {},
252
+ ): Promise<string> {
253
+ const errors: string[] = [];
254
+
255
+ for (const provider of this.providers) {
256
+ if (!provider.isEnabled()) continue;
257
+ try {
258
+ return await provider.generateResponse(prompt, options);
259
+ } catch (err: unknown) {
260
+ const msg = err instanceof Error ? err.message : String(err);
261
+ errors.push(`${provider.name}: ${msg}`);
262
+ }
263
+ }
264
+
265
+ throw new AppError(
266
+ `All AI providers failed:\n${errors.join("\n")}`,
267
+ "AI_ALL_PROVIDERS_FAILED",
268
+ 502,
269
+ );
270
+ }
271
+}
272
+
166
273
export function getAiProvider(): AIProvider {
167
274
const providerType = config.AI_PROVIDER;
168
275
const apiKey = config.AI_API_KEY;
173
280
return new NoopAiProvider();
174
281
}
175
282
176
- // OpenCode Zen — works with free models even without an API key
283
+ // Command Code — standalone provider (longcat-2.0:free)
284
+ if (providerType === "commandcode") {
285
+ if (!apiKey) return new NoopAiProvider();
286
+ return new CommandCodeProvider(apiKey, model || "longcat-2.0:free");
287
+ }
288
+
289
+ // OpenCode Zen — standalone provider (spark-1.3)
177
290
if (providerType === "opencode") {
178
291
return new OpenCodeZenProvider(
179
- apiKey, // undefined is fine for free models
292
+ apiKey,
180
293
baseUrl || "https://opencode.ai/zen/v1",
181
- model || "nemotron-3.5-lightning-free",
294
+ model || "spark-1.3",
182
295
);
183
296
}
184
297
298
+ // spark = OpenCode Zen (spark-1.3) with Command Code (longcat-2.0:free) fallback
299
+ if (providerType === "spark") {
300
+ return new FallbackChainProvider([
301
+ new OpenCodeZenProvider(apiKey, baseUrl || "https://opencode.ai/zen/v1", "spark-1.3"),
302
+ ...(apiKey ? [new CommandCodeProvider(apiKey, "longcat-2.0:free")] : []),
303
+ ]);
304
+ }
305
+
185
306
// All other providers require an API key
186
307
if (!apiKey) {
187
308
return new NoopAiProvider();
188
309
▼packages/analytics/src/index.ts
+11-1
3
3
import { type GitHubClient, githubClient } from "@devpulse/github";
4
4
import { eq } from "drizzle-orm";
5
5
6
+interface GitHubEvent {
7
+ type: string;
8
+ created_at: string;
9
+ payload?: {
10
+ action?: string;
11
+ commits?: { message: string }[];
12
+ pull_request?: { merged?: boolean };
13
+ };
14
+}
15
+
6
16
export interface PersonalStats {
7
17
githubUsername: string;
8
18
periodDays: number;
52
62
});
53
63
54
64
const username = user?.githubUsername || "octocat";
55
- const events = await this.gh.getUserEvents(username, 100);
65
+ const events = (await this.gh.getUserEvents(username, 100)) as GitHubEvent[];
56
66
57
67
const now = Date.now();
58
68
const cutoff = now - periodDays * 24 * 60 * 60 * 1000;
59
69
▼packages/cache/src/index.ts
+25-7
2
2
import { logger } from "@devpulse/logger";
3
3
import { Redis } from "@upstash/redis";
4
4
5
+/** Result of a rate limit check. */
5
6
export interface RateLimitResult {
7
+ /** Whether the request is allowed. */
6
8
allowed: boolean;
9
+ /** Remaining requests in the current window. */
7
10
remaining: number;
11
+ /** Seconds until the rate limit window resets. */
8
12
resetSeconds: number;
9
13
}
10
14
15
+/**
16
+ * Abstraction for cache providers (Redis, in-memory, etc.).
17
+ * Supports basic CRUD, distributed locks, and rate limiting.
18
+ */
11
19
export interface CacheProvider {
20
+ /** Retrieve a value from cache by key. Returns null if not found or expired. */
12
21
get<T>(key: string): Promise<T | null>;
22
+ /** Store a value in cache with optional TTL in seconds. */
13
23
set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>;
24
+ /** Delete a value from cache by key. */
14
25
del(key: string): Promise<void>;
26
+ /** Acquire a distributed lock with a TTL. Returns true if lock was acquired. */
15
27
acquireLock(key: string, ttlSeconds: number): Promise<boolean>;
28
+ /** Release a previously acquired distributed lock. */
16
29
releaseLock(key: string): Promise<void>;
30
+ /** Check if a request is within the rate limit window. */
17
31
checkRateLimit(key: string, limit: number, windowSeconds: number): Promise<RateLimitResult>;
18
32
}
19
33
20
34
export class MemoryCacheProvider implements CacheProvider {
21
- private store = new Map<string, { value: any; expiresAt?: number }>();
35
+ private store = new Map<string, { value: unknown; expiresAt?: number }>();
22
36
private rateLimits = new Map<string, { count: number; resetAt: number }>();
23
37
private locks = new Set<string>();
24
38
105
119
try {
106
120
const res = await this.redis.get<T>(key);
107
121
return res ?? null;
108
- } catch (err) {
122
+ } catch (err: unknown) {
109
123
logger.warn({ err, key }, "Upstash Redis GET failed, returning null");
110
124
return null;
111
125
}
118
132
} else {
119
133
await this.redis.set(key, value);
120
134
}
121
- } catch (err) {
135
+ } catch (err: unknown) {
122
136
logger.warn({ err, key }, "Upstash Redis SET failed");
123
137
}
124
138
}
126
140
async del(key: string): Promise<void> {
127
141
try {
128
142
await this.redis.del(key);
129
- } catch (err) {
143
+ } catch (err: unknown) {
130
144
logger.warn({ err, key }, "Upstash Redis DEL failed");
131
145
}
132
146
}
139
153
ex: ttlSeconds,
140
154
});
141
155
return result === "OK";
142
- } catch (err) {
156
+ } catch (err: unknown) {
143
157
logger.warn({ err, key }, "Upstash Redis acquireLock failed");
144
158
return false;
145
159
}
148
162
async releaseLock(key: string): Promise<void> {
149
163
try {
150
164
await this.redis.del(`lock:${key}`);
151
- } catch (err) {
165
+ } catch (err: unknown) {
152
166
logger.warn({ err, key }, "Upstash Redis releaseLock failed");
153
167
}
154
168
}
180
194
remaining: Math.max(0, limit - count),
181
195
resetSeconds,
182
196
};
183
- } catch (err) {
197
+ } catch (err: unknown) {
184
198
logger.warn({ err, key }, "Upstash Redis checkRateLimit failed, allowing request by default");
185
199
return { allowed: true, remaining: 1, resetSeconds: 1 };
186
200
}
227
241
228
242
export const cache = getCacheProvider();
229
243
244
+/**
245
+ * Retrieves a cached value or fetches and caches it on cache miss.
246
+ * Implements cache-aside pattern with the configured provider.
247
+ */
230
248
export async function getOrSet<T>(
231
249
key: string,
232
250
fetcher: () => Promise<T>,
233
251
▼packages/config/src/index.ts
+8-3
52
52
GITHUB_REDIRECT_URI: z.string().optional(),
53
53
54
54
// Optional AI integration
55
- AI_PROVIDER: z.enum(["opencode", "openai", "anthropic", "gemini", "none"]).default("opencode"),
55
+ // spark = OpenCode Zen (spark-1.3) → Command Code (longcat-2.0:free) fallback chain
56
+ AI_PROVIDER: z
57
+ .enum(["spark", "opencode", "commandcode", "openai", "anthropic", "gemini", "none"])
58
+ .default("spark"),
56
59
AI_API_KEY: z.string().optional(),
57
60
AI_BASE_URL: z.string().default("https://opencode.ai/zen/v1"),
58
- AI_MODEL: z.string().default("nemotron-3.5-lightning-free"),
61
+ AI_MODEL: z.string().default("spark-1.3"),
59
62
60
63
// Security & Rate Limiting
61
64
RATE_LIMIT_USER_MAX: z.coerce.number().int().positive().default(10),
62
65
RATE_LIMIT_USER_WINDOW_SEC: z.coerce.number().int().positive().default(10),
63
66
ENABLE_SECRET_SCANNING: z.coerce.boolean().default(true),
67
+ ENCRYPTION_KEY: z.string().optional(),
68
+ ENCRYPTION_SALT: z.string().optional(),
64
69
});
65
70
66
71
export type Config = z.infer<typeof ConfigSchema>;
77
82
const errorDetails = parsed.error.issues
78
83
.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`)
79
84
.join("\n");
80
- throw new Error(`Configuration validation error:\n${errorDetails}`);
85
+ throw new Error(`Configuration validation failed:\n${errorDetails}`);
81
86
}
82
87
83
88
if (customEnv === process.env) {
84
89
▼packages/devtools/src/crypto-tools.ts
+4-2
89
89
}
90
90
91
91
return res;
92
- } catch (err: any) {
93
- throw new ValidationError(`Failed to decode JWT: ${err.message}`);
92
+ } catch (err: unknown) {
93
+ throw new ValidationError(
94
+ `Failed to decode JWT: ${err instanceof Error ? err.message : String(err)}`,
95
+ );
94
96
}
95
97
}
96
98
▼packages/devtools/src/data-tools.ts
+17-12
5
5
try {
6
6
const parsed = JSON.parse(input);
7
7
return JSON.stringify(parsed, null, indent);
8
- } catch (err: any) {
9
- throw new ValidationError(`Invalid JSON: ${err.message}`);
8
+ } catch (err: unknown) {
9
+ const msg = err instanceof Error ? err.message : String(err);
10
+ throw new ValidationError(`Invalid JSON: ${msg}`);
10
11
}
11
12
}
12
13
14
15
try {
15
16
const parsed = JSON.parse(input);
16
17
return JSON.stringify(parsed);
17
- } catch (err: any) {
18
- throw new ValidationError(`Invalid JSON: ${err.message}`);
18
+ } catch (err: unknown) {
19
+ const msg = err instanceof Error ? err.message : String(err);
20
+ throw new ValidationError(`Invalid JSON: ${msg}`);
19
21
}
20
22
}
21
23
24
26
const parsed = JSON.parse(input);
25
27
const type = Array.isArray(parsed) ? "array" : typeof parsed;
26
28
return { valid: true, type };
27
- } catch (err: any) {
28
- return { valid: false, error: err.message };
29
+ } catch (err: unknown) {
30
+ return { valid: false, error: err instanceof Error ? err.message : String(err) };
29
31
}
30
32
}
31
33
33
35
try {
34
36
const parsed = parseYaml(input);
35
37
return stringifyYaml(parsed);
36
- } catch (err: any) {
37
- throw new ValidationError(`Invalid YAML: ${err.message}`);
38
+ } catch (err: unknown) {
39
+ const msg = err instanceof Error ? err.message : String(err);
40
+ throw new ValidationError(`Invalid YAML: ${msg}`);
38
41
}
39
42
}
40
43
42
45
try {
43
46
const parsed = parseYaml(yamlInput);
44
47
return JSON.stringify(parsed, null, 2);
45
- } catch (err: any) {
46
- throw new ValidationError(`Failed to convert YAML to JSON: ${err.message}`);
48
+ } catch (err: unknown) {
49
+ const msg = err instanceof Error ? err.message : String(err);
50
+ throw new ValidationError(`Failed to convert YAML to JSON: ${msg}`);
47
51
}
48
52
}
49
53
51
55
try {
52
56
const parsed = JSON.parse(jsonInput);
53
57
return stringifyYaml(parsed);
54
- } catch (err: any) {
55
- throw new ValidationError(`Failed to convert JSON to YAML: ${err.message}`);
58
+ } catch (err: unknown) {
59
+ const msg = err instanceof Error ? err.message : String(err);
60
+ throw new ValidationError(`Failed to convert JSON to YAML: ${msg}`);
56
61
}
57
62
}
58
63
59
64
▼packages/devtools/src/helpers-tools.ts
+4-2
25
25
groups: m.groups,
26
26
})),
27
27
};
28
- } catch (err: any) {
29
- throw new ValidationError(`Invalid regular expression: ${err.message}`);
28
+ } catch (err: unknown) {
29
+ throw new ValidationError(
30
+ `Invalid regular expression: ${err instanceof Error ? err.message : String(err)}`,
31
+ );
30
32
}
31
33
}
32
34
33
35
▼packages/devtools/src/network-tools.ts
+5-3
52
52
}
53
53
54
54
return { hostname: cleanHost, recordType, records };
55
- } catch (err: any) {
56
- throw new ValidationError(`DNS lookup failed for ${cleanHost} (${recordType}): ${err.message}`);
55
+ } catch (err: unknown) {
56
+ throw new ValidationError(
57
+ `DNS lookup failed for ${cleanHost} (${recordType}): ${err instanceof Error ? err.message : String(err)}`,
58
+ );
57
59
}
58
60
}
59
61
125
127
},
126
128
);
127
129
128
- socket.on("error", (err: any) => {
130
+ socket.on("error", (err: Error) => {
129
131
reject(new ValidationError(`SSL connection failed: ${err.message}`));
130
132
});
131
133
132
134
▼packages/devtools/src/package-tools.ts
+2-2
46
46
throw new NotFoundError(`npm package '${name}' was not found in registry.`);
47
47
}
48
48
if (!res.ok) {
49
- throw new Error(`npm registry error: ${res.statusText}`);
49
+ throw new Error(`Failed to fetch npm package metadata: registry returned ${res.statusText}`);
50
50
}
51
51
52
52
const data = (await res.json()) as any;
86
86
throw new NotFoundError(`PyPI package '${name}' was not found in index.`);
87
87
}
88
88
if (!res.ok) {
89
- throw new Error(`PyPI registry error: ${res.statusText}`);
89
+ throw new Error(`Failed to fetch PyPI package metadata: registry returned ${res.statusText}`);
90
90
}
91
91
92
92
const data = (await res.json()) as any;
93
93
▼packages/devtools/src/ssrf.ts
+4-2
119
119
);
120
120
}
121
121
}
122
- } catch (err: any) {
122
+ } catch (err: unknown) {
123
123
if (err instanceof SecurityError) throw err;
124
- throw new SecurityError(`Could not resolve hostname '${hostname}': ${err.message}`);
124
+ throw new SecurityError(
125
+ `Could not resolve hostname '${hostname}': ${err instanceof Error ? err.message : String(err)}`,
126
+ );
125
127
}
126
128
127
129
return parsed;
128
130
▼packages/devtools/src/text-tools.ts
+6-4
8
8
try {
9
9
const buf = Buffer.from(input.trim(), "base64");
10
10
return buf.toString("utf-8");
11
- } catch (err: any) {
12
- throw new ValidationError(`Failed to decode Base64 string: ${err.message}`);
11
+ } catch (err: unknown) {
12
+ const msg = err instanceof Error ? err.message : String(err);
13
+ throw new ValidationError(`Failed to decode Base64 string: ${msg}`);
13
14
}
14
15
}
15
16
20
21
export function urlDecode(input: string): string {
21
22
try {
22
23
return decodeURIComponent(input);
23
- } catch (err: any) {
24
- throw new ValidationError(`Failed to decode URL string: ${err.message}`);
24
+ } catch (err: unknown) {
25
+ const msg = err instanceof Error ? err.message : String(err);
26
+ throw new ValidationError(`Failed to decode URL string: ${msg}`);
25
27
}
26
28
}
27
29
28
30
▼packages/devtools/src/time-tools.ts
+15-6
48
48
nextOccurrences: Array<{ iso: string; discord: string }>;
49
49
} {
50
50
try {
51
- const p: any = cronParser;
52
- const parseFn = p.parseExpression || p.default?.parseExpression;
51
+ interface CronParserModule {
52
+ parseExpression: (expr: string) => { next: () => { toDate: () => Date } };
53
+ default?: { parseExpression: (expr: string) => { next: () => { toDate: () => Date } } };
54
+ }
55
+ const p: unknown = cronParser;
56
+ const mod = p as CronParserModule;
57
+ const parseFn = mod.parseExpression || mod.default?.parseExpression;
53
58
const interval = parseFn(expression.trim());
54
59
const nextOccurrences: Array<{ iso: string; discord: string }> = [];
55
60
66
71
description: `Runs on schedule: ${expression.trim()}`,
67
72
nextOccurrences,
68
73
};
69
- } catch (err: any) {
70
- throw new ValidationError(`Invalid cron expression '${expression}': ${err.message}`);
74
+ } catch (err: unknown) {
75
+ throw new ValidationError(
76
+ `Invalid cron expression '${expression}': ${err instanceof Error ? err.message : String(err)}`,
77
+ );
71
78
}
72
79
}
73
80
104
111
toTimezone: toTz,
105
112
converted: formatter.format(date),
106
113
};
107
- } catch (err: any) {
108
- throw new ValidationError(`Timezone conversion failed: ${err.message}`);
114
+ } catch (err: unknown) {
115
+ throw new ValidationError(
116
+ `Timezone conversion failed: ${err instanceof Error ? err.message : String(err)}`,
117
+ );
109
118
}
110
119
}
111
120
▼packages/github/src/client.ts
+477-223
69
69
endpoint: string,
70
70
options: {
71
71
method?: string;
72
- body?: any;
72
+ body?: unknown;
73
73
token?: string;
74
74
accept?: string;
75
75
} = {},
131
131
return getOrSet(
132
132
cacheKey,
133
133
async () => {
134
- const raw = await this.request<any>(`/repos/${owner}/${repo}`);
134
+ const raw = (await this.request<unknown>(`/repos/${owner}/${repo}`)) as {
135
+ id: number;
136
+ name: string;
137
+ full_name: string;
138
+ owner: { login: string; avatar_url: string };
139
+ description: string | null;
140
+ html_url: string;
141
+ language: string | null;
142
+ stargazers_count: number;
143
+ forks_count: number;
144
+ open_issues_count: number;
145
+ default_branch: string;
146
+ private: boolean;
147
+ created_at: string;
148
+ updated_at: string;
149
+ pushed_at: string;
150
+ topics: string[];
151
+ license: { name: string; spdx_id: string } | null;
152
+ };
135
153
return {
136
154
id: raw.id,
137
155
name: raw.name,
283
301
// 2. PULL REQUEST POWER TOOLS
284
302
async getPullRequest(repoInput: string, prNumber: number): Promise<GitHubDetailedPullRequest> {
285
303
const { owner, repo } = this.parseRepoInput(repoInput);
286
- const raw = await this.request<any>(`/repos/${owner}/${repo}/pulls/${prNumber}`);
287
- const reviewsRaw = await this.request<any[]>(
304
+ const raw = (await this.request<unknown>(`/repos/${owner}/${repo}/pulls/${prNumber}`)) as {
305
+ id: number;
306
+ number: number;
307
+ title: string;
308
+ body: string | null;
309
+ state: string;
310
+ html_url: string;
311
+ created_at: string;
312
+ updated_at: string;
313
+ closed_at: string | null;
314
+ merged_at: string | null;
315
+ user: { login: string; avatar_url: string } | null;
316
+ draft: boolean;
317
+ additions: number;
318
+ deletions: number;
319
+ changed_files: number;
320
+ mergeable: boolean | null;
321
+ mergeable_state: string;
322
+ head: { ref: string } | null;
323
+ base: { ref: string } | null;
324
+ requested_reviewers: { login: string }[] | null;
325
+ };
326
+ const reviewsRaw = await this.request<unknown[]>(
288
327
`/repos/${owner}/${repo}/pulls/${prNumber}/reviews`,
289
328
).catch(() => []);
290
329
291
- const reviews = reviewsRaw.map((r) => ({
292
- user: r.user?.login || "reviewer",
293
- state: r.state as any,
294
- submittedAt: r.submitted_at,
295
- }));
330
+ const reviews = reviewsRaw.map((r: unknown) => {
331
+ const review = r as { user?: { login?: string }; state?: string; submitted_at?: string };
332
+ return {
333
+ user: review.user?.login || "reviewer",
334
+ state: (review.state || "") as "APPROVED" | "CHANGES_REQUESTED" | "COMMENTED" | "PENDING",
335
+ submittedAt: review.submitted_at,
336
+ };
337
+ });
296
338
297
- const requestedReviewers = (raw.requested_reviewers || []).map((u: any) => u.login);
339
+ const requestedReviewers = (raw.requested_reviewers || []).map(
340
+ (u: unknown) => (u as { login: string }).login,
341
+ );
298
342
const createdAtMs = new Date(raw.created_at).getTime();
299
343
const closedOrNowMs = raw.closed_at ? new Date(raw.closed_at).getTime() : Date.now();
300
344
const cycleTimeMs = closedOrNowMs - createdAtMs;
309
353
number: raw.number,
310
354
title: raw.title,
311
355
body: raw.body,
312
- state: raw.state,
356
+ state: raw.state as "open" | "closed",
313
357
htmlUrl: raw.html_url,
314
358
createdAt: raw.created_at,
315
359
updatedAt: raw.updated_at,
339
383
340
384
async getPullRequestFiles(repoInput: string, prNumber: number): Promise<GitHubPullRequestFile[]> {
341
385
const { owner, repo } = this.parseRepoInput(repoInput);
342
- const raw = await this.request<any[]>(
386
+ const raw = await this.request<unknown[]>(
343
387
`/repos/${owner}/${repo}/pulls/${prNumber}/files?per_page=100`,
344
388
);
345
- return raw.map((f) => ({
346
- sha: f.sha,
347
- filename: f.filename,
348
- status: f.status,
349
- additions: f.additions || 0,
350
- deletions: f.deletions || 0,
351
- changes: f.changes || 0,
352
- patch: f.patch,
353
- }));
389
+ return raw.map((f) => {
390
+ const file = f as {
391
+ sha: string;
392
+ filename: string;
393
+ status: string;
394
+ additions: number;
395
+ deletions: number;
396
+ changes: number;
397
+ patch?: string;
398
+ };
399
+ return {
400
+ sha: file.sha,
401
+ filename: file.filename,
402
+ status: file.status as "added" | "removed" | "modified" | "renamed",
403
+ additions: file.additions || 0,
404
+ deletions: file.deletions || 0,
405
+ changes: file.changes || 0,
406
+ patch: file.patch,
407
+ };
408
+ });
354
409
}
355
410
356
411
async getPullRequests(
364
419
return getOrSet(
365
420
cacheKey,
366
421
async () => {
367
- const raw = await this.request<any[]>(
422
+ const raw = await this.request<unknown[]>(
368
423
`/repos/${owner}/${repo}/pulls?state=${state}&per_page=${limit}&sort=updated&direction=desc`,
369
424
);
370
- return raw.map((pr) => ({
371
- id: pr.id,
372
- number: pr.number,
373
- title: pr.title,
374
- state: pr.state,
375
- htmlUrl: pr.html_url,
376
- createdAt: pr.created_at,
377
- updatedAt: pr.updated_at,
378
- closedAt: pr.closed_at,
379
- mergedAt: pr.merged_at,
380
- author: {
381
- login: pr.user?.login || "ghost",
382
- avatarUrl: pr.user?.avatar_url || "",
383
- },
384
- draft: pr.draft || false,
385
- }));
425
+ return raw.map((pr) => {
426
+ const pullRequest = pr as {
427
+ id: number;
428
+ number: number;
429
+ title: string;
430
+ state: string;
431
+ html_url: string;
432
+ created_at: string;
433
+ updated_at: string;
434
+ closed_at: string | null;
435
+ merged_at: string | null;
436
+ user: { login: string; avatar_url: string } | null;
437
+ draft: boolean;
438
+ };
439
+ return {
440
+ id: pullRequest.id,
441
+ number: pullRequest.number,
442
+ title: pullRequest.title,
443
+ state: pullRequest.state as "open" | "closed",
444
+ htmlUrl: pullRequest.html_url,
445
+ createdAt: pullRequest.created_at,
446
+ updatedAt: pullRequest.updated_at,
447
+ closedAt: pullRequest.closed_at,
448
+ mergedAt: pullRequest.merged_at,
449
+ author: {
450
+ login: pullRequest.user?.login || "ghost",
451
+ avatarUrl: pullRequest.user?.avatar_url || "",
452
+ },
453
+ draft: pullRequest.draft || false,
454
+ };
455
+ });
386
456
},
387
457
120,
388
458
cache,
427
497
userToken?: string,
428
498
): Promise<{ id: number; state: string; htmlUrl: string }> {
429
499
const { owner, repo } = this.parseRepoInput(repoInput);
430
- const raw = await this.request<any>(`/repos/${owner}/${repo}/pulls/${prNumber}/reviews`, {
500
+ const raw = (await this.request<unknown>(`/repos/${owner}/${repo}/pulls/${prNumber}/reviews`, {
431
501
method: "POST",
432
502
body: { event, body: body || `Review submitted via DevPulse: ${event}` },
433
503
token: userToken,
434
- });
504
+ })) as { id: number; state: string; html_url: string };
435
505
return { id: raw.id, state: raw.state, htmlUrl: raw.html_url };
436
506
}
437
507
440
510
const q = repoInput
441
511
? `${query} repo:${this.parseRepoInput(repoInput).owner}/${this.parseRepoInput(repoInput).repo}`
442
512
: query;
443
- const raw = await this.request<{ items: any[] }>(
513
+ const raw = await this.request<{ items: unknown[] }>(
444
514
`/search/code?q=${encodeURIComponent(q)}&per_page=10`,
445
515
);
446
- return (raw.items || []).map((item) => ({
447
- name: item.name,
448
- path: item.path,
449
- sha: item.sha,
450
- htmlUrl: item.html_url,
516
+ return (raw.items || []).map((item: unknown) => ({
517
+ name: (item as { name: string }).name,
518
+ path: (item as { path: string }).path,
519
+ sha: (item as { sha: string }).sha,
520
+ htmlUrl: (item as { html_url: string }).html_url,
451
521
repository: {
452
- fullName: item.repository.full_name,
453
- htmlUrl: item.repository.html_url,
522
+ fullName: (item as { repository: { full_name: string; html_url: string } }).repository
523
+ .full_name,
524
+ htmlUrl: (item as { repository: { full_name: string; html_url: string } }).repository
525
+ .html_url,
454
526
},
455
- }));
527
+ })) as GitHubCodeSearchResult[];
456
528
}
457
529
458
530
async searchIssues(query: string, repoInput?: string): Promise<GitHubSearchItem[]> {
459
531
const q = repoInput
460
532
? `${query} repo:${this.parseRepoInput(repoInput).owner}/${this.parseRepoInput(repoInput).repo} type:issue`
461
533
: `${query} type:issue`;
462
- const raw = await this.request<{ items: any[] }>(
534
+ const raw = await this.request<{ items: unknown[] }>(
463
535
`/search/issues?q=${encodeURIComponent(q)}&per_page=10`,
464
536
);
465
- return (raw.items || []).map((item) => ({
466
- id: item.id,
467
- title: item.title,
468
- number: item.number,
469
- state: item.state,
470
- htmlUrl: item.html_url,
471
- author: item.user?.login || "ghost",
472
- createdAt: item.created_at,
473
- updatedAt: item.updated_at,
474
- commentsCount: item.comments,
475
- }));
537
+ return (raw.items || []).map((item: unknown) => ({
538
+ id: (item as { id: number }).id,
539
+ title: (item as { title: string }).title,
540
+ number: (item as { number: number }).number,
541
+ state: (item as { state: string }).state,
542
+ htmlUrl: (item as { html_url: string }).html_url,
543
+ author: (item as { user?: { login?: string } }).user?.login || "ghost",
544
+ createdAt: (item as { created_at: string }).created_at,
545
+ updatedAt: (item as { updated_at: string }).updated_at,
546
+ commentsCount: (item as { comments: number }).comments,
547
+ })) as GitHubSearchItem[];
476
548
}
477
549
478
550
async searchPullRequests(query: string, repoInput?: string): Promise<GitHubSearchItem[]> {
479
551
const q = repoInput
480
552
? `${query} repo:${this.parseRepoInput(repoInput).owner}/${this.parseRepoInput(repoInput).repo} type:pr`
481
553
: `${query} type:pr`;
482
- const raw = await this.request<{ items: any[] }>(
554
+ const raw = await this.request<{ items: unknown[] }>(
483
555
`/search/issues?q=${encodeURIComponent(q)}&per_page=10`,
484
556
);
485
- return (raw.items || []).map((item) => ({
486
- id: item.id,
487
- title: item.title,
488
- number: item.number,
489
- state: item.state,
490
- htmlUrl: item.html_url,
491
- author: item.user?.login || "ghost",
492
- createdAt: item.created_at,
493
- updatedAt: item.updated_at,
494
- commentsCount: item.comments,
495
- }));
557
+ return (raw.items || []).map((item: unknown) => ({
558
+ id: (item as { id: number }).id,
559
+ title: (item as { title: string }).title,
560
+ number: (item as { number: number }).number,
561
+ state: (item as { state: string }).state,
562
+ htmlUrl: (item as { html_url: string }).html_url,
563
+ author: (item as { user?: { login?: string } }).user?.login || "ghost",
564
+ createdAt: (item as { created_at: string }).created_at,
565
+ updatedAt: (item as { updated_at: string }).updated_at,
566
+ commentsCount: (item as { comments: number }).comments,
567
+ })) as GitHubSearchItem[];
496
568
}
497
569
498
570
async searchRepositories(query: string): Promise<GitHubRepo[]> {
499
- const raw = await this.request<{ items: any[] }>(
571
+ const raw = await this.request<{ items: unknown[] }>(
500
572
`/search/repositories?q=${encodeURIComponent(query)}&sort=stars&order=desc&per_page=10`,
501
573
);
502
- return (raw.items || []).map((rawRepo) => ({
503
- id: rawRepo.id,
504
- name: rawRepo.name,
505
- fullName: rawRepo.full_name,
506
- owner: {
507
- login: rawRepo.owner.login,
508
- avatarUrl: rawRepo.owner.avatar_url,
509
- },
510
- description: rawRepo.description,
511
- htmlUrl: rawRepo.html_url,
512
- language: rawRepo.language,
513
- stars: rawRepo.stargazers_count,
514
- forks: rawRepo.forks_count,
515
- openIssuesCount: rawRepo.open_issues_count,
516
- defaultBranch: rawRepo.default_branch,
517
- isPrivate: rawRepo.private,
518
- createdAt: rawRepo.created_at,
519
- updatedAt: rawRepo.updated_at,
520
- pushedAt: rawRepo.pushed_at,
521
- topics: rawRepo.topics || [],
522
- license: rawRepo.license
523
- ? { name: rawRepo.license.name, spdxId: rawRepo.license.spdx_id }
524
- : null,
525
- }));
574
+ return (raw.items || []).map((rawRepo: unknown) => {
575
+ const repo = rawRepo as {
576
+ id: number;
577
+ name: string;
578
+ full_name: string;
579
+ owner: { login: string; avatar_url: string };
580
+ description: string | null;
581
+ html_url: string;
582
+ language: string | null;
583
+ stargazers_count: number;
584
+ forks_count: number;
585
+ open_issues_count: number;
586
+ default_branch: string;
587
+ private: boolean;
588
+ created_at: string;
589
+ updated_at: string;
590
+ pushed_at: string;
591
+ topics?: string[];
592
+ license: { name: string; spdx_id: string } | null;
593
+ };
594
+ return {
595
+ id: repo.id,
596
+ name: repo.name,
597
+ fullName: repo.full_name,
598
+ owner: {
599
+ login: repo.owner.login,
600
+ avatarUrl: repo.owner.avatar_url,
601
+ },
602
+ description: repo.description,
603
+ htmlUrl: repo.html_url,
604
+ language: repo.language,
605
+ stars: repo.stargazers_count,
606
+ forks: repo.forks_count,
607
+ openIssuesCount: repo.open_issues_count,
608
+ defaultBranch: repo.default_branch,
609
+ isPrivate: repo.private,
610
+ createdAt: repo.created_at,
611
+ updatedAt: repo.updated_at,
612
+ pushedAt: repo.pushed_at,
613
+ topics: repo.topics || [],
614
+ license: repo.license ? { name: repo.license.name, spdxId: repo.license.spdx_id } : null,
615
+ };
616
+ }) as GitHubRepo[];
526
617
}
527
618
528
619
async searchCommits(query: string, repoInput?: string): Promise<GitHubCommit[]> {
529
620
const q = repoInput
530
621
? `${query} repo:${this.parseRepoInput(repoInput).owner}/${this.parseRepoInput(repoInput).repo}`
531
622
: query;
532
- const raw = await this.request<{ items: any[] }>(
623
+ const raw = await this.request<{ items: unknown[] }>(
533
624
`/search/commits?q=${encodeURIComponent(q)}&per_page=10`,
534
625
{
535
626
accept: "application/vnd.github.cloak-preview+json",
536
627
},
537
628
);
538
- return (raw.items || []).map((c) => ({
539
- sha: c.sha,
540
- message: c.commit.message,
629
+ return (raw.items || []).map((c: unknown) => ({
630
+ sha: (c as { sha: string }).sha,
631
+ message: (c as { commit: { message: string } }).commit.message,
541
632
author: {
542
- name: c.commit.author?.name || "Unknown",
543
- login: c.author?.login,
544
- date: c.commit.author?.date || new Date().toISOString(),
633
+ name: (c as { commit: { author?: { name?: string } } }).commit.author?.name || "Unknown",
634
+ login: (c as { author?: { login?: string } }).author?.login,
635
+ date:
636
+ (c as { commit: { author?: { date?: string } } }).commit.author?.date ||
637
+ new Date().toISOString(),
545
638
},
546
- htmlUrl: c.html_url,
547
- }));
639
+ htmlUrl: (c as { html_url: string }).html_url,
640
+ })) as GitHubCommit[];
548
641
}
549
642
550
643
// 4. CODE INTELLIGENCE
551
644
async getFileContents(repoInput: string, path: string, ref?: string): Promise<GitHubFileContent> {
552
645
const { owner, repo } = this.parseRepoInput(repoInput);
553
646
const cleanPath = path.replace(/^\//, "");
554
- const raw = await this.request<any>(
647
+ const raw = (await this.request<unknown>(
555
648
`/repos/${owner}/${repo}/contents/${cleanPath}${ref ? `?ref=${ref}` : ""}`,
556
- );
649
+ )) as {
650
+ name: string;
651
+ path: string;
652
+ sha: string;
653
+ size: number;
654
+ content?: string;
655
+ encoding?: string;
656
+ html_url: string;
657
+ };
557
658
558
659
let content = "";
559
660
if (raw.content && raw.encoding === "base64") {
563
664
}
564
665
565
666
// Also get last commit modifying this file
566
- const commits = await this.request<any[]>(
667
+ const commits = await this.request<unknown[]>(
567
668
`/repos/${owner}/${repo}/commits?path=${cleanPath}&per_page=1`,
568
669
).catch(() => []);
569
670
let lastCommit: GitHubFileContent["lastCommit"] = undefined;
570
671
if (commits.length > 0) {
571
- const c = commits[0];
672
+ const c = commits[0] as {
673
+ sha: string;
674
+ commit: { message: string; author?: { name?: string; date?: string } };
675
+ author?: { login?: string };
676
+ };
572
677
const match = c.commit.message.match(/#(\d+)/);
573
678
lastCommit = {
574
- sha: c.sha.slice(0, 7),
679
+ sha: c.sha,
575
680
message: c.commit.message.split("\n")[0],
576
681
author: c.author?.login || c.commit.author?.name || "Unknown",
577
682
date: c.commit.author?.date || "",
596
701
const cleanPath = path.replace(/^\//, "");
597
702
const file = await this.getFileContents(repoInput, cleanPath);
598
703
const lines = file.content.split("\n");
599
- const commits = await this.request<any[]>(
704
+ const commits = await this.request<unknown[]>(
600
705
`/repos/${owner}/${repo}/commits?path=${cleanPath}&per_page=5`,
601
706
).catch(() => []);
602
707
603
708
const targetLine = lineNumber ? Math.min(lineNumber, lines.length) : 1;
604
- const commit = commits[0] || {
709
+ const commit = (commits[0] || {
605
710
sha: "unknown",
606
711
commit: { message: "Initial commit", author: { name: "Author", date: "" } },
712
+ }) as {
713
+ sha: string;
714
+ commit: { message: string; author?: { name?: string; date?: string } };
715
+ author?: { login?: string };
607
716
};
608
717
const prMatch = commit.commit.message.match(/#(\d+)/);
609
718
623
732
// 5. GITHUB INVESTIGATION
624
733
async investigateIssue(repoInput: string, issueNumber: number): Promise<InvestigationTimeline> {
625
734
const { owner, repo } = this.parseRepoInput(repoInput);
626
- const issue = await this.request<any>(`/repos/${owner}/${repo}/issues/${issueNumber}`);
627
- const eventsRaw = await this.request<any[]>(
735
+ const issue = (await this.request<unknown>(
736
+ `/repos/${owner}/${repo}/issues/${issueNumber}`,
737
+ )) as {
738
+ number: number;
739
+ title: string;
740
+ body: string | null;
741
+ created_at: string;
742
+ html_url: string;
743
+ user: { login: string } | null;
744
+ };
745
+ const eventsRaw = await this.request<unknown[]>(
628
746
`/repos/${owner}/${repo}/issues/${issueNumber}/events`,
629
747
).catch(() => []);
630
748
642
760
643
761
let stepCounter = 2;
644
762
for (const ev of eventsRaw) {
645
- if (ev.event === "referenced" && ev.commit_id) {
763
+ const event = ev as {
764
+ event: string;
765
+ commit_id: string | null;
766
+ actor: { login: string } | null;
767
+ created_at: string;
768
+ };
769
+ if (event.event === "referenced" && event.commit_id) {
646
770
events.push({
647
771
step: stepCounter++,
648
772
type: "commit",
649
- title: `Commit ${ev.commit_id.slice(0, 7)} Referencing Issue`,
773
+ title: `Commit ${event.commit_id.slice(0, 7)} Referencing Issue`,
650
774
description: "Commit linked to issue resolution",
651
- actor: ev.actor?.login || "committer",
652
- timestamp: ev.created_at,
775
+ actor: event.actor?.login || "committer",
776
+ timestamp: event.created_at,
653
777
});
654
- } else if (ev.event === "closed") {
778
+ } else if (event.event === "closed") {
655
779
events.push({
656
780
step: stepCounter++,
657
781
type: "issue_closed",
658
782
title: `Issue Closed`,
659
783
description: "Issue resolved and marked closed",
660
- actor: ev.actor?.login || "maintainer",
661
- timestamp: ev.created_at,
784
+ actor: event.actor?.login || "maintainer",
785
+ timestamp: event.created_at,
662
786
});
663
787
}
664
788
}
740
864
const daysCount = [0, 0, 0, 0, 0, 0, 0]; // Sun..Sat
741
865
742
866
for (const ev of events) {
743
- const d = new Date(ev.created_at).getDay();
867
+ const event = ev as {
868
+ type: string;
869
+ created_at: string;
870
+ payload?: {
871
+ action?: string;
872
+ commits?: { length: number }[];
873
+ pull_request?: { merged: boolean };
874
+ };
875
+ };
876
+ const d = new Date(event.created_at).getDay();
744
877
daysCount[d] = (daysCount[d] || 0) + 1;
745
878
746
- if (ev.type === "PushEvent") {
747
- commitsCount += ev.payload?.commits?.length || 1;
748
- } else if (ev.type === "PullRequestEvent") {
749
- if (ev.payload?.action === "opened") prsOpened++;
750
- if (ev.payload?.pull_request?.merged) prsMerged++;
751
- } else if (ev.type === "PullRequestReviewEvent") {
879
+ if (event.type === "PushEvent") {
880
+ commitsCount += event.payload?.commits?.length || 1;
881
+ } else if (event.type === "PullRequestEvent") {
882
+ if (event.payload?.action === "opened") prsOpened++;
883
+ if (event.payload?.pull_request?.merged) prsMerged++;
884
+ } else if (event.type === "PullRequestReviewEvent") {
752
885
reviewsCount++;
753
- } else if (ev.type === "IssuesEvent" && ev.payload?.action === "closed") {
886
+ } else if (event.type === "IssuesEvent" && event.payload?.action === "closed") {
754
887
issuesResolved++;
755
888
}
756
889
}
779
912
// 7. GITHUB ACTIONS
780
913
async getWorkflows(repoInput: string): Promise<GitHubWorkflow[]> {
781
914
const { owner, repo } = this.parseRepoInput(repoInput);
782
- const raw = await this.request<{ workflows: any[] }>(
915
+ const raw = await this.request<{ workflows: unknown[] }>(
783
916
`/repos/${owner}/${repo}/actions/workflows`,
784
917
).catch(() => ({ workflows: [] }));
785
- return (raw.workflows || []).map((w) => ({
786
- id: w.id,
787
- name: w.name,
788
- path: w.path,
789
- state: w.state,
790
- htmlUrl: w.html_url,
791
- }));
918
+ return (raw.workflows || []).map((w: unknown) => {
919
+ const workflow = w as {
920
+ id: number;
921
+ name: string;
922
+ path: string;
923
+ state: string;
924
+ html_url: string;
925
+ };
926
+ return {
927
+ id: workflow.id,
928
+ name: workflow.name,
929
+ path: workflow.path,
930
+ state: workflow.state,
931
+ htmlUrl: workflow.html_url,
932
+ };
933
+ }) as GitHubWorkflow[];
792
934
}
793
935
794
936
async getWorkflowRuns(repoInput: string, limit = 10): Promise<GitHubWorkflowRun[]> {
795
937
const { owner, repo } = this.parseRepoInput(repoInput);
796
- const raw = await this.request<{ workflow_runs: any[] }>(
938
+ const raw = await this.request<{ workflow_runs: unknown[] }>(
797
939
`/repos/${owner}/${repo}/actions/runs?per_page=${limit}`,
798
940
).catch(() => ({ workflow_runs: [] }));
799
- return (raw.workflow_runs || []).map((r) => ({
800
- id: r.id,
801
- name: r.name || "Workflow Run",
802
- headBranch: r.head_branch || "main",
803
- headSha: (r.head_sha || "").slice(0, 7),
804
- status: r.status,
805
- conclusion: r.conclusion,
806
- htmlUrl: r.html_url,
807
- runNumber: r.run_number,
808
- event: r.event,
809
- createdAt: r.created_at,
810
- updatedAt: r.updated_at,
811
- }));
941
+ return (raw.workflow_runs || []).map((r: unknown) => {
942
+ const run = r as {
943
+ id: number;
944
+ name: string;
945
+ head_branch: string;
946
+ head_sha: string;
947
+ status: string;
948
+ conclusion: string;
949
+ html_url: string;
950
+ run_number: number;
951
+ event: string;
952
+ created_at: string;
953
+ updated_at: string;
954
+ };
955
+ return {
956
+ id: run.id,
957
+ name: run.name || "Workflow Run",
958
+ headBranch: run.head_branch || "main",
959
+ headSha: (run.head_sha || "").slice(0, 7),
960
+ status: run.status,
961
+ conclusion: run.conclusion,
962
+ htmlUrl: run.html_url,
963
+ runNumber: run.run_number,
964
+ event: run.event,
965
+ createdAt: run.created_at,
966
+ updatedAt: run.updated_at,
967
+ };
968
+ }) as GitHubWorkflowRun[];
812
969
}
813
970
814
971
async rerunWorkflow(repoInput: string, runId: number, userToken?: string): Promise<boolean> {
832
989
// 8. SECURITY
833
990
async getSecurityAdvisories(repoInput: string): Promise<GitHubSecurityAdvisory[]> {
834
991
const { owner, repo } = this.parseRepoInput(repoInput);
835
- const raw = await this.request<any[]>(`/repos/${owner}/${repo}/security-advisories`).catch(
992
+ const raw = await this.request<unknown[]>(`/repos/${owner}/${repo}/security-advisories`).catch(
836
993
() => [],
837
994
);
838
- return raw.map((a) => ({
839
- ghsaId: a.ghsa_id,
840
- cveId: a.cve_id,
841
- summary: a.summary,
842
- description: a.description,
843
- severity: a.severity,
844
- package: {
845
- name: a.vulnerabilities?.[0]?.package?.name || repo,
846
- ecosystem: a.vulnerabilities?.[0]?.package?.ecosystem || "npm",
847
- },
848
- vulnerableVersionRange: a.vulnerabilities?.[0]?.vulnerable_version_range || "*",
849
- patchedVersion: a.vulnerabilities?.[0]?.patched_versions || null,
850
- publishedAt: a.published_at,
851
- htmlUrl: a.html_url,
852
- }));
995
+ return raw.map((a) => {
996
+ const advisory = a as {
997
+ ghsa_id: string;
998
+ cve_id: string | null;
999
+ summary: string;
1000
+ description: string;
1001
+ severity: string;
1002
+ vulnerabilities?: {
1003
+ package?: { name?: string; ecosystem?: string };
1004
+ vulnerable_version_range?: string;
1005
+ patched_versions?: string;
1006
+ }[];
1007
+ published_at: string;
1008
+ html_url: string;
1009
+ };
1010
+ return {
1011
+ ghsaId: advisory.ghsa_id,
1012
+ cveId: advisory.cve_id,
1013
+ summary: advisory.summary,
1014
+ description: advisory.description,
1015
+ severity: advisory.severity as "critical" | "high" | "medium" | "low",
1016
+ package: {
1017
+ name: advisory.vulnerabilities?.[0]?.package?.name || repo,
1018
+ ecosystem: advisory.vulnerabilities?.[0]?.package?.ecosystem || "npm",
1019
+ },
1020
+ vulnerableVersionRange: advisory.vulnerabilities?.[0]?.vulnerable_version_range || "*",
1021
+ patchedVersion: advisory.vulnerabilities?.[0]?.patched_versions || null,
1022
+ publishedAt: advisory.published_at,
1023
+ htmlUrl: advisory.html_url,
1024
+ };
1025
+ });
853
1026
}
854
1027
855
1028
// 9. RELEASE MANAGEMENT
859
1032
head: string,
860
1033
): Promise<{ aheadBy: number; behindBy: number; totalCommits: number; commits: GitHubCommit[] }> {
861
1034
const { owner, repo } = this.parseRepoInput(repoInput);
862
- const raw = await this.request<any>(`/repos/${owner}/${repo}/compare/${base}...${head}`);
863
- const commits = (raw.commits || []).map((c: any) => ({
864
- sha: c.sha.slice(0, 7),
1035
+ const raw = (await this.request<unknown>(
1036
+ `/repos/${owner}/${repo}/compare/${base}...${head}`,
1037
+ )) as {
1038
+ commits: {
1039
+ sha: string;
1040
+ commit: { message: string; author?: { name?: string; date?: string } };
1041
+ author?: { login?: string };
1042
+ html_url: string;
1043
+ }[];
1044
+ ahead_by: number;
1045
+ behind_by: number;
1046
+ total_commits: number;
1047
+ };
1048
+ const commits = (raw.commits || []).map((c) => ({
1049
+ sha: c.sha,
865
1050
message: c.commit.message,
866
1051
author: {
867
1052
name: c.commit.author?.name || "Unknown",
987
1172
return getOrSet(
988
1173
cacheKey,
989
1174
async () => {
990
- const raw = await this.request<any[]>(`/repos/${owner}/${repo}/commits?per_page=${limit}`);
991
- return raw.map((c) => ({
992
- sha: c.sha.slice(0, 7),
993
- message: c.commit.message,
994
- author: {
995
- name: c.commit.author?.name || "Unknown",
996
- login: c.author?.login,
997
- date: c.commit.author?.date || new Date().toISOString(),
998
- },
999
- htmlUrl: c.html_url,
1000
- }));
1175
+ const raw = await this.request<unknown[]>(
1176
+ `/repos/${owner}/${repo}/commits?per_page=${limit}`,
1177
+ );
1178
+ return raw.map((c) => {
1179
+ const commit = c as {
1180
+ sha: string;
1181
+ commit: { message: string; author?: { name?: string; date?: string } };
1182
+ author?: { login?: string };
1183
+ html_url: string;
1184
+ };
1185
+ return {
1186
+ sha: commit.sha,
1187
+ message: commit.commit.message,
1188
+ author: {
1189
+ name: commit.commit.author?.name || "Unknown",
1190
+ login: commit.author?.login,
1191
+ date: commit.commit.author?.date || new Date().toISOString(),
1192
+ },
1193
+ htmlUrl: commit.html_url,
1194
+ };
1195
+ });
1001
1196
},
1002
1197
300,
1003
1198
cache,
1015
1210
return getOrSet(
1016
1211
cacheKey,
1017
1212
async () => {
1018
- const raw = await this.request<any[]>(
1213
+ const raw = await this.request<unknown[]>(
1019
1214
`/repos/${owner}/${repo}/issues?state=${state}&per_page=${limit}&sort=updated&direction=desc`,
1020
1215
);
1021
1216
return raw
1022
- .filter((item) => !item.pull_request)
1023
- .map((issue) => ({
1024
- id: issue.id,
1025
- number: issue.number,
1026
- title: issue.title,
1027
- state: issue.state,
1028
- htmlUrl: issue.html_url,
1029
- createdAt: issue.created_at,
1030
- closedAt: issue.closed_at,
1031
- author: {
1032
- login: issue.user?.login || "ghost",
1033
- avatarUrl: issue.user?.avatar_url || "",
1034
- },
1035
- commentsCount: issue.comments || 0,
1036
- isPullRequest: false,
1037
- }));
1217
+ .filter((item) => !(item as { pull_request?: unknown }).pull_request)
1218
+ .map((issue) => {
1219
+ const i = issue as {
1220
+ id: number;
1221
+ number: number;
1222
+ title: string;
1223
+ state: string;
1224
+ html_url: string;
1225
+ created_at: string;
1226
+ closed_at: string | null;
1227
+ user: { login: string; avatar_url: string } | null;
1228
+ comments: number;
1229
+ };
1230
+ return {
1231
+ id: i.id,
1232
+ number: i.number,
1233
+ title: i.title,
1234
+ state: i.state as "open" | "closed",
1235
+ htmlUrl: i.html_url,
1236
+ createdAt: i.created_at,
1237
+ closedAt: i.closed_at,
1238
+ author: {
1239
+ login: i.user?.login || "ghost",
1240
+ avatarUrl: i.user?.avatar_url || "",
1241
+ },
1242
+ commentsCount: i.comments || 0,
1243
+ isPullRequest: false,
1244
+ };
1245
+ });
1038
1246
},
1039
1247
300,
1040
1248
cache,
1043
1251
1044
1252
async getSingleIssue(repoInput: string, issueNumber: number): Promise<GitHubDetailedIssue> {
1045
1253
const { owner, repo } = this.parseRepoInput(repoInput);
1046
- const raw = await this.request<any>(`/repos/${owner}/${repo}/issues/${issueNumber}`);
1254
+ const raw = (await this.request<unknown>(`/repos/${owner}/${repo}/issues/${issueNumber}`)) as {
1255
+ id: number;
1256
+ number: number;
1257
+ title: string;
1258
+ state: string;
1259
+ html_url: string;
1260
+ created_at: string;
1261
+ closed_at: string | null;
1262
+ user: { login: string; avatar_url: string } | null;
1263
+ comments: number;
1264
+ pull_request?: unknown;
1265
+ body: string | null;
1266
+ labels: ({ name: string } | string)[];
1267
+ };
1047
1268
return {
1048
1269
id: raw.id,
1049
1270
number: raw.number,
1050
1271
title: raw.title,
1051
- state: raw.state,
1272
+ state: raw.state as "open" | "closed",
1052
1273
htmlUrl: raw.html_url,
1053
1274
createdAt: raw.created_at,
1054
1275
closedAt: raw.closed_at,
1059
1280
commentsCount: raw.comments || 0,
1060
1281
isPullRequest: Boolean(raw.pull_request),
1061
1282
body: raw.body || "",
1062
- labels: (raw.labels || []).map((l: any) => (typeof l === "string" ? l : l.name || "")),
1283
+ labels: (raw.labels || []).map((l: unknown) =>
1284
+ typeof l === "string" ? l : (l as { name: string }).name || "",
1285
+ ),
1063
1286
};
1064
1287
}
1065
1288
1070
1293
return getOrSet(
1071
1294
cacheKey,
1072
1295
async () => {
1073
- const raw = await this.request<any[]>(
1296
+ const raw = await this.request<unknown[]>(
1074
1297
`/repos/${owner}/${repo}/contributors?per_page=${limit}`,
1075
1298
);
1076
- return raw.map((c) => ({
1077
- login: c.login,
1078
- avatarUrl: c.avatar_url,
1079
- htmlUrl: c.html_url,
1080
- contributions: c.contributions,
1081
- }));
1299
+ return raw.map((c) => {
1300
+ const contributor = c as {
1301
+ login: string;
1302
+ avatar_url: string;
1303
+ html_url: string;
1304
+ contributions: number;
1305
+ };
1306
+ return {
1307
+ login: contributor.login,
1308
+ avatarUrl: contributor.avatar_url,
1309
+ htmlUrl: contributor.html_url,
1310
+ contributions: contributor.contributions,
1311
+ };
1312
+ });
1082
1313
},
1083
1314
1800,
1084
1315
cache,
1092
1323
return getOrSet(
1093
1324
cacheKey,
1094
1325
async () => {
1095
- const raw = await this.request<any[]>(`/repos/${owner}/${repo}/releases?per_page=${limit}`);
1096
- return raw.map((r) => ({
1097
- id: r.id,
1098
- tagName: r.tag_name,
1099
- name: r.name || r.tag_name,
1100
- publishedAt: r.published_at,
1101
- htmlUrl: r.html_url,
1102
- body: r.body || "",
1103
- prerelease: r.prerelease || false,
1104
- }));
1326
+ const raw = await this.request<unknown[]>(
1327
+ `/repos/${owner}/${repo}/releases?per_page=${limit}`,
1328
+ );
1329
+ return raw.map((r) => {
1330
+ const release = r as {
1331
+ id: number;
1332
+ tag_name: string;
1333
+ name: string | null;
1334
+ published_at: string;
1335
+ html_url: string;
1336
+ body: string | null;
1337
+ prerelease: boolean;
1338
+ };
1339
+ return {
1340
+ id: release.id,
1341
+ tagName: release.tag_name,
1342
+ name: release.name || release.tag_name,
1343
+ publishedAt: release.published_at,
1344
+ htmlUrl: release.html_url,
1345
+ body: release.body || "",
1346
+ prerelease: release.prerelease || false,
1347
+ };
1348
+ });
1105
1349
},
1106
1350
1800,
1107
1351
cache,
1115
1359
return getOrSet(
1116
1360
cacheKey,
1117
1361
async () => {
1118
- const raw = await this.request<any>(`/users/${cleanUser}`);
1362
+ const raw = (await this.request<unknown>(`/users/${cleanUser}`)) as {
1363
+ login: string;
1364
+ name: string | null;
1365
+ avatar_url: string;
1366
+ html_url: string;
1367
+ bio: string | null;
1368
+ public_repos: number;
1369
+ followers: number;
1370
+ following: number;
1371
+ created_at: string;
1372
+ };
1119
1373
return {
1120
1374
login: raw.login,
1121
1375
name: raw.name,
1133
1387
);
1134
1388
}
1135
1389
1136
- async getUserEvents(username: string, limit = 50): Promise<any[]> {
1390
+ async getUserEvents(username: string, limit = 50): Promise<unknown[]> {
1137
1391
const cleanUser = username.replace(/^@/, "").trim();
1138
1392
const cacheKey = `gh:events:${cleanUser.toLowerCase()}:${limit}`;
1139
1393
1140
1394
return getOrSet(
1141
1395
cacheKey,
1142
1396
async () => {
1143
- return this.request<any[]>(`/users/${cleanUser}/events?per_page=${limit}`);
1397
+ return this.request<unknown[]>(`/users/${cleanUser}/events?per_page=${limit}`);
1144
1398
},
1145
1399
300,
1146
1400
cache,
1147
1401
▼packages/monitoring/src/index.ts
+4-3
150
150
}
151
151
}
152
152
}
153
- } catch (err: any) {
153
+ } catch (err: unknown) {
154
154
isHealthy = false;
155
- errorMessage = err.message || "Network / connection timeout";
155
+ errorMessage =
156
+ (err instanceof Error ? err.message : String(err)) || "Network / connection timeout";
156
157
}
157
158
158
159
const responseTimeMs = Date.now() - startTime;
227
228
try {
228
229
const res = await this.checkMonitor(m.id);
229
230
outcomes.push(res);
230
- } catch (err) {
231
+ } catch (err: unknown) {
231
232
logger.error({ err, monitorId: m.id }, "Error during scheduled monitor check");
232
233
}
233
234
}
234
235
▼packages/news/src/index.ts
+27-7
64
64
});
65
65
66
66
if (!response.ok) {
67
- throw new Error(`Algolia HN API error: ${response.statusText}`);
67
+ throw new Error(`Failed to fetch Hacker News articles from Algolia: ${response.statusText}`);
68
68
}
69
69
70
- const data = (await response.json()) as any;
70
+ interface HnHit {
71
+ objectID: string;
72
+ title: string;
73
+ url?: string;
74
+ story_url?: string;
75
+ created_at: string;
76
+ story_text?: string;
77
+ points?: number;
78
+ num_comments?: number;
79
+ }
80
+ interface HnResponse {
81
+ hits: HnHit[];
82
+ }
83
+ const data = (await response.json()) as HnResponse;
71
84
const hits = data.hits || [];
72
85
73
86
return hits
74
- .filter((h: any) => h.title && (h.url || h.story_url))
75
- .map((h: any) => {
87
+ .filter((h) => h.title && (h.url || h.story_url))
88
+ .map((h) => {
76
89
const itemUrl =
77
90
h.url || h.story_url || `https://news.ycombinator.com/item?id=${h.objectID}`;
78
91
const domain = (() => {
130
143
});
131
144
132
145
if (!response.ok) {
133
- throw new Error(`Dev.to API error: ${response.statusText}`);
146
+ throw new Error(`Failed to fetch Dev.to articles: ${response.statusText}`);
134
147
}
135
148
136
- const data = (await response.json()) as any[];
149
+ interface DevToArticle {
150
+ id: number;
151
+ title: string;
152
+ url: string;
153
+ published_at: string;
154
+ description: string;
155
+ }
156
+ const data = (await response.json()) as DevToArticle[];
137
157
138
158
return data.map((art) => ({
139
159
id: `devto-${art.id}`,
163
183
if (items && items.length > 0) {
164
184
return items;
165
185
}
166
- } catch (err) {
186
+ } catch (err: unknown) {
167
187
logger.warn(
168
188
{ err, provider: provider.name },
169
189
"News provider failed, trying next provider",
170
190
▼packages/scheduler/src/index.ts
+13-8
2
2
import { NotFoundError } from "@devpulse/core";
3
3
import { db, reminders } from "@devpulse/database";
4
4
import { logger } from "@devpulse/logger";
5
-import { monitoringService } from "@devpulse/monitoring";
5
+import { type MonitorCheckOutcome, monitoringService } from "@devpulse/monitoring";
6
6
import cronParser from "cron-parser";
7
7
import { and, desc, eq, lte } from "drizzle-orm";
8
8
97
97
async processReminderCompletion(reminder: DueReminder) {
98
98
if (reminder.isRecurring && reminder.cronExpression) {
99
99
try {
100
- const p: any = cronParser;
101
- const parseFn = p.parseExpression || p.default?.parseExpression;
100
+ interface CronParserModule {
101
+ parseExpression: (expr: string) => { next: () => { toDate: () => Date } };
102
+ default?: { parseExpression: (expr: string) => { next: () => { toDate: () => Date } } };
103
+ }
104
+ const mod = cronParser as CronParserModule;
105
+ const parseFn = mod.parseExpression || mod.default?.parseExpression;
102
106
const interval = parseFn(reminder.cronExpression);
103
107
const nextDue = interval.next().toDate();
104
108
await db.update(reminders).set({ dueAt: nextDue }).where(eq(reminders.id, reminder.id));
105
109
return;
106
- } catch (err) {
110
+ } catch (err: unknown) {
107
111
logger.error(
108
112
{ err, reminderId: reminder.id },
109
113
"Failed to calculate next recurring cron time",
121
125
private timer: NodeJS.Timeout | null = null;
122
126
private isRunning = false;
123
127
private onReminderDue?: (reminder: DueReminder) => Promise<void>;
124
- private onMonitorAlert?: (outcome: any) => Promise<void>;
128
+ private onMonitorAlert?: (outcome: MonitorCheckOutcome) => Promise<void>;
125
129
126
130
setReminderHandler(handler: (reminder: DueReminder) => Promise<void>) {
127
131
this.onReminderDue = handler;
128
132
}
129
133
130
- setMonitorAlertHandler(handler: (outcome: any) => Promise<void>) {
134
+ setMonitorAlertHandler(handler: (outcome: MonitorCheckOutcome) => Promise<void>) {
131
135
this.onMonitorAlert = handler;
132
136
}
133
137
139
143
this.timer = setInterval(async () => {
140
144
await this.tick();
141
145
}, intervalMs);
146
+ this.timer.unref?.();
142
147
143
148
// Initial immediate tick
144
149
this.tick().catch((err) => logger.error({ err }, "Initial scheduler tick failed"));
165
170
await this.onReminderDue(rem);
166
171
}
167
172
await reminderService.processReminderCompletion(rem);
168
- } catch (err) {
173
+ } catch (err: unknown) {
169
174
logger.error({ err, reminderId: rem.id }, "Error delivering reminder");
170
175
}
171
176
}
182
187
await this.onMonitorAlert(o);
183
188
}
184
189
}
185
- } catch (err) {
190
+ } catch (err: unknown) {
186
191
logger.error({ err }, "Error running scheduled monitor checks");
187
192
}
188
193
}
189
194
▼packages/security/src/cve.ts
+47-10
36
36
throw new NotFoundError("Vulnerability", cleanId);
37
37
}
38
38
if (!response.ok) {
39
- throw new Error(`OSV API returned status ${response.status}`);
39
+ throw new Error(
40
+ `Failed to fetch CVE data from OSV API: returned status ${response.status}`,
41
+ );
40
42
}
41
43
42
- const data = (await response.json()) as any;
44
+ interface OsvRange {
45
+ events?: { fixed?: string }[];
46
+ }
47
+ interface OsvAffected {
48
+ ranges?: OsvRange[];
49
+ package?: { name: string; ecosystem: string };
50
+ }
51
+ interface OsvVulnerability {
52
+ affected?: OsvAffected[];
53
+ references?: { url: string }[];
54
+ id: string;
55
+ summary: string;
56
+ details?: string;
57
+ severity?: { score?: string }[];
58
+ published?: string;
59
+ modified?: string;
60
+ database_specific?: { severity?: string };
61
+ }
62
+ const data = (await response.json()) as OsvVulnerability;
43
63
const affected = data.affected?.[0];
44
64
const fixedVersions: string[] = [];
45
65
51
71
}
52
72
}
53
73
54
- const references = (data.references || []).map((r: any) => r.url).slice(0, 5);
74
+ const references = ((data as { references?: { url: string }[] }).references || [])
75
+ .map((r: unknown) => (r as { url: string }).url)
76
+ .slice(0, 5);
55
77
56
78
return {
57
79
id: data.id,
85
107
return getOrSet(
86
108
cacheKey,
87
109
async () => {
88
- const body: any = {
110
+ interface OsvQueryBody {
111
+ package: { name: string; ecosystem: string };
112
+ version?: string;
113
+ }
114
+ const body: OsvQueryBody = {
89
115
package: {
90
116
name: packageName.trim(),
91
117
ecosystem: ecosystem.trim(),
102
128
});
103
129
104
130
if (!response.ok) {
105
- throw new Error(`OSV API query error: ${response.statusText}`);
131
+ throw new Error(`Failed to query OSV API for vulnerabilities: ${response.statusText}`);
106
132
}
107
133
108
- const data = (await response.json()) as any;
134
+ interface OsvVulnSummary {
135
+ id: string;
136
+ summary?: string;
137
+ details?: string;
138
+ published?: string;
139
+ modified?: string;
140
+ references?: { url: string }[];
141
+ }
142
+ interface OsvQueryResponse {
143
+ vulns?: OsvVulnSummary[];
144
+ }
145
+ const data = (await response.json()) as OsvQueryResponse;
109
146
const vulns = data.vulns || [];
110
147
111
- return vulns.slice(0, 10).map((v: any) => ({
148
+ return vulns.slice(0, 10).map((v) => ({
112
149
id: v.id,
113
150
summary: v.summary || "Advisory reported",
114
151
details: v.details || "",
115
- published: v.published,
116
- modified: v.modified,
152
+ published: v.published || new Date().toISOString(),
153
+ modified: v.modified || new Date().toISOString(),
117
154
affectedPackage: {
118
155
name: packageName,
119
156
ecosystem,
120
157
},
121
158
fixedVersions: [],
122
- references: (v.references || []).map((r: any) => r.url).slice(0, 3),
159
+ references: (v.references || []).map((r) => r.url).slice(0, 3),
123
160
}));
124
161
},
125
162
3600, // 1 hour cache
126
163
▼packages/security/src/encryption.ts
+19-7
1
1
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "node:crypto";
2
2
import { config } from "@devpulse/config";
3
3
4
-const ENCRYPTION_SALT = "devpulse-secure-credential-salt-v1";
5
-const SECRET =
6
- config.DISCORD_PUBLIC_KEY || config.DISCORD_CLIENT_ID || "devpulse-default-local-encryption-key";
7
-const DERIVED_KEY = scryptSync(SECRET, ENCRYPTION_SALT, 32);
4
+const ENCRYPTION_SALT = process.env.ENCRYPTION_SALT || "devpulse-secure-credential-salt-v1";
5
+
6
+let _derivedKey: Buffer | null = null;
7
+
8
+function getDerivedKey(): Buffer {
9
+ if (_derivedKey) return _derivedKey;
10
+ if (!config.ENCRYPTION_KEY) {
11
+ throw new Error(
12
+ "Failed to initialize encryption: ENCRYPTION_KEY must be set in environment variables. Generate one with: openssl rand -hex 32",
13
+ );
14
+ }
15
+ _derivedKey = scryptSync(config.ENCRYPTION_KEY, ENCRYPTION_SALT, 32);
16
+ return _derivedKey;
17
+}
8
18
9
19
export function encryptSecret(plainText: string): string {
20
+ const key = getDerivedKey();
10
21
const iv = randomBytes(12);
11
- const cipher = createCipheriv("aes-256-gcm", DERIVED_KEY, iv);
22
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
12
23
let encrypted = cipher.update(plainText, "utf8", "hex");
13
24
encrypted += cipher.final("hex");
14
25
const authTag = cipher.getAuthTag().toString("hex");
16
27
}
17
28
18
29
export function decryptSecret(encryptedPayload: string): string {
30
+ const key = getDerivedKey();
19
31
const parts = encryptedPayload.split(":");
20
32
if (parts.length !== 3) {
21
- throw new Error("Invalid encrypted payload format");
33
+ throw new Error("Failed to decrypt data: invalid encrypted payload format");
22
34
}
23
35
const [ivHex, authTagHex, encryptedHex] = parts;
24
36
const iv = Buffer.from(ivHex, "hex");
25
37
const authTag = Buffer.from(authTagHex, "hex");
26
- const decipher = createDecipheriv("aes-256-gcm", DERIVED_KEY, iv);
38
+ const decipher = createDecipheriv("aes-256-gcm", key, iv);
27
39
decipher.setAuthTag(authTag);
28
40
let decrypted = decipher.update(encryptedHex, "hex", "utf8");
29
41
decrypted += decipher.final("utf8");
30
42
▼packages/trending/src/index.ts
+11-11
50
50
51
51
const res = await fetch(url, { headers });
52
52
if (!res.ok) {
53
- throw new Error(`GitHub search API error: ${res.statusText}`);
53
+ throw new Error(`Failed to search GitHub repositories: ${res.statusText}`);
54
54
}
55
55
56
- const data = (await res.json()) as any;
57
- const items = data.items || [];
56
+ const data = (await res.json()) as unknown;
57
+ const items = (data as { items: unknown[] }).items || [];
58
58
59
- return items.map((item: any) => ({
60
- fullName: item.full_name,
61
- description: item.description || "No description provided",
62
- language: item.language || "Unknown",
63
- stars: item.stargazers_count,
64
- forks: item.forks_count,
65
- url: item.html_url,
59
+ return items.map((item: unknown) => ({
60
+ fullName: (item as { full_name: string }).full_name,
61
+ description: (item as { description?: string }).description || "No description provided",
62
+ language: (item as { language?: string }).language || "Unknown",
63
+ stars: (item as { stargazers_count: number }).stargazers_count,
64
+ forks: (item as { forks_count: number }).forks_count,
65
+ url: (item as { html_url: string }).html_url,
66
66
}));
67
67
}
68
68
}
136
136
logger.debug({ provider: p.name }, "Fetching trending repositories");
137
137
const repos = await p.getTrendingRepos(language, period);
138
138
if (repos && repos.length > 0) return repos;
139
- } catch (err) {
139
+ } catch (err: unknown) {
140
140
logger.warn({ err, provider: p.name }, "Trending provider error, trying next");
141
141
}
142
142
}
143
143