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,11 +44,28 @@ GITHUB_CLIENT_ID=your_github_oauth_client_id_here | ||
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,3 +12,8 @@ dist/ | ||
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,7 +13,7 @@ async function main() { | ||
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,7 +5,24 @@ import { db, users } from "@devpulse/database"; | ||
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,11 +33,17 @@ export interface ServerOptions { | ||
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,11 +90,11 @@ export function buildServer(options?: ServerOptions): FastifyInstance { | ||
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,12 +245,14 @@ export function buildServer(options?: ServerOptions): FastifyInstance { | ||
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,8 +101,10 @@ export const actionsCommand: Command = { | ||
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,8 +136,10 @@ export const activityCommand: Command = { | ||
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,8 +191,10 @@ export const aiCommand: Command = { | ||
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,8 +38,10 @@ export const askCommand: Command = { | ||
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
| @@ -0,0 +1,238 @@ | ||
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,8 +60,10 @@ export const codeCommand: Command = { | ||
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,8 +114,10 @@ export const connectCommand: Command = { | ||
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
| @@ -0,0 +1,24 @@ | ||
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,8 +53,10 @@ export const homeCommand: Command = { | ||
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,6 +2,7 @@ import { actionsCommand } from "./actions.js"; | ||
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,6 +25,7 @@ export const commands: Command[] = [ | ||
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,8 +52,10 @@ export const investigateCommand: Command = { | ||
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,7 +121,8 @@ export const prCommand: Command = { | ||
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,7 +138,10 @@ export const prCommand: Command = { | ||
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,7 +156,8 @@ export const prCommand: Command = { | ||
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,8 +200,10 @@ export const prCommand: Command = { | ||
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,8 +104,10 @@ export const releaseCommand: Command = { | ||
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,8 +88,11 @@ export const repoCommand: Command = { | ||
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,8 +146,10 @@ export const searchCommand: Command = { | ||
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,8 +26,10 @@ export const securityCommand: Command = { | ||
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,8 +186,10 @@ export const settingsCommand: Command = { | ||
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,8 +59,10 @@ export const teamCommand: Command = { | ||
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,8 +109,10 @@ export const toolsCommand: Command = { | ||
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,7 +39,8 @@ export const trendingCommand: Command = { | ||
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,8 +63,10 @@ export const trendingCommand: Command = { | ||
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,12 +6,18 @@ import type { | ||
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,13 +125,13 @@ export const watchCommand: Command = { | ||
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,8 +151,10 @@ export const watchCommand: Command = { | ||
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,8 +41,10 @@ export const whyCommand: Command = { | ||
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,6 +8,7 @@ import { | ||
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,9 +84,9 @@ async function handleSlashCommand(interaction: ChatInputCommandInteraction): Pro | ||
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,7 +112,7 @@ async function handleButtonClick(interaction: ButtonInteraction): Promise<void> | ||
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,9 +128,9 @@ async function handleButtonClick(interaction: ButtonInteraction): Promise<void> | ||
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,9 +172,9 @@ async function handleButtonClick(interaction: ButtonInteraction): Promise<void> | ||
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,8 +243,11 @@ async function handleButtonClick(interaction: ButtonInteraction): Promise<void> | ||
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,11 +54,12 @@ export async function handleMessageCreate(message: Message): Promise<void> { | ||
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,7 +108,7 @@ export async function handleMessageCreate(message: Message): Promise<void> { | ||
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,12 +1,21 @@ | ||
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,7 +42,7 @@ export async function handleReady(client: Client<true>): Promise<void> { | ||
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,7 +63,7 @@ export async function handleReady(client: Client<true>): Promise<void> { | ||
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,7 +76,7 @@ export async function handleReady(client: Client<true>): Promise<void> { | ||
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,7 +84,7 @@ export async function handleReady(client: Client<true>): Promise<void> { | ||
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,9 +103,9 @@ export async function handleReady(client: Client<true>): Promise<void> { | ||
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,10 +4,11 @@ import { createBotClient } from "./index.js"; | ||
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,7 +38,7 @@ export async function startUnifiedApp() { | ||
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,7 +47,7 @@ export async function startUnifiedApp() { | ||
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,7 +67,7 @@ export async function startUnifiedApp() { | ||
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,9 +98,9 @@ export async function startUnifiedApp() { | ||
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,7 +112,7 @@ export async function startUnifiedApp() { | ||
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,6 +32,7 @@ export async function withProgressAnimation<T>( | ||
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,7 +432,13 @@ export function createBlameEmbed(repo: string, path: string, blame: GitHubBlameL | ||
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,7 +481,15 @@ export function createSecurityAuditEmbed( | ||
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,7 +497,7 @@ export function createReleaseNotesEmbed(repo: string, notes: any): EmbedBuilder | ||
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,7 +505,7 @@ export function createReleaseNotesEmbed(repo: string, notes: any): EmbedBuilder | ||
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,7 +513,7 @@ export function createReleaseNotesEmbed(repo: string, notes: any): EmbedBuilder | ||
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,7 +521,7 @@ export function createReleaseNotesEmbed(repo: string, notes: any): EmbedBuilder | ||
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,7 +48,7 @@ async function executeTurso( | ||
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 +7,16 @@ | ||
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,7 +134,7 @@ Respond strictly with valid JSON conforming to: | ||
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,7 +346,7 @@ Respond strictly in valid JSON conforming to: | ||
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,7 +445,7 @@ Respond strictly in valid JSON conforming to: | ||
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,7 +539,7 @@ Respond strictly in valid JSON conforming to: | ||
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,7 +75,7 @@ describe("AI Copilot & Code Intelligence Engine", () => { | ||
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,9 +91,8 @@ export class GenericOpenAiCompatibleProvider implements AIProvider { | ||
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,11 +100,7 @@ export class OpenCodeZenProvider implements AIProvider { | ||
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,7 +108,7 @@ export class OpenCodeZenProvider implements AIProvider { | ||
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,7 +129,6 @@ export class OpenCodeZenProvider implements AIProvider { | ||
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,11 +152,124 @@ export class OpenCodeZenProvider implements AIProvider { | ||
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,15 +280,29 @@ export function getAiProvider(): AIProvider { | ||
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,6 +3,16 @@ import { db, repositories, users } from "@devpulse/database"; | ||
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,7 +62,7 @@ export class AnalyticsService { | ||
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,23 +2,37 @@ import { config } from "@devpulse/config"; | ||
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,7 +119,7 @@ export class UpstashRedisProvider implements CacheProvider { | ||
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,7 +132,7 @@ export class UpstashRedisProvider implements CacheProvider { | ||
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,7 +140,7 @@ export class UpstashRedisProvider implements CacheProvider { | ||
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,7 +153,7 @@ export class UpstashRedisProvider implements CacheProvider { | ||
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,7 +162,7 @@ export class UpstashRedisProvider implements CacheProvider { | ||
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,7 +194,7 @@ export class UpstashRedisProvider implements CacheProvider { | ||
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,6 +241,10 @@ export function getCacheProvider(): CacheProvider { | ||
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,15 +52,20 @@ export const ConfigSchema = z.object({ | ||
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,7 +82,7 @@ export function loadConfig(customEnv: Record<string, string | undefined> = proce | ||
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,7 +89,9 @@ export function decodeJwt(token: string): DecodedJwt { | ||
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,8 +5,9 @@ export function formatJson(input: string, indent = 2): string { | ||
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,8 +15,9 @@ export function minifyJson(input: string): string { | ||
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,8 +26,8 @@ export function validateJson(input: string): { valid: boolean; error?: string; t | ||
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,8 +35,9 @@ export function formatYaml(input: string): string { | ||
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,8 +45,9 @@ export function yamlToJson(yamlInput: string): string { | ||
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,8 +55,9 @@ export function jsonToYaml(jsonInput: string): string { | ||
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,8 +25,10 @@ export function testRegex( | ||
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,8 +52,10 @@ export async function lookupDns( | ||
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,7 +127,7 @@ export async function inspectSslCertificate(domain: string, port = 443): Promise | ||
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,7 +46,7 @@ async function fetchNpmPackage(name: string): Promise<PackageMetadata> { | ||
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,7 +86,7 @@ async function fetchPypiPackage(name: string): Promise<PackageMetadata> { | ||
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,9 +119,11 @@ export async function validateSafeUrl(rawUrl: string): Promise<URL> { | ||
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 +8,9 @@ export function base64Decode(input: string): string { | ||
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,8 +21,9 @@ export function urlEncode(input: string): string { | ||
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,8 +48,13 @@ export function parseCronExpression( | ||
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,8 +71,10 @@ export function parseCronExpression( | ||
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,7 +111,9 @@ export function convertTimezone( | ||
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,7 +69,7 @@ export class GitHubClient { | ||
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,7 +131,25 @@ export class GitHubClient { | ||
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,18 +301,44 @@ export class GitHubClient { | ||
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,7 +353,7 @@ export class GitHubClient { | ||
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,18 +383,29 @@ export class GitHubClient { | ||
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,25 +419,40 @@ export class GitHubClient { | ||
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,11 +497,11 @@ export class GitHubClient { | ||
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,120 +510,151 @@ export class GitHubClient { | ||
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,15 +664,19 @@ export class GitHubClient { | ||
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,14 +701,18 @@ export class GitHubClient { | ||
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,8 +732,17 @@ export class GitHubClient { | ||
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,23 +760,29 @@ export class GitHubClient { | ||
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,17 +864,26 @@ export class GitHubClient { | ||
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,36 +912,60 @@ export class GitHubClient { | ||
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,24 +989,40 @@ export class GitHubClient { | ||
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,9 +1032,21 @@ export class GitHubClient { | ||
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,17 +1172,27 @@ export class GitHubClient { | ||
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,26 +1210,39 @@ export class GitHubClient { | ||
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,12 +1251,25 @@ export class GitHubClient { | ||
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,7 +1280,9 @@ export class GitHubClient { | ||
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,15 +1293,23 @@ export class GitHubClient { | ||
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,16 +1323,29 @@ export class GitHubClient { | ||
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,7 +1359,17 @@ export class GitHubClient { | ||
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,14 +1387,14 @@ export class GitHubClient { | ||
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,9 +150,10 @@ export class MonitoringService { | ||
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,7 +228,7 @@ export class MonitoringService { | ||
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,15 +64,28 @@ export class AlgoliaHnNewsProvider implements NewsProvider { | ||
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,10 +143,17 @@ export class DevToNewsProvider implements NewsProvider { | ||
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,7 +183,7 @@ export class NewsService { | ||
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,7 +2,7 @@ import { cache } from "@devpulse/cache"; | ||
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,13 +97,17 @@ export class ReminderService { | ||
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,13 +125,13 @@ export class SchedulerRunner { | ||
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,6 +143,7 @@ export class SchedulerRunner { | ||
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,7 +170,7 @@ export class SchedulerRunner { | ||
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,7 +187,7 @@ export class SchedulerRunner { | ||
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,10 +36,30 @@ export class SecurityService { | ||
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,7 +71,9 @@ export class SecurityService { | ||
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,7 +107,11 @@ export class SecurityService { | ||
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,24 +128,35 @@ export class SecurityService { | ||
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,14 +1,25 @@ | ||
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,14 +27,15 @@ export function encryptSecret(plainText: string): string { | ||
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,19 +50,19 @@ export class GitHubApiTrendingProvider implements TrendingProvider { | ||
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,7 +136,7 @@ export class TrendingService { | ||
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 | |