Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
# Convex deployment configuration
CONVEX_DEPLOYMENT=
NEXT_PUBLIC_CONVEX_URL=
# Convex Service Role Key (Required for securing public functions)
# Generate a secure random string and add it to your Convex environment variables
# CONVEX_SERVICE_ROLE_KEY=


# WorkOS Authentication (Required for user management and conversation persistence)
WORKOS_API_KEY=sk_example_123456789
Expand Down Expand Up @@ -46,9 +50,6 @@ OPENAI_API_KEY=your_openai_api_key_here
# NEXT_PUBLIC_POSTHOG_KEY="phc_your_project_key_here"
# NEXT_PUBLIC_POSTHOG_HOST="https://app.posthog.com"

# Convex Service Role Key (Required for securing public functions)
# Generate a secure random string and add it to your Convex environment variables
# CONVEX_SERVICE_ROLE_KEY=

# Stripe
# STRIPE_API_KEY=
# STRIPE_API_KEY=
# NEXT_PUBLIC_BASE_URL=http://localhost:3000/
98 changes: 33 additions & 65 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,90 +1,58 @@
# HackerAI
<p align="center">
<h1 align="center">HackerAI</h1>
</p>

_Your AI-Powered Penetration Testing Assistant_
<p align="center">
Your AI-Powered Penetration Testing Assistant
</p>

---
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-Apache%202.0%20with%20Commercial%20Restrictions-red.svg" alt="License: Apache 2.0 with Commercial Restrictions"/>
</a>
<a href="https://hackerai.co/">
<img src="https://img.shields.io/badge/Demo-Website-blue" alt="Demo: Website"/>
</a>
</p>

## 🚀 Quick Start
<p align="center">
Demo: <a href="https://hackerai.co/">https://hackerai.co/</a>
</p>

### 1. Install Dependencies
## Getting started

```bash
pnpm install
```

### 2. Set Up Authentication & Database

HackerAI uses **Convex** for real-time database and **WorkOS** for authentication to provide persistent conversations and user management.
### Prerequisites

#### Configure Convex (Required)
You'll need an [OpenRouter](https://openrouter.ai/) account, an [OpenAI](https://platform.openai.com/) account, a [Convex](https://www.convex.dev/) account, and a [WorkOS](https://workos.com/) account.

