Skip to content

Commit 45ba40a

Browse files
feat: add core logic
1 parent ade5fa5 commit 45ba40a

File tree

8 files changed

+460
-18
lines changed

8 files changed

+460
-18
lines changed

.env

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
GROQ_API_KEY=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
2-
HTTPS_PROXY=http://127.0.0.1:7890

package-lock.json

Lines changed: 299 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,10 @@
5252
"typescript": "^5.9.2"
5353
},
5454
"dependencies": {
55+
"enquirer": "^2.4.1",
5556
"groq-sdk": "^0.32.0",
5657
"https-proxy-agent": "^7.0.6",
58+
"keytar": "^7.9.0",
5759
"ora": "^8.2.0",
5860
"sade": "^1.8.1"
5961
},

src/ai/explain.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import Groq from "groq-sdk";
2+
import Enquirer from "enquirer";
3+
import keytar from "keytar";
4+
import { prompt as aiPrompt } from "./prompt";
5+
import ora from "ora";
6+
import { ACCOUNT_NAME, SERVICE_NAME } from "../utils/constants";
7+
import { HttpsProxyAgent } from "https-proxy-agent";
8+
9+
function isValidUrl(input: string) {
10+
try {
11+
new URL(input);
12+
return true;
13+
} catch (error) {
14+
return false;
15+
}
16+
}
17+
18+
async function getApiKey() {
19+
try {
20+
const { prompt } = Enquirer;
21+
// 1. Check env
22+
if (process.env.GROQ_API_KEY) {
23+
return process.env.GROQ_API_KEY;
24+
}
25+
26+
// 2. Check keytar
27+
const savedKey = await keytar.getPassword(SERVICE_NAME, ACCOUNT_NAME);
28+
if (savedKey) {
29+
return savedKey;
30+
}
31+
32+
// 3. Prompt user
33+
const { apiKey: newKey } = await prompt({
34+
type: "password",
35+
name: "apiKey",
36+
message: "Enter your Groq API key:",
37+
validate: (
38+
value: string,
39+
) => (value.trim() === "" ? "API key cannot be empty" : true),
40+
initial: "",
41+
result: (value: string) => value.trim(),
42+
stdin: process.stdin,
43+
stdout: process.stdout,
44+
}) as { apiKey: string };
45+
46+
await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, newKey);
47+
console.log("\n✅ API key saved securely!");
48+
return newKey;
49+
} catch (error) {
50+
console.error(error);
51+
process.exit(1);
52+
}
53+
}
54+
55+
async function getHttpProxyAgent(): Promise<
56+
HttpsProxyAgent<string> | undefined
57+
> {
58+
const envProxy = process.env.HTTPS_PROXY || process.env.https_proxy;
59+
60+
if (envProxy) {
61+
console.log("Using proxy from environment variables");
62+
return new HttpsProxyAgent(envProxy);
63+
}
64+
65+
const { setProxy } = await Enquirer.prompt<{ setProxy: boolean }>({
66+
type: "confirm",
67+
name: "setProxy",
68+
message: "Do you want to set a proxy?",
69+
initial: false,
70+
});
71+
72+
if (!setProxy) return undefined;
73+
74+
const { proxyAddr } = await Enquirer.prompt<{ proxyAddr: string }>({
75+
type: "input",
76+
name: "proxyAddr",
77+
message: "Enter proxy address (e.g., http://user:pass@localhost:7890):",
78+
validate: (value) => isValidUrl(value) || "Invalid URL format",
79+
});
80+
81+
return new HttpsProxyAgent(proxyAddr);
82+
}
83+
84+
// Main function
85+
export async function explain(regex: string) {
86+
try {
87+
const apiKey = await getApiKey();
88+
console.log("apiKey: ", apiKey);
89+
const httpAgent = await getHttpProxyAgent();
90+
91+
const groq = new Groq({ apiKey, ...(httpAgent ? { httpAgent } : {}) });
92+
93+
const spinner = ora("loading ....").start();
94+
const completion = await groq.chat.completions.create({
95+
model: "openai/gpt-oss-20b",
96+
messages: [
97+
{ role: "system", content: "You are a regex explainer." },
98+
{ role: "user", content: aiPrompt(regex) },
99+
],
100+
});
101+
102+
spinner.stop().clear();
103+
104+
console.log("\n📖 Explanation:\n");
105+
if (
106+
completion.choices && completion.choices[0] &&
107+
completion.choices[0].message
108+
) {
109+
console.log(completion.choices[0].message.content);
110+
} else {
111+
console.error(
112+
"Error: Completion result is undefined or malformed.",
113+
);
114+
}
115+
} catch (error) {
116+
console.error(error);
117+
process.exit(1);
118+
}
119+
}

src/ai/prompt.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
export const prompt = (regex: string): string => (`
2+
**Role:** You are RegExpert, a world-class expert in regular expressions. Your sole purpose is to analyze, explain, and provide insights on regex patterns provided by the user.
3+
4+
**Context:** The user will provide you with a regular expression pattern. Your task is to generate a comprehensive, easy-to-understand explanation.
5+
6+
**Instructions:**
7+
1. **Break Down the Pattern:** Deconstruct the regex into its core components.
8+
2. **Explain Each Part:** For each significant component (anchors, character classes, quantifiers, groups, lookarounds, flags, etc.), provide a plain-English description of what it does.
9+
3. **Overall Purpose:** Synthesize the components to describe the overall purpose of the regex in one or two sentences.
10+
4. **Example Matches (Optional):** If possible and clear, provide 1-2 short examples of text that would match and 1 that would not.
11+
5. **Tone:** Be professional, clear, and pedagogical. Avoid unnecessary jargon, but don't oversimplify for complex patterns. Assume the user has basic programming knowledge but might be new to regex.
12+
13+
**Output Format Requirements:**
14+
* **Do not use markdown** (e.g., no \`, **bold**, or headings). This output will be displayed in a terminal.
15+
* Use clear spacing and colons for structure.
16+
* Keep the explanation concise but thorough.
17+
18+
**Safety & Ethics:**
19+
- If the pattern is empty, overly complex to the point of being potentially harmful (e.g., ReDoS vectors), or not a regex, politely decline to analyze it and explain why.
20+
- Do not generate or explain regex patterns for obviously malicious purposes (e.g., matching credit card numbers for extraction).
21+
22+
**Input from user:**
23+
${regex}
24+
`);

src/utils/agent.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { HttpsProxyAgent } from "https-proxy-agent";
2+
3+
export function createHttpsProxyAgent(): HttpsProxyAgent<string> | undefined {
4+
const proxy = process.env?.HTTPS_PROXY as string; // replace with your proxy
5+
if (proxy) {
6+
return new HttpsProxyAgent(proxy);
7+
}
8+
return undefined;
9+
}

src/utils/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const SERVICE_NAME = "regexplain";
2+
export const ACCOUNT_NAME = "user"; // could later be email/username if multi-user

src/utils/program.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import sade from "sade";
22
import { description, version } from "../../package.json";
3+
import { explain } from "../ai/explain";
34

45
export function runProgram() {
56
const program = sade("regexplain <regex>");
@@ -8,5 +9,9 @@ export function runProgram() {
89
.version(version)
910
.describe(description)
1011
.action((regex: string) => {
12+
explain(regex).catch((error) => {
13+
console.error("Error:", error.message);
14+
process.exit(1);
15+
});
1116
}).parse(process.argv);
1217
}

0 commit comments

Comments
 (0)