Skip to content
Open
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.env
.env*
__pycache__/
*.pyc
.venv/
Expand Down
48 changes: 48 additions & 0 deletions app/app/api/auth/github/callback/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { NextRequest, NextResponse } from "next/server";

const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000";
const CLIENT_ID = process.env.GITHUB_CLIENT_ID!;
const CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET!;

export async function GET(req: NextRequest) {
const code = req.nextUrl.searchParams.get("code");
const error = req.nextUrl.searchParams.get("error");

if (error || !code) {
return NextResponse.redirect(
`${BASE_URL}/test?github_error=${error || "missing_code"}`
);
}

const tokenRes = await fetch("https://github.com/login/oauth/access_token", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
code,
redirect_uri: `${BASE_URL}/api/auth/github/callback`,
}),
});

const data = await tokenRes.json();

if (data.error || !data.access_token) {
return NextResponse.redirect(
`${BASE_URL}/test?github_error=${data.error || "token_failed"}`
);
}

const response = NextResponse.redirect(`${BASE_URL}/test?github_connected=true`);
response.cookies.set("github_token", data.access_token, {
httpOnly: true,
secure: true,
sameSite: "none",
maxAge: 60 * 60 * 24 * 30, // GitHub tokens don't expire unless revoked
path: "/",
});
return response;
}
14 changes: 14 additions & 0 deletions app/app/api/auth/github/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { NextResponse } from "next/server";

const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000";
const CLIENT_ID = process.env.GITHUB_CLIENT_ID!;

const SCOPES = ["notifications", "read:user", "repo"].join(" ");

export async function GET() {
const url = new URL("https://github.com/login/oauth/authorize");
url.searchParams.set("client_id", CLIENT_ID);
url.searchParams.set("redirect_uri", `${BASE_URL}/api/auth/github/callback`);
url.searchParams.set("scope", SCOPES);
return NextResponse.redirect(url.toString());
}
53 changes: 53 additions & 0 deletions app/app/api/auth/gmail/callback/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { NextRequest, NextResponse } from "next/server";

const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000";
const CLIENT_ID = process.env.GOOGLE_CLIENT_ID!;
const CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET!;
const REDIRECT_URI = `${BASE_URL}/api/auth/gmail/callback`;

export async function GET(req: NextRequest) {
const code = req.nextUrl.searchParams.get("code");
const error = req.nextUrl.searchParams.get("error");

if (error || !code) {
return NextResponse.redirect(`${BASE_URL}/test?gmail_error=${error || "missing_code"}`);
}

const tokenRes = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
code,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
redirect_uri: REDIRECT_URI,
grant_type: "authorization_code",
}).toString(),
});

const data = await tokenRes.json();

if (data.error || !data.access_token) {
return NextResponse.redirect(`${BASE_URL}/test?gmail_error=${data.error || "token_failed"}`);
}

const response = NextResponse.redirect(`${BASE_URL}/test?gmail_connected=true`);
response.cookies.set("gmail_token", data.access_token, {
httpOnly: true,
secure: true,
sameSite: "none",
maxAge: data.expires_in || 3600,
path: "/",
});
// Store refresh token separately (long-lived)
if (data.refresh_token) {
response.cookies.set("gmail_refresh_token", data.refresh_token, {
httpOnly: true,
secure: true,
sameSite: "none",
maxAge: 60 * 60 * 24 * 30,
path: "/",
});
}
return response;
}
20 changes: 20 additions & 0 deletions app/app/api/auth/gmail/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { NextResponse } from "next/server";

const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000";
const CLIENT_ID = process.env.GOOGLE_CLIENT_ID!;

const SCOPES = [
"https://www.googleapis.com/auth/gmail.readonly",
"https://www.googleapis.com/auth/userinfo.email",
].join(" ");

export async function GET() {
const url = new URL("https://accounts.google.com/o/oauth2/v2/auth");
url.searchParams.set("client_id", CLIENT_ID);
url.searchParams.set("redirect_uri", `${BASE_URL}/api/auth/gmail/callback`);
url.searchParams.set("response_type", "code");
url.searchParams.set("scope", SCOPES);
url.searchParams.set("access_type", "offline");
url.searchParams.set("prompt", "consent");
return NextResponse.redirect(url.toString());
}
73 changes: 73 additions & 0 deletions app/app/api/auth/slack/callback/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { NextRequest, NextResponse } from "next/server";
import { storeCredential } from "@/lib/backend-client";

const SLACK_CLIENT_ID = process.env.SLACK_CLIENT_ID!;
const SLACK_CLIENT_SECRET = process.env.SLACK_CLIENT_SECRET!;
const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000";
const REDIRECT_URI = `${BASE_URL}/api/auth/slack/callback`;

export async function GET(req: NextRequest) {
const code = req.nextUrl.searchParams.get("code");
const error = req.nextUrl.searchParams.get("error");

if (error || !code) {
return NextResponse.redirect(
`${BASE_URL}/test?error=${error || "missing_code"}`
);
}

// Exchange code for token
const params = new URLSearchParams({
client_id: SLACK_CLIENT_ID,
client_secret: SLACK_CLIENT_SECRET,
code,
redirect_uri: REDIRECT_URI,
});

const tokenRes = await fetch("https://slack.com/api/oauth.v2.access", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: params.toString(),
});

