Fix: core cli

This commit is contained in:
Harshith Mullapudi 2025-07-23 12:17:14 +05:30
parent a60dc20bf2
commit 2e08470a03
18 changed files with 5002 additions and 349 deletions

View File

@ -135,7 +135,11 @@ function App() {
// `themeAction` is the action name that's used to change the theme in the session storage.
export default function AppWithProviders() {
return (
<ThemeProvider specifiedTheme={Theme.LIGHT} themeAction="/action/set-theme">
<ThemeProvider
specifiedTheme={Theme.LIGHT}
disableTransitionOnThemeChange={true}
themeAction="/action/set-theme"
>
<App />
</ThemeProvider>
);

View File

@ -3,7 +3,7 @@ import { env } from "~/env.server";
export const impersonationSessionStorage = createCookieSessionStorage({
cookie: {
name: "__impersonate", // use any name you want here
name: "__impersonate_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

View File

@ -9,7 +9,7 @@ export const sessionStorage = createCookieSessionStorage<{
[SESSION_KEY]: AuthUser;
}>({
cookie: {
name: "__session__core", // use any name you want here
name: "__session", // 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

View File

@ -7,12 +7,14 @@ import { type HistoryStep } from "../utils/types";
import {
createConversationHistoryForAgent,
deletePersonalAccessToken,
getCreditsForUser,
getPreviousExecutionHistory,
init,
type RunChatPayload,
updateConversationHistoryMessage,
updateConversationStatus,
updateExecutionStep,
updateUserCredits,
} from "../utils/utils";
const chatQueue = queue({
@ -30,6 +32,8 @@ export const chat = task({
queue: chatQueue,
init,
run: async (payload: RunChatPayload, { init }) => {
const usageCredits = await getCreditsForUser(init?.userId as string);
await updateConversationStatus("running", payload.conversationId);
try {
@ -119,13 +123,7 @@ export const chat = task({
payload.conversationId,
);
// await addToMemory(
// init.conversation.id,
// message,
// agentUserMessage,
// init.preferences,
// init.userName,
// );
usageCredits && (await updateUserCredits(usageCredits, creditForChat));
if (init?.tokenId) {
await deletePersonalAccessToken(init.tokenId);

View File

@ -6,6 +6,7 @@ import {
type Prisma,
PrismaClient,
UserType,
type UserUsage,
type Workspace,
} from "@prisma/client";
@ -56,7 +57,11 @@ function encryptToken(value: string) {
}
const nonce = nodeCrypto.randomBytes(12);
const cipher = nodeCrypto.createCipheriv("aes-256-gcm", encryptionKey, nonce);
const cipher = nodeCrypto.createCipheriv(
"aes-256-gcm",
encryptionKey,
nonce as any,
);
let encrypted = cipher.update(value, "utf8", "hex");
encrypted += cipher.final("hex");
@ -557,3 +562,28 @@ export async function webSearch(args: WebSearchArgs): Promise<WebSearchResult> {
);
}
}
export const getCreditsForUser = async (
userId: string,
): Promise<UserUsage | null> => {
return await prisma.userUsage.findUnique({
where: {
userId,
},
});
};
export const updateUserCredits = async (
userUsage: UserUsage,
usedCredits: number,
) => {
return await prisma.userUsage.update({
where: {
id: userUsage.id,
},
data: {
availableCredits: userUsage.availableCredits - usedCredits,
usedCredits: userUsage.usedCredits + usedCredits,
},
});
};

View File

@ -115,7 +115,7 @@
"react-virtualized": "^9.22.6",
"remix-auth": "^4.2.0",
"remix-auth-oauth2": "^3.4.1",
"remix-themes": "^1.3.1",
"remix-themes": "^2.0.4",
"remix-typedjson": "0.3.1",
"remix-utils": "^7.7.0",
"sdk": "link:@modelcontextprotocol/sdk",

View File

@ -46,6 +46,110 @@ model AuthorizationCode {
updatedAt DateTime @updatedAt
}
model OAuthAuthorizationCode {
id String @id @default(cuid())
code String @unique
// OAuth2 specific fields
clientId String
userId String
redirectUri String
scope String?
state String?
codeChallenge String?
codeChallengeMethod String?
expiresAt DateTime
used Boolean @default(false)
// Relations
client OAuthClient @relation(fields: [clientId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model OAuthClient {
id String @id @default(cuid())
clientId String @unique
clientSecret String
name String
description String?
// Redirect URIs (comma-separated for simplicity)
redirectUris String
// Allowed scopes (comma-separated)
allowedScopes String @default("read")
// Grant types allowed
grantTypes String @default("authorization_code")
// PKCE support
requirePkce Boolean @default(false)
// Client metadata
logoUrl String?
homepageUrl String?
// GitHub-style features
isActive Boolean @default(true)
// Workspace relationship (like GitHub orgs)
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
workspaceId String
// Created by user (for audit trail)
createdBy User @relation(fields: [createdById], references: [id])
createdById String
// Relations
oauthAuthorizationCodes OAuthAuthorizationCode[]
accessTokens OAuthAccessToken[]
refreshTokens OAuthRefreshToken[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model OAuthAccessToken {
id String @id @default(cuid())
token String @unique
clientId String
userId String
scope String?
expiresAt DateTime
revoked Boolean @default(false)
// Relations
client OAuthClient @relation(fields: [clientId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model OAuthRefreshToken {
id String @id @default(cuid())
token String @unique
clientId String
userId String
scope String?
expiresAt DateTime
revoked Boolean @default(false)
// Relations
client OAuthClient @relation(fields: [clientId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Conversation {
id String @id @default(uuid())
createdAt DateTime @default(now())
@ -319,6 +423,26 @@ model User {
Conversation Conversation[]
ConversationHistory ConversationHistory[]
IngestionRule IngestionRule[]
// OAuth2 relations
oauthAuthorizationCodes OAuthAuthorizationCode[]
oauthAccessTokens OAuthAccessToken[]
oauthRefreshTokens OAuthRefreshToken[]
oauthClientsCreated OAuthClient[]
UserUsage UserUsage?
}
model UserUsage {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deleted DateTime?
availableCredits Int @default(0)
usedCredits Int @default(0)
user User @relation(fields: [userId], references: [id])
userId String @unique
}
model WebhookConfiguration {
@ -375,6 +499,7 @@ model Workspace {
WebhookConfiguration WebhookConfiguration[]
Conversation Conversation[]
IngestionRule IngestionRule[]
OAuthClient OAuthClient[]
}
enum AuthenticationMethod {

View File

@ -8,7 +8,7 @@
],
"scripts": {
"build": "dotenv -- turbo run build",
"dev": "dotenv -- turbo run dev",
"dev": "dotenv -- turbo run dev --filter=!core-extension --filter=!@redplanethq/core",
"lint": "dotenv -- turbo run lint",
"format": "dotenv -- prettier --write \"**/*.{ts,tsx,md}\"",
"check-types": "dotenv -- turbo run check-types",

View File

@ -1,6 +1,6 @@
{
"name": "@redplanethq/core",
"version": "0.1.4",
"version": "0.1.6",
"description": "A Command-Line Interface for Core",
"type": "module",
"license": "MIT",
@ -105,6 +105,7 @@
"minimatch": "^10.0.1",
"mlly": "^1.7.1",
"nypm": "^0.5.4",
"nanoid": "3.3.8",
"object-hash": "^3.0.0",
"open": "^10.0.3",
"knex": "3.1.0",

View File

@ -9,7 +9,7 @@ import { handleDockerLogin } from "../utils/docker-login.js";
import { deployTriggerTasks } from "../utils/trigger-deploy.js";
import path from "path";
import * as fs from "fs";
import { initTriggerDatabase } from "../utils/database-init.js";
import { createTriggerConfigJson, initTriggerDatabase } from "../utils/database-init.js";
export async function initCommand() {
// Display the CORE brain logo
@ -66,6 +66,7 @@ export async function initCommand() {
}
} catch (error: any) {
s1.stop(error.message);
outro("❌ Setup failed: " + error.message);
process.exit(1);
}
@ -77,7 +78,8 @@ export async function initCommand() {
showOutput: true,
});
} catch (error: any) {
throw error;
outro("❌ Setup failed: " + error.message);
process.exit(1);
}
// Step 4: Check if postgres is running
@ -98,7 +100,7 @@ export async function initCommand() {
if (retries >= maxRetries) {
s3.stop("L PostgreSQL not accessible on localhost:5432");
outro("Please check your Docker setup and try again");
outro("Please check your Docker setup and try again");
process.exit(1);
}
@ -118,6 +120,7 @@ export async function initCommand() {
}
} catch (error: any) {
s4.stop(error.message);
outro("❌ Setup failed: " + error.message);
process.exit(1);
}
@ -129,7 +132,8 @@ export async function initCommand() {
showOutput: true,
});
} catch (error: any) {
throw error;
outro("❌ Setup failed: " + error.message);
process.exit(1);
}
// Step 7: Check if Trigger.dev configuration already exists
@ -142,10 +146,12 @@ export async function initCommand() {
);
} else {
// Step 8: Show login instructions
outro("🎉 Docker containers are now running!");
const { prodSecretKey, projectRefId } = await initTriggerDatabase(triggerDir);
note("🎉 Docker containers are now running!");
const { prodSecretKey, projectRefId, personalToken } = await initTriggerDatabase(triggerDir);
await createTriggerConfigJson(personalToken as string);
console.log(prodSecretKey, projectRefId);
const openaiApiKey = await text({
message: "Enter your OpenAI API Key:",
validate: (value) => {
@ -167,24 +173,20 @@ export async function initCommand() {
s6.stop("✅ Updated .env with Trigger.dev configuration");
} catch (error: any) {
s6.stop("❌ Failed to update .env file");
throw error;
outro("❌ Setup failed: " + error.message);
process.exit(1);
}
// Step 12: Restart root docker-compose with new configuration
try {
await executeCommandInteractive("docker compose down", {
cwd: rootDir,
message: "Stopping Core services...",
showOutput: true,
});
await executeCommandInteractive("docker compose up -d", {
cwd: rootDir,
message: "Starting Core services with new Trigger.dev configuration...",
showOutput: true,
});
} catch (error: any) {
throw error;
outro("❌ Setup failed: " + error.message);
process.exit(1);
}
}
@ -196,7 +198,6 @@ export async function initCommand() {
await deployTriggerTasks(rootDir);
// Step 15: Final instructions
outro("🎉 Setup Complete!");
note(
[
"Your services are now running:",
@ -212,6 +213,8 @@ export async function initCommand() {
].join("\n"),
"🚀 Services Running"
);
outro("🎉 Setup Complete!");
process.exit(0);
} catch (error: any) {
outro(`❌ Setup failed: ${error.message}`);
process.exit(1);

View File

@ -6,21 +6,19 @@ import dotenv from "dotenv";
import dotenvExpand from "dotenv-expand";
import path from "node:path";
import { log } from "@clack/prompts";
import { customAlphabet } from "nanoid";
// Generate a new token similar to the original: "tr_pat_" + 40 lowercase alphanumeric chars
function generatePersonalToken(count: number) {
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
let token = "tr_pat_";
for (let i = 0; i < count; i++) {
token += chars.charAt(Math.floor(Math.random() * chars.length));
}
return token;
}
import $xdgAppPaths from "xdg-app-paths";
import { mkdirSync, writeFileSync } from "node:fs";
export const xdgAppPaths = $xdgAppPaths as unknown as typeof $xdgAppPaths.default;
const tokenGenerator = customAlphabet("123456789abcdefghijkmnopqrstuvwxyz", 40);
// Generate tokens internally
const TRIGGER_TOKEN = nodeCrypto.randomBytes(32).toString("hex");
let ENCRYPTION_KEY: string;
const COMMON_ID = "9ea0412ea8ef441ca03c7952d011ab56";
const key = generatePersonalToken(20);
const key = tokenGenerator(20);
export async function createOrg(knex: KnexT) {
try {
@ -90,17 +88,26 @@ export async function createPersonalToken(knex: KnexT) {
log.step("Creating CLI personal access token...");
// Generate a new token similar to the original: "tr_pat_" + 40 lowercase alphanumeric chars
const personalToken = generatePersonalToken(40);
const personalToken = `tr_pat_${tokenGenerator(40)}`;
await knex("PersonalAccessToken").insert({
id,
name: "cli",
userId: COMMON_ID,
updatedAt: new Date(),
obfuscatedToken: personalToken,
obfuscatedToken: obfuscateToken(personalToken),
hashedToken: hashToken(personalToken),
encryptedToken: {},
encryptedToken: encryptToken(personalToken),
});
log.success("CLI personal access token created.");
return personalToken;
}
function obfuscateToken(token: string) {
const withoutPrefix = token.replace("tr_pat_", "");
const obfuscated = `${withoutPrefix.slice(0, 4)}${"•".repeat(18)}${withoutPrefix.slice(-4)}`;
return `tr_pat_${obfuscated}`;
}
export async function createProject(knex: KnexT) {
@ -179,9 +186,9 @@ export async function createProject(knex: KnexT) {
}
}
export function encryptToken(value: string) {
function encryptToken(value: string) {
const nonce = nodeCrypto.randomBytes(12);
const cipher = nodeCrypto.createCipheriv("aes-256-gcm", TRIGGER_TOKEN, nonce);
const cipher = nodeCrypto.createCipheriv("aes-256-gcm", ENCRYPTION_KEY, nonce);
let encrypted = cipher.update(value, "utf8", "hex");
encrypted += cipher.final("hex");
@ -203,11 +210,46 @@ export function hashToken(token: string): string {
// Main initialization function
export async function initTriggerDatabase(triggerDir: string) {
log.step("Waiting for Trigger.dev to be ready on http://localhost:8030/login...");
await new Promise((resolve) => setTimeout(resolve, 5000));
// Check if Trigger.dev is up and /login returns 200 before proceeding
const MAX_RETRIES = 30;
const RETRY_DELAY_MS = 2000;
let loginOk = false;
for (let i = 0; i < MAX_RETRIES; i++) {
try {
const res = await fetch("http://localhost:8030/login");
if (res.status === 200) {
loginOk = true;
log.step("Trigger.dev is up and /login returned 200.");
break;
}
} catch (e) {
// ignore, will retry
}
if (i < MAX_RETRIES - 1) {
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
}
}
if (!loginOk) {
log.error("Trigger.dev did not respond with 200 on /login after waiting.");
throw new Error("Trigger.dev is not ready at http://localhost:8030/login");
}
const envPath = path.join(triggerDir, ".env");
log.step(`Loading environment variables from ${envPath}...`);
const envVarsExpand =
dotenvExpand.expand(dotenv.config({ path: envPath, processEnv: {} })).parsed || {};
// Set the encryption key from the .env file
ENCRYPTION_KEY = envVarsExpand.ENCRYPTION_KEY as string;
if (!ENCRYPTION_KEY) {
throw new Error("ENCRYPTION_KEY not found in trigger/.env file");
}
const knex = Knex({
client: "pg", // Use PostgreSQL as the database client
connection: envVarsExpand.DIRECT_URL?.replace("host.docker.internal", "localhost"), // Database connection URL from environment variable
@ -220,19 +262,65 @@ export async function initTriggerDatabase(triggerDir: string) {
await createOrg(knex);
// Create personal access token
await createPersonalToken(knex);
const personalToken = await createPersonalToken(knex);
// Create project and return details
const projectDetails = await createProject(knex);
log.success("Trigger.dev database initialized successfully.");
log.step("Setting things up...");
await new Promise((resolve) => setTimeout(resolve, 5000));
return {
prodSecretKey: projectDetails.prodSecret,
projectRefId: projectDetails.projectRef,
personalToken,
};
} catch (error) {
log.error(`Initialization failed: ${error}`);
throw new Error(`Initialization failed: ${error}`);
}
}
function getGlobalConfigFolderPath() {
const configDir = xdgAppPaths("trigger").config();
return configDir;
}
const CONFIG_FILE = "config.json";
function getAuthConfigFilePath() {
return path.join(getGlobalConfigFolderPath(), CONFIG_FILE);
}
/**
* Creates the Trigger.dev CLI config.json file in ~/Library/Preferences/trigger/config.json
* with the given personal access token. If the config already exists, it will be deleted first.
*
* @param {string} personalToken - The personal access token to store in the config.
*/
export async function createTriggerConfigJson(personalToken: string) {
const configPath = getAuthConfigFilePath();
// If config.json exists, delete it
mkdirSync(path.dirname(configPath), {
recursive: true,
});
const config = {
version: 2,
currentProfile: "default",
profiles: {
default: {
accessToken: personalToken,
apiUrl: "http://localhost:8030",
},
},
};
writeFileSync(path.join(configPath), JSON.stringify(config, undefined, 2), {
encoding: "utf-8",
});
}

View File

@ -27,7 +27,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 } : { ...process.env },
env: options.env ? { ...process.env, ...options.env } : {},
});
let output = "";

View File

@ -2,6 +2,7 @@ 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<void> {
const webappDir = path.join(rootDir, "apps", "webapp");
@ -63,4 +64,4 @@ export async function deployTriggerTasks(rootDir: string): Promise<void> {
"Manual Deployment"
);
}
}
}

View File

@ -0,0 +1,18 @@
-- CreateTable
CREATE TABLE "UserUsage" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deleted" TIMESTAMP(3),
"availableCredits" INTEGER NOT NULL DEFAULT 0,
"usedCredits" INTEGER NOT NULL DEFAULT 0,
"userId" TEXT NOT NULL,
CONSTRAINT "UserUsage_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "UserUsage_userId_key" ON "UserUsage"("userId");
-- AddForeignKey
ALTER TABLE "UserUsage" ADD CONSTRAINT "UserUsage_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -50,17 +50,17 @@ model OAuthAuthorizationCode {
id String @id @default(cuid())
code String @unique
// OAuth2 specific fields
clientId String
userId String
redirectUri String
scope String?
state String?
codeChallenge String?
clientId String
userId String
redirectUri String
scope String?
state String?
codeChallenge String?
codeChallengeMethod String?
expiresAt DateTime
used Boolean @default(false)
expiresAt DateTime
used Boolean @default(false)
// Relations
client OAuthClient @relation(fields: [clientId], references: [id], onDelete: Cascade)
@ -73,43 +73,43 @@ model OAuthAuthorizationCode {
model OAuthClient {
id String @id @default(cuid())
clientId String @unique
clientId String @unique
clientSecret String
name String
description String?
// Redirect URIs (comma-separated for simplicity)
redirectUris String
// Allowed scopes (comma-separated)
allowedScopes String @default("read")
// Grant types allowed
grantTypes String @default("authorization_code")
// PKCE support
requirePkce Boolean @default(false)
// Client metadata
logoUrl String?
homepageUrl String?
// GitHub-style features
isActive Boolean @default(true)
isActive Boolean @default(true)
// Workspace relationship (like GitHub orgs)
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
workspaceId String
// Created by user (for audit trail)
createdBy User @relation(fields: [createdById], references: [id])
createdById String
// Relations
oauthAuthorizationCodes OAuthAuthorizationCode[]
accessTokens OAuthAccessToken[]
refreshTokens OAuthRefreshToken[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
@ -423,12 +423,26 @@ model User {
Conversation Conversation[]
ConversationHistory ConversationHistory[]
IngestionRule IngestionRule[]
// OAuth2 relations
oauthAuthorizationCodes OAuthAuthorizationCode[]
oauthAccessTokens OAuthAccessToken[]
oauthRefreshTokens OAuthRefreshToken[]
oauthClientsCreated OAuthClient[]
UserUsage UserUsage?
}
model UserUsage {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deleted DateTime?
availableCredits Int @default(0)
usedCredits Int @default(0)
user User @relation(fields: [userId], references: [id])
userId String @unique
}
model WebhookConfiguration {

View File

@ -23,7 +23,6 @@ export function createMCPTransportBridge(
// Forward messages from client to server
clientTransport.onmessage = (message: any, extra: any) => {
console.log(message);
log("[Client→Server]", message.method || message.id);
onMessage?.("client-to-server", message);
@ -41,8 +40,6 @@ export function createMCPTransportBridge(
// Forward messages from server to client
serverTransport.onmessage = (message: any, extra: any) => {
console.log(message);
console.log(JSON.stringify(message), JSON.stringify(extra));
log("[Server→Client]", message.method || message.id);
onMessage?.("server-to-client", message);

View File

@ -39,8 +39,6 @@ export class RemixMCPTransport implements Transport {
return;
}
console.log(message, "message");
if (Object.keys(message).length === 0) {
this.send({});
} else {

4922
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff