diff --git a/apps/webapp/app/components/graph/graph.tsx b/apps/webapp/app/components/graph/graph.tsx index 3cd16a9..f3063e3 100644 --- a/apps/webapp/app/components/graph/graph.tsx +++ b/apps/webapp/app/components/graph/graph.tsx @@ -5,6 +5,7 @@ import { useCallback, useImperativeHandle, forwardRef, + useState, } from "react"; import Sigma from "sigma"; import GraphologyGraph from "graphology"; @@ -186,10 +187,6 @@ export const Graph = forwardRef( } groups[key].relations.push(triplet.relation.value); groups[key].relationData.push(triplet.relation); - groups[key].label = groups[key].relations - .join(", ") - .replace("HAS_", "") - .toLowerCase(); return groups; }, @@ -216,7 +213,7 @@ export const Graph = forwardRef( }); graph.forEachEdge((edge) => { graph.setEdgeAttribute(edge, "highlighted", false); - graph.setEdgeAttribute(edge, "color", theme.link.stroke); + graph.setEdgeAttribute(edge, "color", "#0000001A"); }); selectedNodeRef.current = null; selectedEdgeRef.current = null; @@ -271,6 +268,71 @@ export const Graph = forwardRef( useImperativeHandle(ref, () => graphRefMethods.current); + // Calculate optimal ForceAtlas2 parameters based on graph properties + const calculateOptimalParameters = useCallback((graph: GraphologyGraph) => { + const nodeCount = graph.order; + const edgeCount = graph.size; + + if (nodeCount === 0) + return { scalingRatio: 30, gravity: 5, iterations: 600 }; + + // Calculate graph density (0 to 1) + const maxPossibleEdges = (nodeCount * (nodeCount - 1)) / 2; + const density = maxPossibleEdges > 0 ? edgeCount / maxPossibleEdges : 0; + + // Calculate optimal scaling ratio based on node count + // More nodes = need more space to prevent overcrowding + let scalingRatio: number; + if (nodeCount < 10) { + scalingRatio = 15; // Tight for small graphs + } else if (nodeCount < 50) { + scalingRatio = 20 + (nodeCount - 10) * 0.5; // Gradual increase + } else if (nodeCount < 200) { + scalingRatio = 40 + (nodeCount - 50) * 0.2; // Slower increase + } else { + scalingRatio = Math.min(80, 70 + (nodeCount - 200) * 0.05); // Cap at 80 + } + + // Calculate optimal gravity based on density and node count + let gravity: number; + if (density > 0.3) { + // Dense graphs need less gravity to prevent overcrowding + gravity = 1 + density * 2; + } else if (density > 0.1) { + // Medium density graphs + gravity = 3 + density * 5; + } else { + // Sparse graphs need more gravity to keep components together + gravity = Math.min(8, 5 + (1 - density) * 3); + } + + // Adjust gravity based on node count + if (nodeCount < 20) { + gravity *= 1.5; // Smaller graphs benefit from stronger gravity + } else if (nodeCount > 100) { + gravity *= 0.8; // Larger graphs need gentler gravity + } + + // Calculate iterations based on complexity + const complexity = nodeCount + edgeCount; + let iterations: number; + if (complexity < 50) { + iterations = 400; + } else if (complexity < 200) { + iterations = 600; + } else if (complexity < 500) { + iterations = 800; + } else { + iterations = Math.min(1200, 1000 + complexity * 0.2); + } + + return { + scalingRatio: Math.round(scalingRatio * 10) / 10, + gravity: Math.round(gravity * 10) / 10, + iterations: Math.round(iterations), + }; + }, []); + useEffect(() => { if (isInitializedRef.current || !containerRef.current) return; isInitializedRef.current = true; @@ -303,25 +365,28 @@ export const Graph = forwardRef( // }); // layout.start(); + // Calculate optimal parameters for this graph + const optimalParams = calculateOptimalParameters(graph); + const settings = forceAtlas2.inferSettings(graph); forceAtlas2.assign(graph, { - iterations: 600, + iterations: optimalParams.iterations, settings: { ...settings, barnesHutOptimize: true, - strongGravityMode: false, - gravity: 1, - scalingRatio: 10, - slowDown: 5, + strongGravityMode: true, + gravity: optimalParams.gravity, + scalingRatio: optimalParams.scalingRatio, + slowDown: 3, }, }); noverlap.assign(graph, { - maxIterations: 150, + maxIterations: 200, settings: { - margin: 5, - expansion: 1.1, - gridSize: 20, + margin: 10, + expansion: 1.5, + gridSize: 30, }, }); } diff --git a/apps/webapp/app/components/integrations/mcp-auth-section.tsx b/apps/webapp/app/components/integrations/mcp-auth-section.tsx index 4c7105e..541df20 100644 --- a/apps/webapp/app/components/integrations/mcp-auth-section.tsx +++ b/apps/webapp/app/components/integrations/mcp-auth-section.tsx @@ -1,12 +1,14 @@ import React, { useCallback, useState } from "react"; import { useFetcher } from "@remix-run/react"; import { Button } from "~/components/ui/button"; -import { Check } from "lucide-react"; +import { Check, Copy } from "lucide-react"; +import { Input } from "../ui/input"; interface MCPAuthSectionProps { integration: { id: string; name: string; + slug: string; }; activeAccount?: { id: string; @@ -17,6 +19,49 @@ interface MCPAuthSectionProps { hasMCPAuth: boolean; } +interface MCPUrlBoxProps { + mcpUrl: string; +} + +function MCPUrlBox({ mcpUrl }: MCPUrlBoxProps) { + const [copied, setCopied] = useState(false); + + const handleCopy = useCallback(() => { + navigator.clipboard.writeText(mcpUrl).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }); + }, [mcpUrl]); + + return ( +
+ e.target.select()} + /> + +
+ ); +} + export function MCPAuthSection({ integration, activeAccount, @@ -26,7 +71,10 @@ export function MCPAuthSection({ const mcpFetcher = useFetcher<{ redirectURL: string }>(); const disconnectMcpFetcher = useFetcher(); - const isMCPConnected = activeAccount?.integrationConfiguration?.mcp; + const isMCPConnected = !!activeAccount?.integrationConfiguration?.mcp; + const isConnected = !!activeAccount; + + const mcpUrl = `https://core.heysol.ai/api/v1/mcp/${integration.slug}`; const handleMCPConnect = useCallback(() => { setIsMCPConnecting(true); @@ -43,7 +91,7 @@ export function MCPAuthSection({ encType: "application/json", }, ); - }, [integration.id, mcpFetcher]); + }, [integration.id, mcpFetcher, activeAccount?.id]); const handleMCPDisconnect = useCallback(() => { if (!activeAccount?.id) return; @@ -77,57 +125,78 @@ export function MCPAuthSection({ } }, [disconnectMcpFetcher.state, disconnectMcpFetcher.data]); - if (!hasMCPAuth || !activeAccount) return null; + // Show nothing if not connected at all + if (!isConnected) return null; + // Show MCP box if: + // - hasMCPAuth is true (always show MCP section) + // - OR hasMCPAuth is false but integration is connected (show MCP URL box only) return (

MCP Authentication

- {isMCPConnected ? ( -
-
-

- MCP Connected -

-

- MCP (Model Context Protocol) authentication is active + {hasMCPAuth ? ( + isMCPConnected ? ( +

+
+

+ MCP Connected +

+

+ MCP (Model Context Protocol) authentication is active +

+ +
+ +
+
+
+ ) : ( +
+

+ MCP (Model Context Protocol) Authentication +

+

+ This integration requires MCP (Model Context Protocol) + authentication. Please provide the required MCP credentials in + addition to any other authentication method.

+
-
- ) : activeAccount ? ( + ) + ) : ( + // hasMCPAuth is false, but integration is connected: show just the MCPUrlBox
-

- MCP (Model Context Protocol) Authentication -

-

- This integration requires MCP (Model Context Protocol) - authentication. Please provide the required MCP credentials in - addition to any other authentication method. -

-
- +
+

+ Integration Connected +

+

+ You can use the MCP endpoint for this integration: +

+
- ) : null} + )}
); } diff --git a/apps/webapp/app/components/integrations/section.tsx b/apps/webapp/app/components/integrations/section.tsx index d0c63fa..e92024d 100644 --- a/apps/webapp/app/components/integrations/section.tsx +++ b/apps/webapp/app/components/integrations/section.tsx @@ -14,7 +14,7 @@ export function Section({ children, }: SectionProps) { return ( -
+
{icon && <>{icon}}

{title}

diff --git a/apps/webapp/app/components/logo/core.svg b/apps/webapp/app/components/logo/core.svg index 6242699..0487725 100644 --- a/apps/webapp/app/components/logo/core.svg +++ b/apps/webapp/app/components/logo/core.svg @@ -1,16 +1,16 @@ - - - - - - - - - - - - + + + + + + + + + + + + @@ -19,18 +19,18 @@ - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/apps/webapp/app/components/logo/logo.tsx b/apps/webapp/app/components/logo/logo.tsx index 955489e..387fb70 100644 --- a/apps/webapp/app/components/logo/logo.tsx +++ b/apps/webapp/app/components/logo/logo.tsx @@ -15,7 +15,7 @@ export default function StaticLogo({ width, height }: LogoProps) { @@ -119,7 +119,7 @@ export default function StaticLogo({ width, height }: LogoProps) { ) { const user = useUser(); - console.log(user); return ( ( {...props} disabled={isLoading ?? disabled} > - {isLoading ? : <>} + {isLoading ? : <>} {children} ); diff --git a/apps/webapp/app/components/ui/slider.tsx b/apps/webapp/app/components/ui/slider.tsx new file mode 100644 index 0000000..e254007 --- /dev/null +++ b/apps/webapp/app/components/ui/slider.tsx @@ -0,0 +1,25 @@ +import * as SliderPrimitive from "@radix-ui/react-slider"; +import * as React from "react"; +import { cn } from "~/lib/utils"; + +const Slider = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + + +)); +Slider.displayName = SliderPrimitive.Root.displayName; + +export { Slider }; diff --git a/apps/webapp/app/lib/usePersistentState.ts b/apps/webapp/app/lib/usePersistentState.ts new file mode 100644 index 0000000..0c92115 --- /dev/null +++ b/apps/webapp/app/lib/usePersistentState.ts @@ -0,0 +1,44 @@ +import { useState, useEffect } from "react"; + +/** + * A hook that persists state to localStorage + * @param key - The localStorage key to store the value under + * @param defaultValue - The default value to use if nothing is stored + * @returns A tuple of [value, setValue] similar to useState + */ +export function usePersistentState( + key: string, + defaultValue: T +): [T, (value: T | ((prevValue: T) => T)) => void] { + const [state, setState] = useState(() => { + // Only access localStorage on the client side + if (typeof window === "undefined") { + return defaultValue; + } + + try { + const item = window.localStorage.getItem(key); + return item ? JSON.parse(item) : defaultValue; + } catch (error) { + console.warn(`Error reading localStorage key "${key}":`, error); + return defaultValue; + } + }); + + const setValue = (value: T | ((prevValue: T) => T)) => { + try { + // Allow value to be a function so we have the same API as useState + const valueToStore = value instanceof Function ? value(state) : value; + setState(valueToStore); + + // Save to localStorage on the client side + if (typeof window !== "undefined") { + window.localStorage.setItem(key, JSON.stringify(valueToStore)); + } + } catch (error) { + console.warn(`Error setting localStorage key "${key}":`, error); + } + }; + + return [state, setValue]; +} \ No newline at end of file diff --git a/apps/webapp/app/routes/api.v1.mcp.memory.tsx b/apps/webapp/app/routes/api.v1.mcp.memory.tsx index 30bbc4f..3a26fc7 100644 --- a/apps/webapp/app/routes/api.v1.mcp.memory.tsx +++ b/apps/webapp/app/routes/api.v1.mcp.memory.tsx @@ -35,7 +35,6 @@ setInterval( // MCP request body schema const MCPRequestSchema = z.object({}).passthrough(); - const SourceParams = z.object({ source: z.string().optional(), }); @@ -63,7 +62,9 @@ const handleMCPRequest = async ( ) => { const sessionId = request.headers.get("mcp-session-id") as string | undefined; const source = - request.headers.get("source") || (params.source as string | undefined); + (request.headers.get("source") as string | undefined) ?? + (params.source as string | undefined); + if (!source) { return json( { @@ -257,7 +258,12 @@ const { action, loader } = createHybridActionApiRoute( const method = request.method; if (method === "POST") { - return await handleMCPRequest(request, body, authentication, searchParams); + return await handleMCPRequest( + request, + body, + authentication, + searchParams, + ); } else if (method === "DELETE") { return await handleDelete(request, authentication); } else { diff --git a/apps/webapp/app/routes/home.integration.$slug.tsx b/apps/webapp/app/routes/home.integration.$slug.tsx index fc1e453..1e422e4 100644 --- a/apps/webapp/app/routes/home.integration.$slug.tsx +++ b/apps/webapp/app/routes/home.integration.$slug.tsx @@ -138,7 +138,7 @@ export default function IntegrationDetail() { const hasApiKey = !!specData?.auth?.api_key; const hasOAuth2 = !!specData?.auth?.OAuth2; const hasMCPAuth = !!( - specData?.mcp.type === "url" && specData?.mcp.needsAuth + specData?.mcp.type === "http" && specData?.mcp.needsAuth ); const Component = getIcon(integration.icon as IconType); @@ -163,84 +163,86 @@ export default function IntegrationDetail() { }, ]} /> -
-
- -
- } - > -
- {/* Authentication Methods */} -
-

Authentication Methods

-
- {hasApiKey && ( -
- - API Key authentication - -
- )} - {hasOAuth2 && ( -
- - - OAuth 2.0 authentication - -
- )} - {!hasApiKey && !hasOAuth2 && !hasMCPAuth && ( -
- No authentication method specified -
- )} +
+
+
+
-
- - {/* Connect Section */} - {!activeAccount && (hasApiKey || hasOAuth2) && ( -
-

- Connect to {integration.name} -

- - {/* API Key Authentication */} - - - {/* OAuth Authentication */} - + } + > +
+ {/* Authentication Methods */} +
+

Authentication Methods

+
+ {hasApiKey && ( +
+ + API Key authentication + +
+ )} + {hasOAuth2 && ( +
+ + + OAuth 2.0 authentication + +
+ )} + {!hasApiKey && !hasOAuth2 && !hasMCPAuth && ( +
+ No authentication method specified +
+ )} +
- )} - {/* Connected Account Info */} - + {/* Connect Section */} + {!activeAccount && (hasApiKey || hasOAuth2) && ( +
+

+ Connect to {integration.name} +

- {/* MCP Authentication Section */} - + {/* API Key Authentication */} + - {/* Ingestion Rule Section */} - -
- + {/* OAuth Authentication */} + +
+ )} + + {/* Connected Account Info */} + + + {/* MCP Authentication Section */} + + + {/* Ingestion Rule Section */} + +
+ +
); diff --git a/apps/webapp/app/routes/oauth.authorize.tsx b/apps/webapp/app/routes/oauth.authorize.tsx index 33933a6..8e2944a 100644 --- a/apps/webapp/app/routes/oauth.authorize.tsx +++ b/apps/webapp/app/routes/oauth.authorize.tsx @@ -14,8 +14,17 @@ import { Button } from "~/components/ui/button"; import { Card, CardContent } from "~/components/ui/card"; import { Arrows } from "~/components/icons"; import Logo from "~/components/logo/logo"; -import { AlignLeft, LayoutGrid, Pen, User, Mail, Shield, Database } from "lucide-react"; - +import { + AlignLeft, + LayoutGrid, + Pen, + User, + Mail, + Shield, + Database, + LoaderCircle, +} from "lucide-react"; +import { useState } from "react"; export const loader = async ({ request }: LoaderFunctionArgs) => { // Check if user is authenticated @@ -36,13 +45,16 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { // Handle both space-separated (from URL encoding) and comma-separated scopes if (scopeParam) { // First, try splitting by spaces (common in OAuth2 URLs) - let scopes = scopeParam.split(/\s+/).filter(s => s.length > 0); - + let scopes = scopeParam.split(/\s+/).filter((s) => s.length > 0); + // If no spaces found, try splitting by commas if (scopes.length === 1) { - scopes = scopeParam.split(",").map(s => s.trim()).filter(s => s.length > 0); + scopes = scopeParam + .split(",") + .map((s) => s.trim()) + .filter((s) => s.length > 0); } - + scopeParam = scopes.join(","); } else { throw new Error("Scope is not found"); @@ -85,7 +97,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { } // Validate scopes - if (!oauth2Service.validateScopes(client, params.scope || '')) { + if (!oauth2Service.validateScopes(client, params.scope || "")) { return redirect( `${params.redirect_uri}?error=${OAuth2Errors.INVALID_SCOPE}&error_description=Invalid scope${params.state ? `&state=${params.state}` : ""}`, ); @@ -151,7 +163,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { codeChallenge: params.code_challenge, codeChallengeMethod: params.code_challenge_method, workspaceId: workspace.id, - }); + }); // Redirect back to client with authorization code const redirectUrl = new URL(params.redirect_uri); redirectUrl.searchParams.set("code", authCode); @@ -173,8 +185,8 @@ export const action = async ({ request }: ActionFunctionArgs) => { }; export default function OAuthAuthorize() { - const { user, client, params } = useLoaderData(); - + const { user, client, params } = useLoaderData(); + const [isRedirecting, setIsRedirecting] = useState(false); const getScopeIcon = (scope: string) => { switch (scope) { @@ -255,68 +267,93 @@ export default function OAuthAuthorize() { className={`flex items-center gap-2 border-x border-t border-gray-300 p-2 ${isLast ? "border-b" : ""} ${isFirst ? "rounded-tl-md rounded-tr-md" : ""} ${isLast ? "rounded-br-md rounded-bl-md" : ""} `} >
{getScopeIcon(trimmedScope)}
-
- {getScopeDescription(trimmedScope)} -
+
{getScopeDescription(trimmedScope)}
); })} -
- - - - {params.scope && ( - - )} - {params.state && ( - - )} - {params.code_challenge && ( - - )} - {params.code_challenge_method && ( - - )} - -
- - + {isRedirecting ? ( +
+ + + Redirecting to the page... (Close this page if it doesn't + redirect in 5 seconds) +
- + ) : ( +
{ + // Only show loading if allow is clicked + const form = e.target as HTMLFormElement; + const allowBtn = form.querySelector( + 'button[name="action"][value="allow"]', + ); + if ((e.nativeEvent as SubmitEvent).submitter === allowBtn) { + setIsRedirecting(true); + } + }} + > + + + + {params.scope && ( + + )} + {params.state && ( + + )} + {params.code_challenge && ( + + )} + {params.code_challenge_method && ( + + )} + +
+ + +
+
+ )}
diff --git a/apps/webapp/app/services/redirectTo.server.ts b/apps/webapp/app/services/redirectTo.server.ts index 53828e0..94eb169 100644 --- a/apps/webapp/app/services/redirectTo.server.ts +++ b/apps/webapp/app/services/redirectTo.server.ts @@ -6,7 +6,7 @@ const ONE_DAY = 60 * 60 * 24; export const { commitSession, getSession } = createCookieSessionStorage({ cookie: { - name: "__redirectTo", + name: "__redirectTo__core", path: "/", httpOnly: true, sameSite: "lax", diff --git a/apps/webapp/app/services/sessionStorage.server.ts b/apps/webapp/app/services/sessionStorage.server.ts index 4d26749..ff92c4e 100644 --- a/apps/webapp/app/services/sessionStorage.server.ts +++ b/apps/webapp/app/services/sessionStorage.server.ts @@ -9,7 +9,7 @@ export const sessionStorage = createCookieSessionStorage<{ [SESSION_KEY]: AuthUser; }>({ cookie: { - name: "__session", // use any name you want here + name: "__session__core", // use any name you want here sameSite: "lax", // this helps with CSRF path: "/", // remember to add this so the cookie will work in all routes httpOnly: true, // for security reasons, make this cookie http only diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 2a74d5e..99b1220 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -6,8 +6,8 @@ "scripts": { "build": "remix vite:build", "dev": "node ./server.mjs", - "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", - "lint:fix": "eslint --fix --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", + "lint": "eslint --fix --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", + "lint:fix": "eslint 'app/**/*.{ts,tsx,js,jsx}' --rule 'turbo/no-undeclared-env-vars:error' -f table", "start": "remix-serve ./build/server/index.js", "typecheck": "tsc", "trigger:dev": "pnpm dlx trigger.dev@4.0.0-v4-beta.22 dev", @@ -38,6 +38,7 @@ "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-slider": "^1.3.5", "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.1.7", diff --git a/apps/webapp/public/logo-dark.svg b/apps/webapp/public/logo-dark.svg index 6242699..0487725 100644 --- a/apps/webapp/public/logo-dark.svg +++ b/apps/webapp/public/logo-dark.svg @@ -1,16 +1,16 @@ - - - - - - - - - - - - + + + + + + + + + + + + @@ -19,18 +19,18 @@ - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/docker-compose.yaml b/docker-compose.yaml index 7c95ce2..078befb 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -32,9 +32,12 @@ services: ports: - "3033:3000" depends_on: - - postgres - - redis - - neo4j + postgres: + condition: service_healthy + redis: + condition: service_started + neo4j: + condition: service_healthy networks: - core @@ -51,6 +54,12 @@ services: - postgres_data:/var/lib/postgresql/data networks: - core + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $POSTGRES_USER"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s redis: container_name: core-redis @@ -72,6 +81,12 @@ services: - neo4j_data:/data networks: - core + healthcheck: + test: ["CMD-SHELL", "cypher-shell -u $NEO4J_USERNAME -p $NEO4J_PASSWORD 'RETURN 1'"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 20s networks: core: diff --git a/packages/core-cli/package.json b/packages/core-cli/package.json index 31c3cd1..d76c865 100644 --- a/packages/core-cli/package.json +++ b/packages/core-cli/package.json @@ -1,6 +1,6 @@ { "name": "@redplanethq/core", - "version": "0.1.6", + "version": "0.1.8", "description": "A Command-Line Interface for Core", "type": "module", "license": "MIT", diff --git a/packages/core-cli/src/cli/index.ts b/packages/core-cli/src/cli/index.ts index 5b145cf..8157f4b 100644 --- a/packages/core-cli/src/cli/index.ts +++ b/packages/core-cli/src/cli/index.ts @@ -2,24 +2,19 @@ import { Command } from "commander"; import { initCommand } from "../commands/init.js"; import { startCommand } from "../commands/start.js"; import { stopCommand } from "../commands/stop.js"; +import { VERSION } from "./version.js"; const program = new Command(); -program.name("core").description("Core CLI - A Command-Line Interface for Core").version("0.1.0"); +program.name("core").description("Core CLI - A Command-Line Interface for Core").version(VERSION); program .command("init") .description("Initialize Core development environment (run once)") .action(initCommand); -program - .command("start") - .description("Start Core development environment") - .action(startCommand); +program.command("start").description("Start Core development environment").action(startCommand); -program - .command("stop") - .description("Stop Core development environment") - .action(stopCommand); +program.command("stop").description("Stop Core development environment").action(stopCommand); program.parse(process.argv); diff --git a/packages/core-cli/src/cli/version.ts b/packages/core-cli/src/cli/version.ts new file mode 100644 index 0000000..a0fb2e7 --- /dev/null +++ b/packages/core-cli/src/cli/version.ts @@ -0,0 +1 @@ +export const VERSION = "0.1.7"; diff --git a/packages/core-cli/src/commands/init.ts b/packages/core-cli/src/commands/init.ts index 5820195..a8d543f 100644 --- a/packages/core-cli/src/commands/init.ts +++ b/packages/core-cli/src/commands/init.ts @@ -10,6 +10,8 @@ import { deployTriggerTasks } from "../utils/trigger-deploy.js"; import path from "path"; import * as fs from "fs"; import { createTriggerConfigJson, initTriggerDatabase } from "../utils/database-init.js"; +import { parse } from "dotenv"; +import { expand } from "dotenv-expand"; export async function initCommand() { // Display the CORE brain logo @@ -179,10 +181,17 @@ export async function initCommand() { // Step 12: Restart root docker-compose with new configuration try { + const file = fs.readFileSync(envPath); + + const parsed = parse(file); + const envVarsExpand = expand({ parsed, processEnv: {} }).parsed || {}; + + console.log(envVarsExpand); await executeCommandInteractive("docker compose up -d", { cwd: rootDir, message: "Starting Core services with new Trigger.dev configuration...", showOutput: true, + env: envVarsExpand, }); } catch (error: any) { outro("❌ Setup failed: " + error.message); diff --git a/packages/core-cli/src/commands/start.ts b/packages/core-cli/src/commands/start.ts index 00cbbef..eba14c3 100644 --- a/packages/core-cli/src/commands/start.ts +++ b/packages/core-cli/src/commands/start.ts @@ -1,7 +1,8 @@ -import { intro, outro, note, log, confirm } from "@clack/prompts"; +import { intro, outro, note, log } from "@clack/prompts"; import { executeCommandInteractive } from "../utils/docker-interactive.js"; import { printCoreBrainLogo } from "../utils/ascii.js"; import path from "path"; +import * as fs from "fs"; export async function startCommand() { // Display the CORE brain logo @@ -10,9 +11,19 @@ export async function startCommand() { intro("🚀 Starting Core Development Environment"); // Step 1: Confirm this is the Core repository - const isCoreRepo = await confirm({ - message: "Are you currently in the Core repository directory?", - }); + // Check if package.json name has "core" in it, else exit + const pkgPath = path.join(process.cwd(), "package.json"); + let isCoreRepo = false; + try { + if (fs.existsSync(pkgPath)) { + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); + if (typeof pkg.name === "string" && pkg.name.includes("core")) { + isCoreRepo = true; + } + } + } catch (err) { + // ignore, will prompt below + } if (!isCoreRepo) { note( diff --git a/packages/core-cli/src/commands/stop.ts b/packages/core-cli/src/commands/stop.ts index 111222b..e384cb3 100644 --- a/packages/core-cli/src/commands/stop.ts +++ b/packages/core-cli/src/commands/stop.ts @@ -1,7 +1,8 @@ -import { intro, outro, log, confirm, note } from "@clack/prompts"; +import { intro, outro, log, note } from "@clack/prompts"; import { executeCommandInteractive } from "../utils/docker-interactive.js"; import { printCoreBrainLogo } from "../utils/ascii.js"; import path from "path"; +import * as fs from "fs"; export async function stopCommand() { // Display the CORE brain logo @@ -10,10 +11,19 @@ export async function stopCommand() { intro("🛑 Stopping Core Development Environment"); // Step 1: Confirm this is the Core repository - const isCoreRepo = await confirm({ - message: "Are you currently in the Core repository directory?", - }); - + // Check if package.json name has "core" in it, else exit + const pkgPath = path.join(process.cwd(), "package.json"); + let isCoreRepo = false; + try { + if (fs.existsSync(pkgPath)) { + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); + if (typeof pkg.name === "string" && pkg.name.includes("core")) { + isCoreRepo = true; + } + } + } catch (err) { + // ignore, will prompt below + } if (!isCoreRepo) { 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.', diff --git a/packages/core-cli/src/utils/ascii.ts b/packages/core-cli/src/utils/ascii.ts index 6b5dbfb..5df2765 100644 --- a/packages/core-cli/src/utils/ascii.ts +++ b/packages/core-cli/src/utils/ascii.ts @@ -1,4 +1,5 @@ import chalk from "chalk"; +import { VERSION } from "../cli/version.js"; export function printCoreBrainLogo(): void { const brain = ` @@ -20,5 +21,9 @@ export function printCoreBrainLogo(): void { `; console.log(chalk.cyan(brain)); - console.log(chalk.bold.white(" 🧠 CORE - Contextual Observation & Recall Engine \n")); + console.log( + chalk.bold.white( + ` 🧠 CORE - Contextual Observation & Recall Engine ${VERSION ? chalk.gray(`(${VERSION})`) : ""}\n` + ) + ); } diff --git a/packages/core-cli/src/utils/database-init.ts b/packages/core-cli/src/utils/database-init.ts index 3e7ddc0..3a5c460 100644 --- a/packages/core-cli/src/utils/database-init.ts +++ b/packages/core-cli/src/utils/database-init.ts @@ -2,14 +2,14 @@ import Knex, { Knex as KnexT } from "knex"; import { v4 as uuidv4 } from "uuid"; import nodeCrypto from "node:crypto"; -import dotenv from "dotenv"; -import dotenvExpand from "dotenv-expand"; +import { parse } from "dotenv"; +import { expand } from "dotenv-expand"; import path from "node:path"; import { log } from "@clack/prompts"; import { customAlphabet } from "nanoid"; import $xdgAppPaths from "xdg-app-paths"; -import { mkdirSync, writeFileSync } from "node:fs"; +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; export const xdgAppPaths = $xdgAppPaths as unknown as typeof $xdgAppPaths.default; @@ -241,8 +241,10 @@ export async function initTriggerDatabase(triggerDir: string) { const envPath = path.join(triggerDir, ".env"); log.step(`Loading environment variables from ${envPath}...`); - const envVarsExpand = - dotenvExpand.expand(dotenv.config({ path: envPath, processEnv: {} })).parsed || {}; + const file = readFileSync(envPath); + + const parsed = parse(file); + const envVarsExpand = expand({ parsed, processEnv: {} }).parsed || {}; // Set the encryption key from the .env file ENCRYPTION_KEY = envVarsExpand.ENCRYPTION_KEY as string; diff --git a/packages/core-cli/src/utils/docker-interactive.ts b/packages/core-cli/src/utils/docker-interactive.ts index 1cd59d4..46eb5f3 100644 --- a/packages/core-cli/src/utils/docker-interactive.ts +++ b/packages/core-cli/src/utils/docker-interactive.ts @@ -10,6 +10,8 @@ export interface CommandOptions { export function executeCommandInteractive(command: string, options: CommandOptions): Promise { return new Promise((resolve, reject) => { + console.log(process.env); + const s = spinner(); s.start(options.message); @@ -27,7 +29,7 @@ export function executeCommandInteractive(command: string, options: CommandOptio cwd: options.cwd, stdio: options.showOutput ? ["ignore", "pipe", "pipe"] : "ignore", detached: false, - env: options.env ? { ...process.env, ...options.env } : {}, + env: options.env ? { ...process.env, ...options.env } : { ...process.env }, }); let output = ""; diff --git a/packages/core-cli/src/utils/env-docker.ts b/packages/core-cli/src/utils/env-docker.ts index 9fe37e9..88db0c0 100644 --- a/packages/core-cli/src/utils/env-docker.ts +++ b/packages/core-cli/src/utils/env-docker.ts @@ -1,7 +1,8 @@ import path from "path"; -import dotenv from "dotenv"; -import dotenvExpand from "dotenv-expand"; +import { parse } from "dotenv"; +import { expand } from "dotenv-expand"; +import * as fs from "fs"; /** * Reads environment variables from .env file and replaces localhost URLs with host.docker.internal @@ -12,8 +13,10 @@ export async function getDockerCompatibleEnvVars(rootDir: string): Promise { return envVarsExpand[key] || ""; diff --git a/packages/core-cli/src/utils/trigger-deploy.ts b/packages/core-cli/src/utils/trigger-deploy.ts index e679f41..50a3c7a 100644 --- a/packages/core-cli/src/utils/trigger-deploy.ts +++ b/packages/core-cli/src/utils/trigger-deploy.ts @@ -2,7 +2,6 @@ import { note, log } from "@clack/prompts"; import { executeCommandInteractive } from "./docker-interactive.js"; import { getDockerCompatibleEnvVars } from "./env-docker.js"; import path from "path"; -import { createTriggerConfigJson } from "./database-init.js"; export async function deployTriggerTasks(rootDir: string): Promise { const webappDir = path.join(rootDir, "apps", "webapp"); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 49a478d..d0a062b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -114,6 +114,9 @@ importers: '@radix-ui/react-separator': specifier: ^1.1.7 version: 1.1.7(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slider': + specifier: ^1.3.5 + version: 1.3.5(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': specifier: ^1.2.3 version: 1.2.3(@types/react@18.2.69)(react@18.3.1) @@ -3330,6 +3333,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-slider@1.3.5': + resolution: {integrity: sha512-rkfe2pU2NBAYfGaxa3Mqosi7VZEWX5CxKaanRv0vZd4Zhl9fvQrg0VM93dv3xGLGfrHuoTRF3JXH8nb9g+B3fw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-slot@1.0.0': resolution: {integrity: sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==} peerDependencies: @@ -14014,6 +14030,25 @@ snapshots: '@types/react': 18.2.69 '@types/react-dom': 18.3.7(@types/react@18.2.69) + '@radix-ui/react-slider@1.3.5(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.2.69)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.2.69 + '@types/react-dom': 18.3.7(@types/react@18.2.69) + '@radix-ui/react-slot@1.0.0(react@18.3.1)': dependencies: '@babel/runtime': 7.27.6 diff --git a/trigger/.env.example b/trigger/.env.example index 78902ca..8492b68 100644 --- a/trigger/.env.example +++ b/trigger/.env.example @@ -33,7 +33,6 @@ TRIGGER_DB=trigger DB_HOST=host.docker.internal DB_PORT=5432 -DB_SCHEMA=sigma # POSTGRES_DB=postgres