const data = await tokenRes.json();

if (!data.ok) {
return NextResponse.redirect(
`${BASE_URL}/test?error=${data.error}`
);
}

const userToken = data.authed_user?.access_token;
if (!userToken) {
return NextResponse.redirect(
`${BASE_URL}/test?error=no_user_token`
);
}

const scopes = (data.authed_user?.scope || "").split(",").filter(Boolean);

// Store in backend credential vault (source of truth)
try {
const backendRes = await storeCredential("slack", userToken, scopes);
if (!backendRes.ok) {
console.error("Backend credential store failed:", await backendRes.text());
}
} catch (err) {
console.error("Could not reach backend, falling back to cookie:", err);
// Fallback: store in cookie so the app still works without the backend
const fallback = NextResponse.redirect(
`${BASE_URL}/test?connected=true&mode=local`
);
fallback.cookies.set("slack_token", userToken, {
httpOnly: true,
secure: true,
sameSite: "none",
maxAge: 60 * 60 * 24 * 30,
path: "/",
});
return fallback;
}

return NextResponse.redirect(`${BASE_URL}/test?connected=true`);
}
29 changes: 29 additions & 0 deletions app/app/api/auth/slack/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { NextResponse } from "next/server";
import { getSlackOAuthUrl } from "@/lib/backend-client";

const SLACK_CLIENT_ID = process.env.SLACK_CLIENT_ID!;
const BACKEND_API_KEY = process.env.BACKEND_API_KEY;
const REDIRECT_URI = `${process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"}/api/auth/slack/callback`;

const SCOPES = [
"channels:history",
"channels:read",
"groups:history",
"groups:read",
"users:read",
"reactions:read",
].join(",");

export async function GET() {
// If backend is configured, route through it (tokens stored in encrypted vault)
if (BACKEND_API_KEY) {
return NextResponse.redirect(getSlackOAuthUrl());
}

// No backend — do OAuth directly in the frontend (cookie fallback)
const url = new URL("https://slack.com/oauth/v2/authorize");
url.searchParams.set("client_id", SLACK_CLIENT_ID);
url.searchParams.set("user_scope", SCOPES);
url.searchParams.set("redirect_uri", REDIRECT_URI);
return NextResponse.redirect(url.toString());
}
44 changes: 44 additions & 0 deletions app/app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { NextRequest } from "next/server";
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

const SYSTEM_PROMPTS: Record<string, string> = {
slack: `You are a Slack Agent. Summarize discussions, extract action items, flag blockers. Be concise and action-oriented. Never just describe — always recommend next steps.`,
gmail: `You are an Inbox Agent. Summarize emails, detect urgency, suggest replies and actions. Be concise. Always recommend what to do next.`,
github: `You are a GitHub Agent. Track PRs, issues, and code review status. Flag blockers, suggest review priorities. Be direct.`,
global: `You are an AI assistant with visibility across Slack, Gmail, and GitHub. Cross-reference information across tools, identify patterns and blockers, and suggest concrete next steps.`,
};

export async function POST(req: NextRequest) {
const { messages, agentId, context } = await req.json();

const system = context
? `${SYSTEM_PROMPTS[agentId] || SYSTEM_PROMPTS.global}\n\nCurrent data context:\n${context}`
: SYSTEM_PROMPTS[agentId] || SYSTEM_PROMPTS.global;

const stream = client.messages.stream({
model: "claude-sonnet-4-6",
max_tokens: 1024,
system,
messages,
});

const readable = new ReadableStream({
async start(controller) {
for await (const chunk of stream) {
if (
chunk.type === "content_block_delta" &&
chunk.delta.type === "text_delta"
) {
controller.enqueue(new TextEncoder().encode(chunk.delta.text));
}
}
controller.close();
},
});

return new Response(readable, {
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
}
36 changes: 36 additions & 0 deletions app/app/api/employees/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { NextRequest, NextResponse } from "next/server";
import { hireEmployee, listEmployees, fireEmployee } from "@/lib/backend-client";

export async function GET() {
try {
const res = await listEmployees();
if (res.ok) {
const data = await res.json();
return NextResponse.json({ ok: true, employees: data });
}
return NextResponse.json({ ok: false, employees: [] });
} catch {
return NextResponse.json({ ok: false, employees: [] });
}
}

export async function POST(req: NextRequest) {
const { role, config } = await req.json();
try {
const res = await hireEmployee(role, config);
const data = await res.json();
return NextResponse.json(data, { status: res.ok ? 201 : 500 });
} catch {
return NextResponse.json({ error: "Backend unavailable" }, { status: 503 });
}
}

export async function DELETE(req: NextRequest) {
const { agentId } = await req.json();
try {
await fireEmployee(agentId);
return NextResponse.json({ ok: true });
} catch {
return NextResponse.json({ error: "Backend unavailable" }, { status: 503 });
}
}
Loading