Fix: OAuth screen

This commit is contained in:
Harshith Mullapudi 2025-07-19 16:42:38 +05:30
parent e50c5a7c64
commit c4f7de9049
19 changed files with 314 additions and 263 deletions

View File

@ -3,6 +3,7 @@ import { useEffect, useState, useCallback, useRef } from "react";
import { AutoSizer, List, type ListRowRenderer } from "react-virtualized"; import { AutoSizer, List, type ListRowRenderer } from "react-virtualized";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import { Button } from "../ui"; import { Button } from "../ui";
import { LoaderCircle } from "lucide-react";
type ConversationItem = { type ConversationItem = {
id: string; id: string;
@ -179,28 +180,27 @@ export const ConversationList = ({
return ( return (
<div className="flex h-full flex-col pt-1 pl-1"> <div className="flex h-full flex-col pt-1 pl-1">
<div className="group grow overflow-hidden"> {!isLoading && conversations.length > 0 && (
<AutoSizer> <div className="group grow overflow-hidden">
{({ height, width }) => ( <AutoSizer>
<List {({ height, width }) => (
height={height} <List
width={width} height={height}
rowCount={rowCount} width={width}
rowHeight={32} // Slightly taller for better click area rowCount={rowCount}
rowRenderer={rowRenderer} rowHeight={32} // Slightly taller for better click area
overscanRowCount={5} rowRenderer={rowRenderer}
/> overscanRowCount={5}
)} />
</AutoSizer> )}
</div> </AutoSizer>
</div>
)}
{isLoading && conversations.length === 0 && ( {isLoading && conversations.length === 0 && (
<div className="flex items-center justify-center p-8"> <div className="flex items-center justify-center p-8">
<div className="flex items-center space-x-2"> <div className="flex flex-col items-center gap-2">
<div className="border-primary h-4 w-4 animate-spin rounded-full border-2 border-t-transparent" /> <LoaderCircle className="text-primary h-4 w-4 animate-spin" />
<span className="text-muted-foreground text-sm">
Loading conversations...
</span>
</div> </div>
</div> </div>
)} )}

View File

@ -0,0 +1,19 @@
import type { IconProps } from "./types";
export function Arrows({ size = 18, className }: IconProps) {
return (
<svg
className={className}
width={size}
height={size}
viewBox="0 0 16 16"
fill="lch(38.893% 1 282.863 / 1)"
role="img"
focusable="false"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M5.378 7.5a.6.6 0 0 1 .6.6l-.002 1.774c2.375.224 8.027.917 8.027 1.328 0 .311-2.675.796-8.024 1.453l-.001 1.747a.6.6 0 0 1-.992.454L1.14 11.548a.4.4 0 0 1 0-.607l3.848-3.297a.6.6 0 0 1 .39-.144Zm4.79-6.291a.6.6 0 0 1 .846-.064l3.847 3.309a.4.4 0 0 1 0 .607l-3.848 3.296a.6.6 0 0 1-.99-.455V6.128C7.65 5.904 1.998 5.21 1.998 4.799c0-.31 2.675-.795 8.024-1.452l.001-1.747a.6.6 0 0 1 .145-.391Z"></path>
</svg>
);
}

View File

@ -1,2 +1,3 @@
export * from "./slack-icon"; export * from "./slack-icon";
export * from "./linear-icon"; export * from "./linear-icon";
export * from "./arrows";

View File

@ -1,5 +1,5 @@
import React, { useMemo } from "react"; import React, { useMemo } from "react";
import { Search } from "lucide-react"; import { Layout, LayoutGrid, Search } from "lucide-react";
import { IntegrationCard } from "./integration-card"; import { IntegrationCard } from "./integration-card";
interface IntegrationGridProps { interface IntegrationGridProps {
@ -25,8 +25,8 @@ export function IntegrationGrid({
if (integrations.length === 0) { if (integrations.length === 0) {
return ( return (
<div className="mt-20 flex flex-col items-center justify-center"> <div className="mt-20 flex flex-col items-center justify-center">
<Search className="text-muted-foreground mb-2 h-12 w-12" /> <LayoutGrid className="text-muted-foreground mb-2 h-10 w-10" />
<h3 className="text-lg font-medium">No integrations found</h3> <h3 className="text-lg">No integrations found</h3>
</div> </div>
); );
} }

View File

@ -1,11 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { import { ListFilter, X } from "lucide-react";
ChevronsUpDown,
Filter,
FilterIcon,
ListFilter,
X,
} from "lucide-react";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { import {
Popover, Popover,

View File

@ -3,7 +3,6 @@ import {
SidebarGroup, SidebarGroup,
SidebarGroupContent, SidebarGroupContent,
SidebarMenu, SidebarMenu,
SidebarMenuButton,
SidebarMenuItem, SidebarMenuItem,
} from "../ui/sidebar"; } from "../ui/sidebar";
import { useLocation, useNavigate } from "@remix-run/react"; import { useLocation, useNavigate } from "@remix-run/react";

View File

@ -8,20 +8,13 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "../ui/dropdown-menu"; } from "../ui/dropdown-menu";
import { import { SidebarMenu, SidebarMenuItem, useSidebar } from "../ui/sidebar";
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "../ui/sidebar";
import type { User } from "~/models/user.server"; import type { User } from "~/models/user.server";
import { Button } from "../ui"; import { Button } from "../ui";
import { cn } from "~/lib/utils"; import { useNavigate } from "@remix-run/react";
import { useLocation, useNavigate } from "@remix-run/react";
export function NavUser({ user }: { user: User }) { export function NavUser({ user }: { user: User }) {
const { isMobile } = useSidebar(); const { isMobile } = useSidebar();
const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
@ -55,7 +48,7 @@ export function NavUser({ user }: { user: User }) {
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem
className="flex gap-2" className="flex gap-2"
onClick={() => navigate("/settings")} onClick={() => navigate("/settings/api")}
> >
<Settings size={16} /> <Settings size={16} />
Settings Settings

View File

@ -1,4 +1,8 @@
import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from "@remix-run/node"; import {
type ActionFunctionArgs,
type LoaderFunctionArgs,
json,
} from "@remix-run/node";
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from "@prisma/client";
import { requireAuth } from "~/utils/auth-helper"; import { requireAuth } from "~/utils/auth-helper";
import crypto from "crypto"; import crypto from "crypto";
@ -63,12 +67,23 @@ export const action = async ({ request }: ActionFunctionArgs) => {
try { try {
const body = await request.json(); const body = await request.json();
const { name, description, redirectUris, allowedScopes, requirePkce, logoUrl, homepageUrl } = body; const {
name,
description,
redirectUris,
allowedScopes,
requirePkce,
logoUrl,
homepageUrl,
} = body;
// Validate required fields // Validate required fields
if (!name || !redirectUris) { if (!name || !redirectUris) {
return json({ error: "Name and redirectUris are required" }, { status: 400 }); return json(
{ error: "Name and redirectUris are required" },
{ status: 400 },
);
} }
// Get user's workspace // Get user's workspace
@ -83,7 +98,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
// Generate client credentials // Generate client credentials
const clientId = crypto.randomUUID(); const clientId = crypto.randomUUID();
const clientSecret = crypto.randomBytes(32).toString('hex'); const clientSecret = crypto.randomBytes(32).toString("hex");
// Create OAuth client // Create OAuth client
const client = await prisma.oAuthClient.create({ const client = await prisma.oAuthClient.create({
@ -92,8 +107,12 @@ export const action = async ({ request }: ActionFunctionArgs) => {
clientSecret, clientSecret,
name, name,
description: description || null, description: description || null,
redirectUris: Array.isArray(redirectUris) ? redirectUris.join(',') : redirectUris, redirectUris: Array.isArray(redirectUris)
allowedScopes: Array.isArray(allowedScopes) ? allowedScopes.join(',') : allowedScopes || "read", ? redirectUris.join(",")
: redirectUris,
allowedScopes: Array.isArray(allowedScopes)
? allowedScopes.join(",")
: allowedScopes || "read",
requirePkce: requirePkce || false, requirePkce: requirePkce || false,
logoUrl: logoUrl || null, logoUrl: logoUrl || null,
homepageUrl: homepageUrl || null, homepageUrl: homepageUrl || null,
@ -116,14 +135,14 @@ export const action = async ({ request }: ActionFunctionArgs) => {
}, },
}); });
return json({ return json({
success: true, success: true,
client, client,
message: "OAuth client created successfully. Save the client_secret securely - it won't be shown again." message:
"OAuth client created successfully. Save the client_secret securely - it won't be shown again.",
}); });
} catch (error) { } catch (error) {
console.error("Error creating OAuth client:", error); console.error("Error creating OAuth client:", error);
return json({ error: "Internal server error" }, { status: 500 }); return json({ error: "Internal server error" }, { status: 500 });
} }
}; };

View File

@ -8,7 +8,6 @@ import { createHybridActionApiRoute } from "~/services/routeBuilders/apiBuilder.
import { addToQueue } from "~/lib/ingest.server"; import { addToQueue } from "~/lib/ingest.server";
import { SearchService } from "~/services/search.server"; import { SearchService } from "~/services/search.server";
import { handleTransport } from "~/utils/mcp"; import { handleTransport } from "~/utils/mcp";
import { IngestBodyRequest } from "~/trigger/ingest/ingest";
// Map to store transports by session ID with cleanup tracking // Map to store transports by session ID with cleanup tracking
const transports: { const transports: {
@ -39,16 +38,14 @@ const MCPRequestSchema = z.object({}).passthrough();
// Search parameters schema for MCP tool // Search parameters schema for MCP tool
const SearchParamsSchema = z.object({ const SearchParamsSchema = z.object({
query: z.string(), query: z.string().describe("The search query in third person perspective"),
startTime: z.string().optional(), validAt: z.string().optional().describe("The valid at time in ISO format"),
endTime: z.string().optional(), startTime: z.string().optional().describe("The start time in ISO format"),
spaceId: z.string().optional(), endTime: z.string().optional().describe("The end time in ISO format"),
limit: z.number().optional(), });
maxBfsDepth: z.number().optional(),
includeInvalidated: z.boolean().optional(), const IngestSchema = z.object({
entityTypes: z.array(z.string()).optional(), message: z.string().describe("The data to ingest in text format"),
scoreThreshold: z.number().optional(),
minResults: z.number().optional(),
}); });
const searchService = new SearchService(); const searchService = new SearchService();
@ -60,6 +57,22 @@ const handleMCPRequest = async (
authentication: any, authentication: any,
) => { ) => {
const sessionId = request.headers.get("mcp-session-id") as string | undefined; const sessionId = request.headers.get("mcp-session-id") as string | undefined;
const source = request.headers.get("source") as string | undefined;
if (!source) {
return json(
{
jsonrpc: "2.0",
error: {
code: -32601,
message: "No source found",
},
id: null,
},
{ status: 400 },
);
}
let transport: StreamableHTTPServerTransport; let transport: StreamableHTTPServerTransport;
try { try {
@ -104,14 +117,18 @@ const handleMCPRequest = async (
{ {
title: "Ingest Data", title: "Ingest Data",
description: "Ingest data into the memory system", description: "Ingest data into the memory system",
inputSchema: IngestBodyRequest.shape, inputSchema: IngestSchema.shape,
}, },
async (args) => { async (args) => {
try { try {
const userId = authentication.userId; const userId = authentication.userId;
const response = addToQueue( const response = addToQueue(
args as z.infer<typeof IngestBodyRequest>, {
episodeBody: args.message,
referenceTime: new Date().toISOString(),
source,
},
userId, userId,
); );
return { return {

View File

@ -57,7 +57,7 @@ export default function LogsActivity() {
}, },
]} ]}
/> />
<div className="flex h-[calc(100vh_-_56px)] flex-col space-y-6 p-4 px-5"> <div className="flex h-[calc(100vh_-_56px)] flex-col items-center space-y-6 p-4 px-5">
{isInitialLoad ? ( {isInitialLoad ? (
<> <>
<LoaderCircle className="text-primary h-4 w-4 animate-spin" />{" "} <LoaderCircle className="text-primary h-4 w-4 animate-spin" />{" "}

View File

@ -45,7 +45,7 @@ export default function LogsAll() {
}, },
]} ]}
/> />
<div className="h-[calc(100vh_-_56px)] space-y-6 p-4 px-5"> <div className="flex h-[calc(100vh_-_56px)] flex-col items-center space-y-6 p-4 px-5">
{isInitialLoad ? ( {isInitialLoad ? (
<> <>
<LoaderCircle className="text-primary h-4 w-4 animate-spin" />{" "} <LoaderCircle className="text-primary h-4 w-4 animate-spin" />{" "}

View File

@ -13,7 +13,7 @@ import {
CardTitle, CardTitle,
} from "~/components/ui/card"; } from "~/components/ui/card";
import { Form, useNavigation } from "@remix-run/react"; import { Form, useNavigation } from "@remix-run/react";
import { Inbox, Loader, Mail } from "lucide-react"; import { Inbox, Loader, LoaderCircle, Mail } from "lucide-react";
import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { z } from "zod"; import { z } from "zod";
import { LoginPageLayout } from "~/components/layout/login-page-layout"; import { LoginPageLayout } from "~/components/layout/login-page-layout";
@ -203,7 +203,7 @@ export default function LoginMagicLinkPage() {
data-action="send a magic link" data-action="send a magic link"
> >
{isLoading ? ( {isLoading ? (
<Loader className="mr-2 size-5" color="white" /> <LoaderCircle className="text-primary h-4 w-4 animate-spin" />
) : ( ) : (
<Mail className="text-text-bright mr-2 size-5" /> <Mail className="text-text-bright mr-2 size-5" />
)} )}

View File

@ -1,14 +1,25 @@
import { type ActionFunctionArgs, type LoaderFunctionArgs, redirect } from "@remix-run/node"; import {
import { Form, useLoaderData, useSearchParams } from "@remix-run/react"; type ActionFunctionArgs,
type LoaderFunctionArgs,
redirect,
} from "@remix-run/node";
import { Form, useLoaderData } from "@remix-run/react";
import { getUser } from "~/services/session.server"; import { getUser } from "~/services/session.server";
import { oauth2Service, OAuth2Errors, type OAuth2AuthorizeRequest } from "~/services/oauth2.server"; import {
oauth2Service,
OAuth2Errors,
type OAuth2AuthorizeRequest,
} from "~/services/oauth2.server";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card"; import { Card, CardContent } from "~/components/ui/card";
import { Arrows } from "~/components/icons";
import Logo from "~/components/logo/logo";
import { AlignLeft, LayoutGrid, Pen } from "lucide-react";
export const loader = async ({ request }: LoaderFunctionArgs) => { export const loader = async ({ request }: LoaderFunctionArgs) => {
// Check if user is authenticated // Check if user is authenticated
const user = await getUser(request); const user = await getUser(request);
if (!user) { if (!user) {
// Redirect to login with return URL // Redirect to login with return URL
const url = new URL(request.url); const url = new URL(request.url);
@ -18,33 +29,52 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
} }
const url = new URL(request.url); const url = new URL(request.url);
let scopeParam = url.searchParams.get("scope") || undefined;
// If scope is present, remove spaces after commas (e.g., "read, write" -> "read,write")
if (scopeParam) {
scopeParam = scopeParam
.split(",")
.map((s) => s.trim())
.join(",");
} else {
throw new Error("Scope is not found");
}
const params: OAuth2AuthorizeRequest = { const params: OAuth2AuthorizeRequest = {
client_id: url.searchParams.get("client_id") || "", client_id: url.searchParams.get("client_id") || "",
redirect_uri: url.searchParams.get("redirect_uri") || "", redirect_uri: url.searchParams.get("redirect_uri") || "",
response_type: url.searchParams.get("response_type") || "", response_type: url.searchParams.get("response_type") || "",
scope: url.searchParams.get("scope") || undefined, scope: scopeParam,
state: url.searchParams.get("state") || undefined, state: url.searchParams.get("state") || undefined,
code_challenge: url.searchParams.get("code_challenge") || undefined, code_challenge: url.searchParams.get("code_challenge") || undefined,
code_challenge_method: url.searchParams.get("code_challenge_method") || undefined, code_challenge_method:
url.searchParams.get("code_challenge_method") || undefined,
}; };
// Validate required parameters // Validate required parameters
if (!params.client_id || !params.redirect_uri || !params.response_type) { if (!params.client_id || !params.redirect_uri || !params.response_type) {
return redirect(`${params.redirect_uri}?error=${OAuth2Errors.INVALID_REQUEST}&error_description=Missing required parameters${params.state ? `&state=${params.state}` : ""}`); return redirect(
`${params.redirect_uri}?error=${OAuth2Errors.INVALID_REQUEST}&error_description=Missing required parameters${params.state ? `&state=${params.state}` : ""}`,
);
} }
// Only support authorization code flow // Only support authorization code flow
if (params.response_type !== "code") { if (params.response_type !== "code") {
return redirect(`${params.redirect_uri}?error=${OAuth2Errors.UNSUPPORTED_RESPONSE_TYPE}&error_description=Only authorization code flow is supported${params.state ? `&state=${params.state}` : ""}`); return redirect(
`${params.redirect_uri}?error=${OAuth2Errors.UNSUPPORTED_RESPONSE_TYPE}&error_description=Only authorization code flow is supported${params.state ? `&state=${params.state}` : ""}`,
);
} }
try { try {
// Validate client // Validate client
const client = await oauth2Service.validateClient(params.client_id); const client = await oauth2Service.validateClient(params.client_id);
// Validate redirect URI // Validate redirect URI
if (!oauth2Service.validateRedirectUri(client, params.redirect_uri)) { if (!oauth2Service.validateRedirectUri(client, params.redirect_uri)) {
return redirect(`${params.redirect_uri}?error=${OAuth2Errors.INVALID_REQUEST}&error_description=Invalid redirect URI${params.state ? `&state=${params.state}` : ""}`); return redirect(
`${params.redirect_uri}?error=${OAuth2Errors.INVALID_REQUEST}&error_description=Invalid redirect URI${params.state ? `&state=${params.state}` : ""}`,
);
} }
return { return {
@ -53,41 +83,48 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
params, params,
}; };
} catch (error) { } catch (error) {
return redirect(`${params.redirect_uri}?error=${OAuth2Errors.INVALID_CLIENT}&error_description=Invalid client${params.state ? `&state=${params.state}` : ""}`); return redirect(
`${params.redirect_uri}?error=${OAuth2Errors.INVALID_CLIENT}&error_description=Invalid client${params.state ? `&state=${params.state}` : ""}`,
);
} }
}; };
export const action = async ({ request }: ActionFunctionArgs) => { export const action = async ({ request }: ActionFunctionArgs) => {
const user = await getUser(request); const user = await getUser(request);
if (!user) { if (!user) {
return redirect("/login"); return redirect("/login");
} }
const formData = await request.formData(); const formData = await request.formData();
const action = formData.get("action"); const action = formData.get("action");
const params: OAuth2AuthorizeRequest = { const params: OAuth2AuthorizeRequest = {
client_id: formData.get("client_id") as string, client_id: formData.get("client_id") as string,
redirect_uri: formData.get("redirect_uri") as string, redirect_uri: formData.get("redirect_uri") as string,
response_type: formData.get("response_type") as string, response_type: formData.get("response_type") as string,
scope: formData.get("scope") as string || undefined, scope: (formData.get("scope") as string) || undefined,
state: formData.get("state") as string || undefined, state: (formData.get("state") as string) || undefined,
code_challenge: formData.get("code_challenge") as string || undefined, code_challenge: (formData.get("code_challenge") as string) || undefined,
code_challenge_method: formData.get("code_challenge_method") as string || undefined, code_challenge_method:
(formData.get("code_challenge_method") as string) || undefined,
}; };
if (action === "deny") { if (action === "deny") {
return redirect(`${params.redirect_uri}?error=${OAuth2Errors.ACCESS_DENIED}&error_description=User denied access${params.state ? `&state=${params.state}` : ""}`); return redirect(
`${params.redirect_uri}?error=${OAuth2Errors.ACCESS_DENIED}&error_description=User denied access${params.state ? `&state=${params.state}` : ""}`,
);
} }
if (action === "allow") { if (action === "allow") {
try { try {
// Validate client again // Validate client again
const client = await oauth2Service.validateClient(params.client_id); const client = await oauth2Service.validateClient(params.client_id);
if (!oauth2Service.validateRedirectUri(client, params.redirect_uri)) { if (!oauth2Service.validateRedirectUri(client, params.redirect_uri)) {
return redirect(`${params.redirect_uri}?error=${OAuth2Errors.INVALID_REQUEST}&error_description=Invalid redirect URI${params.state ? `&state=${params.state}` : ""}`); return redirect(
`${params.redirect_uri}?error=${OAuth2Errors.INVALID_REQUEST}&error_description=Invalid redirect URI${params.state ? `&state=${params.state}` : ""}`,
);
} }
// Create authorization code // Create authorization code
@ -109,90 +146,129 @@ export const action = async ({ request }: ActionFunctionArgs) => {
return redirect(redirectUrl.toString()); return redirect(redirectUrl.toString());
} catch (error) { } catch (error) {
return redirect(`${params.redirect_uri}?error=${OAuth2Errors.SERVER_ERROR}&error_description=Failed to create authorization code${params.state ? `&state=${params.state}` : ""}`); return redirect(
`${params.redirect_uri}?error=${OAuth2Errors.SERVER_ERROR}&error_description=Failed to create authorization code${params.state ? `&state=${params.state}` : ""}`,
);
} }
} }
return redirect(`${params.redirect_uri}?error=${OAuth2Errors.INVALID_REQUEST}&error_description=Invalid action${params.state ? `&state=${params.state}` : ""}`); return redirect(
`${params.redirect_uri}?error=${OAuth2Errors.INVALID_REQUEST}&error_description=Invalid action${params.state ? `&state=${params.state}` : ""}`,
);
}; };
export default function OAuthAuthorize() { export default function OAuthAuthorize() {
const { user, client, params } = useLoaderData<typeof loader>(); const { user, client, params } = useLoaderData<typeof loader>();
const [searchParams] = useSearchParams();
const getIcon = (scope: string) => {
if (scope === "read") {
return <AlignLeft size={16} />;
}
return <Pen size={16} />;
};
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gray-50"> <div className="bg-background-2 flex min-h-screen items-center justify-center">
<Card className="w-full max-w-md"> <Card className="bg-background-3 shadow-1 w-full max-w-md rounded-lg p-5">
<CardHeader>
<CardTitle>Authorize Application</CardTitle>
<CardDescription>
<strong>{client.name}</strong> wants to access your Echo account
</CardDescription>
</CardHeader>
<CardContent> <CardContent>
<div className="space-y-4"> <div className="flex items-center justify-center gap-4">
<div className="flex items-center space-x-3"> {client.logoUrl ? (
{client.logoUrl && ( <img
<img src={client.logoUrl}
src={client.logoUrl} alt={client.name}
alt={client.name} className="h-[40px] w-[40px] rounded"
className="w-8 h-8 rounded" />
/> ) : (
)} <LayoutGrid size={40} />
)}
<Arrows size={16} />
<Logo width={40} height={40} />
</div>
<div className="mt-4 space-y-4">
<div className="flex items-center justify-center space-x-3 text-center">
<div> <div>
<p className="font-medium">{client.name}</p> <p className="text-lg font-normal">
{client.description && ( {client.name} is requesting access
<p className="text-sm text-gray-600">{client.description}</p> </p>
)} <p className="text-muted-foreground text-sm">
Authenticating with your {user.name} workspace
</p>
</div> </div>
</div> </div>
<div className="bg-gray-50 p-3 rounded-lg"> <p className="text-muted-foreground mb-2 text-sm">Permissions</p>
<p className="text-sm font-medium mb-2">This application will be able to:</p> <ul className="text-muted-foreground text-sm">
<ul className="text-sm text-gray-600 space-y-1"> {params.scope?.split(",").map((scope, index, arr) => {
{params.scope ? ( const isFirst = index === 0;
params.scope.split(' ').map((scope, index) => ( const isLast = index === arr.length - 1;
<li key={index}> {scope === 'read' ? 'Read your profile information' : scope}</li> return (
)) <li
) : ( key={index}
<li> Read your profile information</li> className={`border-border flex items-center gap-2 border-x border-t p-2 ${isLast ? "border-b" : ""} ${isFirst ? "rounded-tl-md rounded-tr-md" : ""} ${isLast ? "rounded-br-md rounded-bl-md" : ""} `}
)} >
</ul> <div>{getIcon(scope)}</div>
</div> <div>
{scope.charAt(0).toUpperCase() + scope.slice(1)} access to
<div className="bg-blue-50 p-3 rounded-lg"> your workspace
<p className="text-sm text-blue-800"> </div>
<strong>Signed in as:</strong> {user.email} </li>
</p> );
</div> })}
</ul>
<Form method="post" className="space-y-3"> <Form method="post" className="space-y-3">
<input type="hidden" name="client_id" value={params.client_id} /> <input type="hidden" name="client_id" value={params.client_id} />
<input type="hidden" name="redirect_uri" value={params.redirect_uri} /> <input
<input type="hidden" name="response_type" value={params.response_type} /> type="hidden"
{params.scope && <input type="hidden" name="scope" value={params.scope} />} name="redirect_uri"
{params.state && <input type="hidden" name="state" value={params.state} />} value={params.redirect_uri}
{params.code_challenge && <input type="hidden" name="code_challenge" value={params.code_challenge} />} />
{params.code_challenge_method && <input type="hidden" name="code_challenge_method" value={params.code_challenge_method} />} <input
type="hidden"
<div className="flex space-x-3"> name="response_type"
<Button value={params.response_type}
type="submit" />
name="action" {params.scope && (
value="allow" <input type="hidden" name="scope" value={params.scope} />
className="flex-1" )}
> {params.state && (
Allow Access <input type="hidden" name="state" value={params.state} />
</Button> )}
<Button {params.code_challenge && (
type="submit" <input
name="action" type="hidden"
name="code_challenge"
value={params.code_challenge}
/>
)}
{params.code_challenge_method && (
<input
type="hidden"
name="code_challenge_method"
value={params.code_challenge_method}
/>
)}
<div className="flex justify-end space-x-3">
<Button
type="submit"
name="action"
value="deny" value="deny"
variant="outline" size="lg"
className="flex-1" variant="secondary"
> >
Deny Deny
</Button> </Button>
<Button
type="submit"
name="action"
value="allow"
size="lg"
className="shadow-none"
>
Allow Access
</Button>
</div> </div>
</Form> </Form>
</div> </div>
@ -200,4 +276,4 @@ export default function OAuthAuthorize() {
</Card> </Card>
</div> </div>
); );
} }

View File

@ -305,7 +305,7 @@
--radius-full: 9999px; --radius-full: 9999px;
--shadow: 0px 6px 20px 0px rgba(0, 0, 0, 0.15), 0px 0px 2px 0px rgba(0, 0, 0, 0.2); --shadow: 0px 6px 20px 0px rgba(0, 0, 0, 0.15), 0px 0px 2px 0px rgba(0, 0, 0, 0.2);
--shadow-1: 0px 6px 20px 0px rgba(0, 0, 0, 0.15), 0px 0px 2px 0px rgba(0, 0, 0, 0.2); --shadow-1: lch(0 0 0 / 0.022) 0px 3px 6px -2px, lch(0 0 0 / 0.044) 0px 1px 1px;
--font-sans: "Geist Variable", "Helvetica Neue", "Helvetica", "Arial", sans-serif; --font-sans: "Geist Variable", "Helvetica Neue", "Helvetica", "Arial", sans-serif;
--font-mono: "Geist Mono Variable", monaco, Consolas, "Lucida Console", monospace; --font-mono: "Geist Mono Variable", monaco, Consolas, "Lucida Console", monospace;

View File

@ -12,7 +12,6 @@ export interface SearchMemoryParams {
export interface AddMemoryParams { export interface AddMemoryParams {
message: string; message: string;
referenceTime?: string; referenceTime?: string;
source?: string;
spaceId?: string; spaceId?: string;
sessionId?: string; sessionId?: string;
metadata?: any; metadata?: any;
@ -38,7 +37,7 @@ export const addMemory = async (params: AddMemoryParams) => {
...params, ...params,
episodeBody: params.message, episodeBody: params.message,
referenceTime: params.referenceTime || new Date().toISOString(), referenceTime: params.referenceTime || new Date().toISOString(),
source: params.source || "CORE", source: "CORE",
}; };
const response = await axios.post( const response = await axios.post(

View File

@ -35,19 +35,23 @@ npm install -g @redplanethq/core
### Initial Setup ### Initial Setup
1. **Run the initialization command:** 1. **Clone the Core repository:**
```bash
git clone https://github.com/redplanethq/core.git
cd core
```
2. **Run the initialization command:**
```bash ```bash
core init core init
``` ```
2. **The CLI will guide you through the complete setup process:** 3. **The CLI will guide you through the complete setup process:**
#### Step 1: Repository Validation #### Step 1: Prerequisites Check
- The CLI shows a checklist of required tools
- The CLI checks if you're in the Core repository - Confirms you're in the Core repository directory
- If not, it offers to clone the repository for you - Exits with instructions if prerequisites aren't met
- Choose **Yes** to clone automatically, or **No** to clone manually
#### Step 2: Environment Configuration #### Step 2: Environment Configuration
@ -130,9 +134,9 @@ After setup, these services will be available:
If you run commands outside the Core repository: If you run commands outside the Core repository:
- The CLI will offer to clone the repository automatically - The CLI will ask you to confirm you're in the Core repository
- Choose **Yes** to clone in the current directory - If not, it provides instructions to clone the repository
- Or navigate to the Core repository manually - Navigate to the Core repository directory before running commands again
### Docker Issues ### Docker Issues

View File

@ -1,5 +1,4 @@
import { intro, outro, text, confirm, spinner, note, log } from "@clack/prompts"; import { intro, outro, text, confirm, spinner, note, log } from "@clack/prompts";
import { isValidCoreRepo } from "../utils/git.js";
import { fileExists, updateEnvFile } from "../utils/file.js"; import { fileExists, updateEnvFile } from "../utils/file.js";
import { checkPostgresHealth } from "../utils/docker.js"; import { checkPostgresHealth } from "../utils/docker.js";
import { executeDockerCommandInteractive } from "../utils/docker-interactive.js"; import { executeDockerCommandInteractive } from "../utils/docker-interactive.js";
@ -14,42 +13,17 @@ export async function initCommand() {
intro("🚀 Core Development Environment Setup"); intro("🚀 Core Development Environment Setup");
// Step 1: Validate repository // Step 1: Confirm this is the Core repository
if (!isValidCoreRepo()) { note("Please ensure you have:\n• Docker and Docker Compose installed\n• Git installed\n• pnpm package manager installed\n• You are in the Core repository directory", "📋 Prerequisites");
log.warning("This directory is not a Core repository");
note( const isCoreRepo = await confirm({
"The Core repository is required to run the development environment.\nWould you like to clone it in the current directory?", message: "Are you currently in the Core repository directory?",
"🔍 Repository Not Found" });
);
const shouldClone = await confirm({ if (!isCoreRepo) {
message: "Clone the Core repository here?", note("Please clone the Core repository first:\n\ngit clone https://github.com/redplanethq/core.git\ncd core\n\nThen run 'core init' again.", "📥 Clone Repository");
}); outro("❌ Setup cancelled. Please navigate to the Core repository first.");
process.exit(1);
if (!shouldClone) {
outro("❌ Setup cancelled. Please navigate to the Core repository or clone it first.");
process.exit(1);
}
// Clone the repository
try {
await executeDockerCommandInteractive("git clone https://github.com/redplanethq/core.git .", {
cwd: process.cwd(),
message: "Cloning Core repository...",
showOutput: true,
});
log.success("Core repository cloned successfully!");
note(
'Please run "core init" again to initialize the development environment.',
"✅ Repository Ready"
);
outro("🎉 Core repository is now available!");
process.exit(0);
} catch (error: any) {
outro(`❌ Failed to clone repository: ${error.message}`);
process.exit(1);
}
} }
const rootDir = process.cwd(); const rootDir = process.cwd();

View File

@ -1,5 +1,4 @@
import { intro, outro, note, log, confirm } from '@clack/prompts'; import { intro, outro, note, log, confirm } from '@clack/prompts';
import { isValidCoreRepo } from '../utils/git.js';
import { executeDockerCommandInteractive } from '../utils/docker-interactive.js'; import { executeDockerCommandInteractive } from '../utils/docker-interactive.js';
import { printCoreBrainLogo } from '../utils/ascii.js'; import { printCoreBrainLogo } from '../utils/ascii.js';
import path from 'path'; import path from 'path';
@ -10,36 +9,15 @@ export async function startCommand() {
intro('🚀 Starting Core Development Environment'); intro('🚀 Starting Core Development Environment');
// Step 1: Validate repository // Step 1: Confirm this is the Core repository
if (!isValidCoreRepo()) { const isCoreRepo = await confirm({
log.warning('This directory is not a Core repository'); message: 'Are you currently in the Core repository directory?',
note('The Core repository is required to run the development environment.\nWould you like to clone it in the current directory?', '🔍 Repository Not Found'); });
const shouldClone = await confirm({
message: 'Clone the Core repository here?',
});
if (!shouldClone) { if (!isCoreRepo) {
outro('❌ Setup cancelled. Please navigate to the Core repository or clone it first.'); note('Please navigate to the Core repository first:\n\ngit clone https://github.com/redplanethq/core.git\ncd core\n\nThen run "core start" again.', '📥 Core Repository Required');
process.exit(1); outro('❌ Please navigate to the Core repository first.');
} process.exit(1);
// Clone the repository
try {
await executeDockerCommandInteractive('git clone https://github.com/redplanethq/core.git .', {
cwd: process.cwd(),
message: 'Cloning Core repository...',
showOutput: true
});
log.success('Core repository cloned successfully!');
note('You can now run "core start" to start the development environment.', '✅ Repository Ready');
outro('🎉 Core repository is now available!');
process.exit(0);
} catch (error: any) {
outro(`❌ Failed to clone repository: ${error.message}`);
process.exit(1);
}
} }
const rootDir = process.cwd(); const rootDir = process.cwd();

View File

@ -1,5 +1,4 @@
import { intro, outro, log, confirm, note } from '@clack/prompts'; import { intro, outro, log, confirm, note } from '@clack/prompts';
import { isValidCoreRepo } from '../utils/git.js';
import { executeDockerCommandInteractive } from '../utils/docker-interactive.js'; import { executeDockerCommandInteractive } from '../utils/docker-interactive.js';
import { printCoreBrainLogo } from '../utils/ascii.js'; import { printCoreBrainLogo } from '../utils/ascii.js';
import path from 'path'; import path from 'path';
@ -10,36 +9,15 @@ export async function stopCommand() {
intro('🛑 Stopping Core Development Environment'); intro('🛑 Stopping Core Development Environment');
// Step 1: Validate repository // Step 1: Confirm this is the Core repository
if (!isValidCoreRepo()) { const isCoreRepo = await confirm({
log.warning('This directory is not a Core repository'); message: 'Are you currently in the Core repository directory?',
note('The Core repository is required to stop the development environment.\nWould you like to clone it in the current directory?', '🔍 Repository Not Found'); });
const shouldClone = await confirm({
message: 'Clone the Core repository here?',
});
if (!shouldClone) { if (!isCoreRepo) {
outro('❌ Setup cancelled. Please navigate to the Core repository or clone it first.'); note('Please navigate to the Core repository first:\n\ngit clone https://github.com/redplanethq/core.git\ncd core\n\nThen run "core stop" again.', '📥 Core Repository Required');
process.exit(1); outro('❌ Please navigate to the Core repository first.');
} process.exit(1);
// Clone the repository
try {
await executeDockerCommandInteractive('git clone https://github.com/redplanethq/core.git .', {
cwd: process.cwd(),
message: 'Cloning Core repository...',
showOutput: true
});
log.success('Core repository cloned successfully!');
note('You can now run "core stop" to stop the development environment.', '✅ Repository Ready');
outro('🎉 Core repository is now available!');
process.exit(0);
} catch (error: any) {
outro(`❌ Failed to clone repository: ${error.message}`);
process.exit(1);
}
} }
const rootDir = process.cwd(); const rootDir = process.cwd();