1. Create a Convex account at [convex.dev](https://convex.dev/)
2. Initialize Convex in your project:
```bash
npx convex dev
```
3. Follow the prompts to create a new project
**Optional:** To execute terminal commands in isolated containers instead of your local machine, add web search functionality, or enable other advanced features, you can fill out the optional environment variables after running the setup script.

#### Configure WorkOS Authentication (Required)

1. Create a WorkOS account at [workos.com](https://workos.com/)
2. Create a new project and get your API credentials
3. Configure redirect URI: `http://localhost:3000/callback`

### 3. Configure Environment

Create `.env.local` from the example file:
### Clone the repo

```bash
cp .env.local.example .env.local
git clone https://github.com/hackerai-tech/hackerai.git
```

Then fill in your API keys and configuration values in the `.env.local` file.

### 4. Deploy Convex Functions
### Navigate to the project directory

```bash
npx convex deploy
cd hackerai
```

### 5. Launch Application
### Install dependencies

```bash
pnpm dev
pnpm install
```

Visit **[http://localhost:3000](http://localhost:3000)** and start your penetration testing journey! 🎯
### Run the setup script

---

## 🔑 Required Services

| Service | Purpose | Get Started |
| -------------- | ----------------------------------------- | --------------------------------------- |
| **Convex** | Real-time database & conversation storage | [convex.dev](https://convex.dev/) |
| **WorkOS** | User authentication & session management | [workos.com](https://workos.com/) |
| **OpenRouter** | LLM access (Claude, GPT, etc.) | [openrouter.ai](https://openrouter.ai/) |
| **OpenAI** | Moderation API | [openai.com](https://openai.com/api/) |

## 🔧 Optional Enhancements

### Sandbox Mode

Execute terminal commands in isolated containers instead of your local machine:

| Service | Purpose | Get API Key |
| ------- | ------------------------- | --------------------------- |
| **E2B** | Secure isolated execution | [e2b.dev](https://e2b.dev/) |

```env
TERMINAL_EXECUTION_MODE=sandbox
E2B_API_KEY=your_e2b_api_key_here
```bash
pnpm run setup
```

### Web Search
### Start the development server

Enable AI to search the web for up-to-date information:

```env
EXA_API_KEY=your_exa_api_key_here
```bash
pnpm run dev
```
7 changes: 4 additions & 3 deletions app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,6 @@ export async function POST(req: NextRequest) {
const { userId, isPro } = await getUserIDAndPro(req);
const userLocation = geolocation(req);

// Check rate limit for the user
await checkRateLimit(userId, isPro);

// Truncate messages to stay within token limit with file tokens included
const truncatedMessages = await truncateMessagesWithFileTokens(messages);

Expand All @@ -70,6 +67,9 @@ export async function POST(req: NextRequest) {
regenerate,
});

// Check rate limit for the user
await checkRateLimit(userId, isPro);

// Process chat messages with moderation and analytics
const posthog = PostHogClient();
const { executionMode, processedMessages, hasMediaFiles } =
Expand Down Expand Up @@ -162,6 +162,7 @@ export async function POST(req: NextRequest) {
for (const message of messages) {
await saveMessage({
chatId,
userId,
message,
});
}
Expand Down
22 changes: 21 additions & 1 deletion app/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import { ScrollToBottomButton } from "./ScrollToBottomButton";
import { AttachmentButton } from "./AttachmentButton";
import { useFileUpload } from "../hooks/useFileUpload";
import { useEffect, useRef } from "react";
import { countInputTokens, MAX_TOKENS } from "@/lib/token-utils";
import { toast } from "sonner";

interface ChatInputProps {
onSubmit: (e: React.FormEvent) => void;
Expand Down Expand Up @@ -85,11 +87,29 @@ export const ChatInput = ({
[isGenerating, onStop],
);

// Handle paste events for file uploads
// Handle paste events for file uploads and token validation
useEffect(() => {
const handlePaste = async (e: ClipboardEvent) => {
// Only handle paste if the textarea is focused
if (textareaRef.current === document.activeElement) {
// Check if pasting text content
const clipboardData = e.clipboardData;
if (clipboardData) {
const pastedText = clipboardData.getData("text");

if (pastedText) {
// Check token limit for the pasted text only
const tokenCount = countInputTokens(pastedText, []);
if (tokenCount > MAX_TOKENS) {
e.preventDefault();
toast.error("Content is too long to paste", {
description: `The content you're trying to paste is too large (${tokenCount.toLocaleString()} tokens). Please copy a smaller amount.`,
});
return;
}
}
}

const filesProcessed = await handlePasteEvent(e);
// If files were processed, the event.preventDefault() is already called
// in handlePasteEvent, so no additional action needed here
Expand Down
82 changes: 82 additions & 0 deletions app/components/FeedbackInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import TextareaAutosize from "react-textarea-autosize";

interface FeedbackInputProps {
onSend: (details: string) => Promise<void>;
onCancel: () => void;
}

export const FeedbackInput = ({ onSend, onCancel }: FeedbackInputProps) => {
const [details, setDetails] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);

const handleSend = async () => {
if (!details.trim()) return;

setIsSubmitting(true);
try {
await onSend(details.trim());
setDetails("");
} catch (error) {
console.error("Failed to send feedback:", error);
} finally {
setIsSubmitting(false);
}
};

const handleCancel = () => {
setDetails("");
onCancel();
};

const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSend();
} else if (e.key === "Escape") {
e.preventDefault();
handleCancel();
}
// Allow Shift+Enter for new lines
};

return (
<div className="mt-2 p-3 bg-muted/50 rounded-lg border border-border animate-in fade-in-0 slide-in-from-bottom-2 duration-200">
<div className="flex flex-col space-y-3">
<div className="flex-1">
<TextareaAutosize
value={details}
onChange={(e) => setDetails(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={"What went wrong?"}
className="flex rounded-md border-input focus-visible:outline-none focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 overflow-hidden flex-1 bg-transparent p-2 border-0 focus-visible:ring-0 focus-visible:ring-offset-0 w-full placeholder:text-muted-foreground text-[15px] shadow-none resize-none min-h-[36px]"
rows={2}
maxRows={6}
autoFocus
disabled={isSubmitting}
/>
</div>
<div className="flex justify-end space-x-2">
<Button
size="sm"
variant="ghost"
onClick={handleCancel}
disabled={isSubmitting}
className="shrink-0"
>
Cancel
</Button>
<Button
size="sm"
onClick={handleSend}
disabled={!details.trim() || isSubmitting}
className="shrink-0"
>
Send
</Button>
</div>
</div>
</div>
);
};
Loading