@dubeyvishal/orbital-cli 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -0
- package/package.json +65 -0
- package/server/prisma/schema.prisma +117 -0
- package/server/src/cli/ai/googleService.js +107 -0
- package/server/src/cli/chat/chat-with-ai-agent.js +263 -0
- package/server/src/cli/chat/chat-with-ai-tools.js +432 -0
- package/server/src/cli/chat/chat-with-ai.js +269 -0
- package/server/src/cli/commands/ai/wakeUp.js +95 -0
- package/server/src/cli/commands/auth/aboutMe.js +76 -0
- package/server/src/cli/commands/auth/login.js +242 -0
- package/server/src/cli/commands/auth/logout.js +38 -0
- package/server/src/cli/commands/config/setkey.js +19 -0
- package/server/src/cli/main.js +45 -0
- package/server/src/config/agentConfig.js +176 -0
- package/server/src/config/env.js +100 -0
- package/server/src/config/googleConfig.js +6 -0
- package/server/src/config/toolConfig.js +113 -0
- package/server/src/lib/auth.js +37 -0
- package/server/src/lib/db.js +12 -0
- package/server/src/lib/dbHealth.js +106 -0
- package/server/src/lib/orbitalConfig.js +44 -0
- package/server/src/lib/token.js +86 -0
- package/server/src/service/chatService.js +156 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import dotenv from "dotenv";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import os from "os";
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
|
|
10
|
+
// Load env from the server package root (server/.env) regardless of where the process is started.
|
|
11
|
+
const serverEnvPath = path.resolve(__dirname, "../../.env");
|
|
12
|
+
|
|
13
|
+
dotenv.config({ path: serverEnvPath });
|
|
14
|
+
|
|
15
|
+
// Load Orbital user config (stored in the user's home directory) and
|
|
16
|
+
// hydrate env vars if they are not already set.
|
|
17
|
+
try {
|
|
18
|
+
const orbitalConfigPath = path.join(os.homedir(), ".orbital", "config.json");
|
|
19
|
+
if (!process.env.GOOGLE_GENERATIVE_AI_API_KEY && fs.existsSync(orbitalConfigPath)) {
|
|
20
|
+
const raw = fs.readFileSync(orbitalConfigPath, "utf-8");
|
|
21
|
+
const parsed = JSON.parse(raw);
|
|
22
|
+
if (parsed?.geminiApiKey && typeof parsed.geminiApiKey === "string") {
|
|
23
|
+
process.env.GOOGLE_GENERATIVE_AI_API_KEY = parsed.geminiApiKey.trim();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
// ignore
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const stripWrappingQuotes = (value) => {
|
|
31
|
+
if (typeof value !== "string") return value;
|
|
32
|
+
return value.replace(/^\s*"|"\s*$/g, "").trim();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const normalizeNeonPostgresUrl = (raw) => {
|
|
36
|
+
const cleaned = stripWrappingQuotes(raw);
|
|
37
|
+
if (!cleaned) return cleaned;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const url = new URL(cleaned);
|
|
41
|
+
const host = url.hostname || "";
|
|
42
|
+
|
|
43
|
+
const isNeon = host.endsWith("neon.tech");
|
|
44
|
+
if (!isNeon) return cleaned;
|
|
45
|
+
|
|
46
|
+
const isPooler = host.includes("-pooler.");
|
|
47
|
+
|
|
48
|
+
// Neon requires TLS.
|
|
49
|
+
if (!url.searchParams.has("sslmode")) url.searchParams.set("sslmode", "require");
|
|
50
|
+
|
|
51
|
+
// When using Neon pooler (PgBouncer), Prisma should run in PgBouncer mode
|
|
52
|
+
// and keep connection limits low.
|
|
53
|
+
if (isPooler && !url.searchParams.has("pgbouncer")) {
|
|
54
|
+
url.searchParams.set("pgbouncer", "true");
|
|
55
|
+
}
|
|
56
|
+
if (isPooler && !url.searchParams.has("connection_limit")) {
|
|
57
|
+
url.searchParams.set("connection_limit", "1");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Avoid long hangs on cold/paused branches or blocked networks.
|
|
61
|
+
if (!url.searchParams.has("connect_timeout")) {
|
|
62
|
+
url.searchParams.set("connect_timeout", "10");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return url.toString();
|
|
66
|
+
} catch {
|
|
67
|
+
return cleaned;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (process.env.DATABASE_URL) {
|
|
72
|
+
process.env.DATABASE_URL = normalizeNeonPostgresUrl(process.env.DATABASE_URL);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (process.env.DIRECT_DATABASE_URL) {
|
|
76
|
+
process.env.DIRECT_DATABASE_URL = normalizeNeonPostgresUrl(
|
|
77
|
+
process.env.DIRECT_DATABASE_URL
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Helpful warnings for common Neon misconfigurations.
|
|
82
|
+
try {
|
|
83
|
+
if (process.env.DATABASE_URL) {
|
|
84
|
+
const url = new URL(process.env.DATABASE_URL);
|
|
85
|
+
if (url.hostname.includes("-pooler.") && url.searchParams.get("pgbouncer") !== "true") {
|
|
86
|
+
// eslint-disable-next-line no-console
|
|
87
|
+
console.warn(
|
|
88
|
+
"[env] DATABASE_URL points to a Neon pooler host but is missing `pgbouncer=true`. Add it to avoid Prisma connection issues."
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
if (url.hostname.endsWith("neon.tech") && url.searchParams.get("sslmode") !== "require") {
|
|
92
|
+
// eslint-disable-next-line no-console
|
|
93
|
+
console.warn(
|
|
94
|
+
"[env] Neon Postgres should use TLS. Ensure DATABASE_URL includes `sslmode=require`."
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// ignore
|
|
100
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { google } from "@ai-sdk/google";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
|
|
4
|
+
export const availableTools = [
|
|
5
|
+
{
|
|
6
|
+
id: "google_search",
|
|
7
|
+
name: "Google Search",
|
|
8
|
+
description:
|
|
9
|
+
"Access the latest information using Google Search. Useful for current events, news, and real-time information",
|
|
10
|
+
getTool: () => google.tools.googleSearch({}),
|
|
11
|
+
enabled: false,
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: "code_execution",
|
|
15
|
+
name: "Code Execution",
|
|
16
|
+
description:
|
|
17
|
+
"Generate and execute Python code to perform calculations, solve problems or provide accurate information",
|
|
18
|
+
getTool: () => google.tools.codeExecution({}),
|
|
19
|
+
enabled: false,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: "url_context",
|
|
23
|
+
name: "URL Context",
|
|
24
|
+
description:
|
|
25
|
+
"Provide specific URLs that you want the model to analyse directly from the prompt. Supports up to 20 URLs per request.",
|
|
26
|
+
getTool: () => google.tools.urlContext({}),
|
|
27
|
+
enabled: false,
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export const getEnabledTools = () => {
|
|
32
|
+
const tools = {};
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
for (const toolConfig of availableTools) {
|
|
36
|
+
if (toolConfig.enabled) {
|
|
37
|
+
tools[toolConfig.id] = toolConfig.getTool();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (Object.keys(tools).length > 0) {
|
|
42
|
+
console.log(
|
|
43
|
+
chalk.gray(`[DEBUG] Enabled tools: ${Object.keys(tools).join(", ")}`)
|
|
44
|
+
);
|
|
45
|
+
} else {
|
|
46
|
+
console.log(chalk.yellow(`[DEBUG] No tools enabled`));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return Object.keys(tools).length > 0 ? tools : undefined;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error(
|
|
52
|
+
chalk.red(`[ERROR] Failed to initialize tools:`),
|
|
53
|
+
error?.message || error
|
|
54
|
+
);
|
|
55
|
+
console.error(
|
|
56
|
+
chalk.yellow(`Make sure you have @ai-sdk/google version 2.0+ installed`)
|
|
57
|
+
);
|
|
58
|
+
console.error(chalk.yellow(`Run: npm install @ai-sdk/google@latest`));
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const toggleTool = (toolId) => {
|
|
64
|
+
const tool = availableTools.find((t) => t.id === toolId);
|
|
65
|
+
|
|
66
|
+
if (tool) {
|
|
67
|
+
tool.enabled = !tool.enabled;
|
|
68
|
+
console.log(
|
|
69
|
+
chalk.gray(`[DEBUG] Tool ${toolId} toggled to ${tool.enabled}`)
|
|
70
|
+
);
|
|
71
|
+
return tool.enabled;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log(chalk.red(`[DEBUG] Tool ${toolId} not found`));
|
|
75
|
+
return false;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const toogleTool = toggleTool;
|
|
79
|
+
|
|
80
|
+
export const enableTools = (toolIds = []) => {
|
|
81
|
+
console.log(chalk.gray(`[DEBUG] enableTools called with:`), toolIds);
|
|
82
|
+
|
|
83
|
+
availableTools.forEach((tool) => {
|
|
84
|
+
const wasEnabled = tool.enabled;
|
|
85
|
+
tool.enabled = toolIds.includes(tool.id);
|
|
86
|
+
|
|
87
|
+
if (tool.enabled !== wasEnabled) {
|
|
88
|
+
console.log(
|
|
89
|
+
chalk.gray(`[DEBUG] ${tool.id}: ${wasEnabled} -> ${tool.enabled}`)
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const enabledCount = availableTools.filter((t) => t.enabled).length;
|
|
95
|
+
console.log(
|
|
96
|
+
chalk.gray(
|
|
97
|
+
`[DEBUG] Total tools enabled: ${enabledCount} / ${availableTools.length}`
|
|
98
|
+
)
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const getEnabledToolNames = () => {
|
|
103
|
+
const names = availableTools.filter((t) => t.enabled).map((t) => t.name);
|
|
104
|
+
console.log(chalk.gray(`[DEBUG] getEnabledToolNames returning:`), names);
|
|
105
|
+
return names;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export const resetTools = () => {
|
|
109
|
+
availableTools.forEach((tool) => {
|
|
110
|
+
tool.enabled = false;
|
|
111
|
+
});
|
|
112
|
+
console.log(chalk.gray(`[DEBUG] All tools have been reset (disabled)`));
|
|
113
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import "../config/env.js";
|
|
2
|
+
import { betterAuth } from "better-auth";
|
|
3
|
+
import { prismaAdapter } from "better-auth/adapters/prisma";
|
|
4
|
+
import { deviceAuthorization } from "better-auth/plugins";
|
|
5
|
+
import prisma from "./db.js";
|
|
6
|
+
|
|
7
|
+
export const auth = betterAuth({
|
|
8
|
+
database: prismaAdapter(prisma, {
|
|
9
|
+
provider: "postgresql",
|
|
10
|
+
}),
|
|
11
|
+
// IMPORTANT: When the frontend proxies `/api/*` to the backend (Next.js rewrites),
|
|
12
|
+
// auth cookies are set on the frontend origin. The OAuth callback must therefore
|
|
13
|
+
// also land on the frontend origin to avoid `state_mismatch`.
|
|
14
|
+
baseURL:
|
|
15
|
+
process.env.BETTER_AUTH_BASE_URL ||
|
|
16
|
+
process.env.FRONTEND_URL ||
|
|
17
|
+
process.env.CLIENT_ORIGIN ||
|
|
18
|
+
"http://localhost:3000",
|
|
19
|
+
basePath:"/api/auth" ,
|
|
20
|
+
trustedOrigins: [
|
|
21
|
+
process.env.CLIENT_ORIGIN ||
|
|
22
|
+
process.env.FRONTEND_URL ||
|
|
23
|
+
"https://smart-cli-based-agent-t7x4.vercel.app",
|
|
24
|
+
"http://localhost:3000",
|
|
25
|
+
],
|
|
26
|
+
plugins: [
|
|
27
|
+
deviceAuthorization({
|
|
28
|
+
verificationUri: "/device",
|
|
29
|
+
}),
|
|
30
|
+
],
|
|
31
|
+
socialProviders :{
|
|
32
|
+
github : {
|
|
33
|
+
clientId : process.env.GITHUB_CLIENT_ID ,
|
|
34
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import "../config/env.js";
|
|
2
|
+
import { PrismaClient } from "@prisma/client";
|
|
3
|
+
|
|
4
|
+
const globalForPrisma = globalThis;
|
|
5
|
+
|
|
6
|
+
const prisma = globalForPrisma.prisma ?? new PrismaClient();
|
|
7
|
+
|
|
8
|
+
if (process.env.NODE_ENV !== "production") {
|
|
9
|
+
globalForPrisma.prisma = prisma;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default prisma;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import prisma from "./db.js";
|
|
3
|
+
|
|
4
|
+
const stripWrappingQuotes = (value) => {
|
|
5
|
+
if (typeof value !== "string") return value;
|
|
6
|
+
return value.replace(/^\s*"|"\s*$/g, "").trim();
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const getDatabaseHostHint = () => {
|
|
10
|
+
const raw = stripWrappingQuotes(process.env.DATABASE_URL);
|
|
11
|
+
if (!raw) return undefined;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const url = new URL(raw);
|
|
15
|
+
const port = url.port || "5432";
|
|
16
|
+
return `${url.hostname}:${port}`;
|
|
17
|
+
} catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const isPrismaDbConnectionError = (error) => {
|
|
23
|
+
const message = String(error?.message || "");
|
|
24
|
+
return (
|
|
25
|
+
error?.name === "PrismaClientInitializationError" ||
|
|
26
|
+
message.includes("Can't reach database server") ||
|
|
27
|
+
message.includes("P1001")
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const formatDbConnectionTroubleshooting = () => {
|
|
32
|
+
const hostHint = getDatabaseHostHint();
|
|
33
|
+
const hostLine = hostHint ? ` at ${hostHint}` : "";
|
|
34
|
+
|
|
35
|
+
return [
|
|
36
|
+
chalk.red(`Database connection failed${hostLine}.`),
|
|
37
|
+
chalk.gray("\nFix checklist:"),
|
|
38
|
+
chalk.gray("- Verify `server/.env` has a valid `DATABASE_URL`"),
|
|
39
|
+
chalk.gray("- Ensure your Neon project/branch is running (not paused)"),
|
|
40
|
+
chalk.gray("- Check VPN/firewall/outbound access to port 5432"),
|
|
41
|
+
chalk.gray("- If you changed schema, run: `npm run prisma:migrate` in server/"),
|
|
42
|
+
].join("\n");
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const ensureDbConnection = async () => {
|
|
46
|
+
return ensureDbConnectionWithRetry();
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
50
|
+
|
|
51
|
+
export const ensureDbConnectionWithRetry = async (options = {}) => {
|
|
52
|
+
const {
|
|
53
|
+
retries = Number(process.env.DB_CONNECT_RETRIES || 6),
|
|
54
|
+
initialDelayMs = 500,
|
|
55
|
+
maxDelayMs = 5000,
|
|
56
|
+
logAttempts = true,
|
|
57
|
+
} = options;
|
|
58
|
+
|
|
59
|
+
const attempts = Math.max(1, Number(retries) + 1);
|
|
60
|
+
let delayMs = initialDelayMs;
|
|
61
|
+
|
|
62
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
63
|
+
try {
|
|
64
|
+
await prisma.$connect();
|
|
65
|
+
await prisma.$queryRaw`SELECT 1`;
|
|
66
|
+
return true;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
const isConnectionError = isPrismaDbConnectionError(error);
|
|
69
|
+
const isLastAttempt = attempt === attempts;
|
|
70
|
+
|
|
71
|
+
if (!isLastAttempt && isConnectionError) {
|
|
72
|
+
if (logAttempts) {
|
|
73
|
+
const hostHint = getDatabaseHostHint();
|
|
74
|
+
const hostLine = hostHint ? ` (${hostHint})` : "";
|
|
75
|
+
console.log(
|
|
76
|
+
chalk.yellow(
|
|
77
|
+
`Database not reachable${hostLine}. Retry ${attempt}/${attempts - 1} in ${delayMs}ms...`
|
|
78
|
+
)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
await sleep(delayMs);
|
|
82
|
+
delayMs = Math.min(maxDelayMs, Math.floor(delayMs * 1.8));
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (isConnectionError) {
|
|
87
|
+
console.log(formatDbConnectionTroubleshooting());
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log(chalk.red(`Database error: ${error?.message || error}`));
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return false;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const ensureDbConnectionOrExit = async (options = {}) => {
|
|
100
|
+
const ok = await ensureDbConnectionWithRetry(options);
|
|
101
|
+
if (ok) return true;
|
|
102
|
+
|
|
103
|
+
// eslint-disable-next-line no-console
|
|
104
|
+
console.error(chalk.red("\nServer cannot start without database connectivity. Exiting."));
|
|
105
|
+
process.exit(1);
|
|
106
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import fsPromises from "fs/promises";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
|
|
6
|
+
export const ORBITAL_CONFIG_DIR = path.join(os.homedir(), ".orbital");
|
|
7
|
+
export const ORBITAL_CONFIG_FILE = path.join(ORBITAL_CONFIG_DIR, "config.json");
|
|
8
|
+
|
|
9
|
+
export const readOrbitalConfigSync = () => {
|
|
10
|
+
try {
|
|
11
|
+
if (!fs.existsSync(ORBITAL_CONFIG_FILE)) return {};
|
|
12
|
+
const raw = fs.readFileSync(ORBITAL_CONFIG_FILE, "utf-8");
|
|
13
|
+
const parsed = JSON.parse(raw);
|
|
14
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
15
|
+
} catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const getGeminiApiKeySync = () => {
|
|
21
|
+
const config = readOrbitalConfigSync();
|
|
22
|
+
const key = config?.geminiApiKey;
|
|
23
|
+
return typeof key === "string" ? key.trim() : "";
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const setGeminiApiKey = async (apiKey) => {
|
|
27
|
+
const trimmed = typeof apiKey === "string" ? apiKey.trim() : "";
|
|
28
|
+
if (!trimmed) throw new Error("API key is required");
|
|
29
|
+
|
|
30
|
+
await fsPromises.mkdir(ORBITAL_CONFIG_DIR, { recursive: true });
|
|
31
|
+
|
|
32
|
+
const nextConfig = {
|
|
33
|
+
geminiApiKey: trimmed,
|
|
34
|
+
updatedAt: new Date().toISOString(),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
await fsPromises.writeFile(
|
|
38
|
+
ORBITAL_CONFIG_FILE,
|
|
39
|
+
JSON.stringify(nextConfig, null, 2),
|
|
40
|
+
"utf-8",
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return true;
|
|
44
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
|
|
2
|
+
import chalk from "chalk"
|
|
3
|
+
import fs from "fs/promises";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import path from "path";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export const CONFIG_DIR = path.join(os.homedir(), ".better-auth");
|
|
9
|
+
export const TOKEN_FILE = path.join(CONFIG_DIR, "token.json");
|
|
10
|
+
|
|
11
|
+
export const getStoredToken = async ()=>{
|
|
12
|
+
try{
|
|
13
|
+
const data = await fs.readFile(TOKEN_FILE , "utf-8");
|
|
14
|
+
const token = JSON.parse(data);
|
|
15
|
+
return token ;
|
|
16
|
+
}
|
|
17
|
+
catch(error){
|
|
18
|
+
return null ;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
export const storeToken = async(token)=>{
|
|
24
|
+
try{
|
|
25
|
+
await fs.mkdir(CONFIG_DIR , { recursive : true });
|
|
26
|
+
|
|
27
|
+
const tokenData = {
|
|
28
|
+
access_token : token.access_token,
|
|
29
|
+
refresh_token : token.refresh_token ,
|
|
30
|
+
token_type : token.token_type ,
|
|
31
|
+
scope : token.scope ,
|
|
32
|
+
expires_at : token.expires_in
|
|
33
|
+
? new Date(Date.now() + token.expires_in*1000).toISOString()
|
|
34
|
+
: null ,
|
|
35
|
+
created_at : new Date().toISOString() ,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
await fs.writeFile(TOKEN_FILE , JSON.stringify(tokenData , null , 2), "utf-8");
|
|
39
|
+
return true ;
|
|
40
|
+
}
|
|
41
|
+
catch(err){
|
|
42
|
+
console.log(chalk.red("Failed to store token: " ) , err.message);
|
|
43
|
+
return false ;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const clearStoredToken = async ()=>{
|
|
48
|
+
try{
|
|
49
|
+
await fs.unlink(TOKEN_FILE);
|
|
50
|
+
return true ;
|
|
51
|
+
}
|
|
52
|
+
catch(err){
|
|
53
|
+
return false ;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const isTokenExpired = async()=>{
|
|
58
|
+
const token = await getStoredToken();
|
|
59
|
+
if(!token || !token.expires_at){
|
|
60
|
+
return true ;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const expiresAt = new Date(token.expires_at);
|
|
64
|
+
const now = new Date();
|
|
65
|
+
|
|
66
|
+
return expiresAt.getTime() - now.getTime() < 5*60*1000 ;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const requireAuth = async()=>{
|
|
70
|
+
const token = await getStoredToken();
|
|
71
|
+
|
|
72
|
+
if(!token){
|
|
73
|
+
console.log(
|
|
74
|
+
chalk.red("Not authenticated . PLease run 'orbital login' first")
|
|
75
|
+
)
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
if(await isTokenExpired()){
|
|
79
|
+
console.log(
|
|
80
|
+
chalk.yellow("Your session has expired. Please login again.")
|
|
81
|
+
);
|
|
82
|
+
console.log(chalk.gray("Run: orbital login \n"));
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
return token ;
|
|
86
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import prisma from "../lib/db.js";
|
|
2
|
+
|
|
3
|
+
export class ChatService{
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create a new conversation
|
|
7
|
+
* @param {string} userId - user id
|
|
8
|
+
* @param {string} mode - chat tool , or agent
|
|
9
|
+
* @param {string} title - Optional conversation title
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
async createConversation(userId , mode ="chat" , title=null){
|
|
13
|
+
return prisma.conversation.create({
|
|
14
|
+
data : {
|
|
15
|
+
userId ,
|
|
16
|
+
mode ,
|
|
17
|
+
title : title || `New ${mode} conversation`
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* get or create a new conversation for user
|
|
24
|
+
* @param {string} userId - user id
|
|
25
|
+
* @param {string} conversationalId - Optional conversation id
|
|
26
|
+
* @param {string} mode - chat , tool or agent
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
async getOrCreateConversation(userId , conversationId =null , mode="chat"){
|
|
30
|
+
if(conversationId){
|
|
31
|
+
const conversation = await prisma.conversation.findFirst({
|
|
32
|
+
where : {
|
|
33
|
+
id: conversationId ,
|
|
34
|
+
userId
|
|
35
|
+
},
|
|
36
|
+
include : {
|
|
37
|
+
messages : {
|
|
38
|
+
orderBy :{
|
|
39
|
+
createdAt : "asc"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if(conversation){
|
|
46
|
+
return conversation ;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return await this.createConversation(userId , mode)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Add a new message to conversation
|
|
54
|
+
* @param {string} conversationalId - conversation id
|
|
55
|
+
* @param {string} role - user , assistant , system ,tool
|
|
56
|
+
* @param {string | object} content - message content
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
async addMessage(conversationId , role , content){
|
|
60
|
+
|
|
61
|
+
const contentStr = typeof content === "string" ? content :
|
|
62
|
+
JSON.stringify(content);
|
|
63
|
+
|
|
64
|
+
return await prisma.message.create({
|
|
65
|
+
data :{
|
|
66
|
+
conversationId ,
|
|
67
|
+
role ,
|
|
68
|
+
content : contentStr
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get conversion messages
|
|
75
|
+
* @param {string} conversationId - conversational -id
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
async getMessages(conversationId){
|
|
79
|
+
const messages = await prisma.message.findMany({
|
|
80
|
+
where : {conversationId},
|
|
81
|
+
orderBy : {createdAt : "asc"}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return messages.map((msg) => ({
|
|
85
|
+
...msg,
|
|
86
|
+
content: this.parseContent(msg.content),
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param {string} userI - user id
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
async getUserConversation(userId){
|
|
95
|
+
return await prisma.conversation.findMany({
|
|
96
|
+
where : {userId} ,
|
|
97
|
+
orderBy :{updatedAt : "desc"},
|
|
98
|
+
include :{
|
|
99
|
+
messages : {
|
|
100
|
+
take : 1,
|
|
101
|
+
orderBy : {createdAt : "desc"}
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param {string} conversationId - Conversation ID
|
|
109
|
+
* @param {string} userId - User ID
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
async deleteConversation(conversationId , userId){
|
|
113
|
+
return await prisma.conversation.deleteMany({
|
|
114
|
+
where : {
|
|
115
|
+
id : conversationId ,
|
|
116
|
+
userId
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @param {string} conversationId - Conversation ID
|
|
123
|
+
* @param {string} title - User ID
|
|
124
|
+
*/
|
|
125
|
+
|
|
126
|
+
async updateTitle(conversationId , title){
|
|
127
|
+
return await prisma.conversation.update({
|
|
128
|
+
where : {id : conversationId},
|
|
129
|
+
data : {title}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Helper to parse content ( json to string)
|
|
135
|
+
*/
|
|
136
|
+
|
|
137
|
+
parseContent(content){
|
|
138
|
+
try{
|
|
139
|
+
return JSON.parse(content);
|
|
140
|
+
}
|
|
141
|
+
catch{
|
|
142
|
+
return content
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* @param {Array} messages - database messages
|
|
148
|
+
*/
|
|
149
|
+
|
|
150
|
+
formatMessageForAI(messages){
|
|
151
|
+
return messages.map((msg)=>({
|
|
152
|
+
role : msg.role ,
|
|
153
|
+
content : typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content),
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
}
|