@dubeyvishal/orbital-cli 1.0.2 → 1.0.4

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.
Files changed (38) hide show
  1. package/README.md +13 -0
  2. package/package.json +10 -7
  3. package/server/{src/cli/ai → ai}/googleService.js +1 -1
  4. package/server/{src/cli/chat → chat}/chat-with-ai-agent.js +21 -22
  5. package/server/{src/cli/chat → chat}/chat-with-ai-tools.js +22 -21
  6. package/server/{src/cli/chat → chat}/chat-with-ai.js +21 -18
  7. package/server/commands/General/openApp.js +71 -0
  8. package/server/commands/General/playSong.js +17 -0
  9. package/server/commands/General/searchYoutube.js +21 -0
  10. package/server/{src/cli/commands → commands}/ai/wakeUp.js +19 -28
  11. package/server/{src/cli/commands → commands}/auth/aboutMe.js +1 -1
  12. package/server/{src/cli/commands → commands}/auth/login.js +1 -1
  13. package/server/{src/cli/commands → commands}/auth/logout.js +1 -1
  14. package/server/{src/cli/commands → commands}/config/setkey.js +1 -1
  15. package/server/config/env.js +20 -0
  16. package/server/generalApp/Apps.js +21 -0
  17. package/server/{src/cli/main.js → main.js} +11 -3
  18. package/server/utils/apiClient.js +40 -0
  19. package/server/utils/chatServiceClient.js +73 -0
  20. package/server/prisma/migrations/20260105143219_test_migration/migration.sql +0 -7
  21. package/server/prisma/migrations/20260105151026_authentication/migration.sql +0 -78
  22. package/server/prisma/migrations/20260114105919_add_devicecode_conversation_message/migration.sql +0 -50
  23. package/server/prisma/migrations/migration_lock.toml +0 -3
  24. package/server/prisma/schema.prisma +0 -117
  25. package/server/src/config/env.js +0 -100
  26. package/server/src/index.js +0 -102
  27. package/server/src/lib/auth.js +0 -37
  28. package/server/src/lib/db.js +0 -18
  29. package/server/src/lib/dbHealth.js +0 -106
  30. package/server/src/prisma/migrations/20260107093841_device_flow/migration.sql +0 -94
  31. package/server/src/prisma/migrations/migration_lock.toml +0 -3
  32. package/server/src/prisma/schema.prisma +0 -115
  33. package/server/src/service/chatService.js +0 -156
  34. /package/server/{src/config → config}/agentConfig.js +0 -0
  35. /package/server/{src/config → config}/googleConfig.js +0 -0
  36. /package/server/{src/config → config}/toolConfig.js +0 -0
  37. /package/server/{src/lib → utils}/orbitalConfig.js +0 -0
  38. /package/server/{src/lib → utils}/token.js +0 -0
package/README.md CHANGED
@@ -6,6 +6,19 @@ Built with **Next.js**, **Node.js**, **Express**, and **Commander.js**, with sec
6
6
 
7
7
  ---
8
8
 
9
+ ## 📸 Project Screenshots
10
+
11
+ ### 🔐 Orbital Login Screen
12
+ ![Orbital Login Screen](./images/pic1.png)
13
+
14
+ ### 🛠️ Orbital Wakeup – Tools Loaded
15
+ ![Orbital Wakeup](./images/pic2.png)
16
+
17
+ ### 🤖 Agentic Mode
18
+ ![Agentic Mode](./images/pic3.png)
19
+
20
+ ---
21
+
9
22
  ## ✨ Features
10
23
 
11
24
  ### AI Chat Mode
package/package.json CHANGED
@@ -1,17 +1,22 @@
1
1
  {
2
2
  "name": "@dubeyvishal/orbital-cli",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "A fullstack CLI-based AI platform with chat mode, multi-tool agents, and agentic AI workflows. Includes GitHub login with device authorization, secure authentication, and modular client–server architecture for building intelligent automation tools.",
5
5
  "author": "Vishal Dubey",
6
6
  "license": "MIT",
7
7
  "private": false,
8
8
  "type": "module",
9
9
  "bin": {
10
- "orbital": "server/src/cli/main.js"
10
+ "orbital": "server/main.js"
11
11
  },
12
12
  "files": [
13
- "server/src",
14
- "server/prisma",
13
+ "server/main.js",
14
+ "server/commands",
15
+ "server/chat",
16
+ "server/ai",
17
+ "server/config",
18
+ "server/generalApp",
19
+ "server/utils",
15
20
  "README.md"
16
21
  ],
17
22
  "publishConfig": {
@@ -39,8 +44,6 @@
39
44
  "zod": "^4.3.5"
40
45
  },
41
46
  "scripts": {
42
- "postinstall": "cd server && npx prisma generate",
43
- "install-all": "npm install --prefix client && npm install --prefix server",
44
47
  "start-client": "npm start --prefix client",
45
48
  "start-server": "npm start --prefix server",
46
49
  "dev": "npm run start-server && npm run start-client"
@@ -53,4 +56,4 @@
53
56
  "client-server",
54
57
  "javascript"
55
58
  ]
56
- }
59
+ }
@@ -1,6 +1,6 @@
1
1
  import { google } from "@ai-sdk/google";
2
2
  import { streamText, generateObject } from "ai";
3
- import { config } from "../../config/googleConfig.js";
3
+ import { config } from "../config/googleConfig.js";
4
4
  import chalk from "chalk";
5
5
 
6
6
  export class AIService {
@@ -5,11 +5,10 @@ import yoctoSpinner from "yocto-spinner";
5
5
  import { marked } from "marked";
6
6
  import { markedTerminal } from "marked-terminal";
7
7
  import { AIService } from "../ai/googleService.js";
8
- import { ChatService } from "../../service/chatService.js";
9
- import { getStoredToken } from "../../lib/token.js";
10
- import prisma from "../../lib/db.js";
11
- import { ensureDbConnection } from "../../lib/dbHealth.js";
12
- import { generateApplication } from "../../config/agentConfig.js";
8
+ import { ChatServiceClient as ChatService } from "../utils/chatServiceClient.js";
9
+ import { getStoredToken } from "../utils/token.js";
10
+
11
+ import { generateApplication } from "../config/agentConfig.js";
13
12
 
14
13
  marked.use(markedTerminal());
15
14
 
@@ -27,26 +26,26 @@ const getUserFromToken = async () => {
27
26
  throw new Error("Not authenticated. Please run 'orbital login' first.");
28
27
  }
29
28
 
30
- const dbOk = await ensureDbConnection();
31
- if (!dbOk) {
32
- throw new Error(
33
- "Database unavailable. Fix DATABASE_URL/connectivity and try again."
34
- );
35
- }
36
-
37
29
  const spinner = yoctoSpinner({ text: "Authenticating..." }).start();
38
30
 
39
- const user = await prisma.user.findFirst({
40
- where: {
41
- sessions: {
42
- some: { token: token.access_token },
43
- },
44
- },
45
- });
31
+ let user;
32
+ try {
33
+ const response = await fetch("https://smart-cli-based-agent.onrender.com/api/me", {
34
+ headers: {
35
+ Authorization: `Bearer ${token.access_token}`
36
+ }
37
+ });
38
+
39
+ if (!response.ok) {
40
+ throw new Error("Failed to authenticate.");
41
+ }
42
+ const data = await response.json();
43
+ user = data?.user;
46
44
 
47
- if (!user) {
48
- spinner.error("User not found");
49
- throw new Error("User not found. Please login again");
45
+ if (!user) throw new Error("User not found");
46
+ } catch(err) {
47
+ spinner.error("Authentication failed");
48
+ throw new Error("User not found or connection failed. Please login again.");
50
49
  }
51
50
 
52
51
  spinner.success(`Welcome back, ${user.name}!`);
@@ -12,17 +12,16 @@ import yoctoSpinner from "yocto-spinner";
12
12
  import { marked } from "marked";
13
13
  import { markedTerminal } from "marked-terminal";
14
14
  import { AIService } from "../ai/googleService.js";
15
- import { ChatService } from "../../service/chatService.js";
16
- import { getStoredToken } from "../../lib/token.js";
17
- import prisma from "../../lib/db.js";
18
- import { ensureDbConnection } from "../../lib/dbHealth.js";
15
+ import { ChatServiceClient as ChatService } from "../utils/chatServiceClient.js";
16
+ import { getStoredToken } from "../utils/token.js";
17
+
19
18
  import {
20
19
  availableTools,
21
20
  enableTools,
22
21
  getEnabledTools,
23
22
  getEnabledToolNames,
24
23
  resetTools,
25
- } from "../../config/toolConfig.js";
24
+ } from "../config/toolConfig.js";
26
25
 
27
26
  marked.use(
28
27
  markedTerminal({
@@ -53,24 +52,26 @@ const getUserFromToken = async () => {
53
52
  throw new Error("Not authenticated. Please run 'orbital login' first.");
54
53
  }
55
54
 
56
- const dbOk = await ensureDbConnection();
57
- if (!dbOk) {
58
- throw new Error("Database unavailable. Fix DATABASE_URL/connectivity and try again.");
59
- }
60
-
61
55
  const spinner = yoctoSpinner({ text: "Authenticating..." }).start();
62
56
 
63
- const user = await prisma.user.findFirst({
64
- where: {
65
- sessions: {
66
- some: { token: token.access_token },
67
- },
68
- },
69
- });
57
+ let user;
58
+ try {
59
+ const response = await fetch("https://smart-cli-based-agent.onrender.com/api/me", {
60
+ headers: {
61
+ Authorization: `Bearer ${token.access_token}`
62
+ }
63
+ });
64
+
65
+ if (!response.ok) {
66
+ throw new Error("Failed to authenticate.");
67
+ }
68
+ const data = await response.json();
69
+ user = data?.user;
70
70
 
71
- if (!user) {
72
- spinner.error("User not found");
73
- throw new Error("User not found. Please login again");
71
+ if (!user) throw new Error("User not found");
72
+ } catch(err) {
73
+ spinner.error("Authentication failed");
74
+ throw new Error("User not found or connection failed. Please login again.");
74
75
  }
75
76
 
76
77
  spinner.success(`Welcome back, ${user.name}!`);
@@ -88,7 +89,7 @@ const selectTools = async () => {
88
89
  const toolOptions = availableTools.map((tool) => ({
89
90
  value: tool.id,
90
91
  label: tool.name,
91
- // Long hints wrap in the terminal and look like duplicated options.
92
+
92
93
  hint: truncateHint(tool.description),
93
94
  }));
94
95
 
@@ -5,10 +5,9 @@ import yoctoSpinner from "yocto-spinner";
5
5
  import {marked} from "marked";
6
6
  import {markedTerminal} from "marked-terminal";
7
7
  import {AIService} from "../ai/googleService.js";
8
- import {ChatService} from "../../service/chatService.js"
9
- import {getStoredToken} from "../../lib/token.js";
10
- import prisma from "../../lib/db.js"
11
- import { ensureDbConnection } from "../../lib/dbHealth.js";
8
+ import { ChatServiceClient as ChatService } from "../utils/chatServiceClient.js";
9
+ import {getStoredToken} from "../utils/token.js";
10
+
12
11
 
13
12
  marked.use(
14
13
  markedTerminal({
@@ -38,22 +37,26 @@ const getUserFromToken = async()=>{
38
37
  throw new Error("Not authenticated. Please run 'orbital login' first.");
39
38
  }
40
39
 
41
- const dbOk = await ensureDbConnection();
42
- if(!dbOk){
43
- throw new Error("Database unavailable. Fix DATABASE_URL/connectivity and try again.");
44
- }
45
-
46
40
  const spinner = yoctoSpinner({text: "Authenticating..."}).start();
47
- const user = await prisma.user.findFirst({
48
- where : {
49
- sessions : {
50
- some : {token : token.access_token} ,
51
- }}
52
- });
53
- if(!user){
54
- spinner.error("User not found");
55
- throw new Error("User not found. Please login again");
41
+
42
+ let user;
43
+ try {
44
+ const response = await fetch("https://smart-cli-based-agent.onrender.com/api/me", {
45
+ headers: {
46
+ Authorization: `Bearer ${token.access_token}`
47
+ }
48
+ });
49
+ if (!response.ok) {
50
+ throw new Error("Failed to authenticate.");
51
+ }
52
+ const data = await response.json();
53
+ user = data?.user;
54
+ if (!user) throw new Error("User not found");
55
+ } catch(err) {
56
+ spinner.error("Authentication failed");
57
+ throw new Error("User not found or connection failed. Please login again.");
56
58
  }
59
+
57
60
  spinner.success(`Welcome back , ${user.name}!`);
58
61
  return user;
59
62
  }
@@ -0,0 +1,71 @@
1
+ import chalk from "chalk";
2
+ import { Command } from "commander";
3
+ import yoctoSpinner from "yocto-spinner";
4
+ import { getApiClient } from "../../utils/apiClient.js";
5
+ import {openGithub , openLinkedin , openLeetcode , openGmail , openWhatsApp} from "../../generalApp/Apps.js"
6
+
7
+ const openAppAction = async () => {
8
+ const spinner = yoctoSpinner({ text: "Fetching user information..." });
9
+ spinner.start();
10
+
11
+ let user;
12
+ try {
13
+ const api = await getApiClient();
14
+ const res = await api.get("/api/me");
15
+ user = res.user;
16
+ } catch (err) {
17
+ console.log(chalk.red(err.message));
18
+ return;
19
+ } finally {
20
+ spinner.stop();
21
+ }
22
+
23
+ if (!user) {
24
+ console.log(chalk.red("User not found."));
25
+ return;
26
+ }
27
+
28
+ console.log(chalk.green(`Welcome back, ${user.name}!\n`));
29
+
30
+ while (true) {
31
+ const choice = await select({
32
+ message: chalk.yellow("Select the app to open"),
33
+ options: [
34
+ { value: "github", label: "GitHub", hint: "View repositories & projects" },
35
+ { value: "whatsapp", label: "WhatsApp", hint: "Open messaging app" },
36
+ { value: "gmail", label: "Gmail", hint: "Check emails quickly" },
37
+ { value: "linkedin", label: "LinkedIn", hint: "View connections & jobs" },
38
+ { value: "leetcode", label: "LeetCode", hint: "Practice coding problems" },
39
+ { value: "exit", label: "Exit", hint: "Close launcher" },
40
+ ],
41
+ });
42
+
43
+ if (choice === "exit") {
44
+ console.log(" Exiting launcher...");
45
+ break;
46
+ }
47
+
48
+ switch (choice) {
49
+ case "github":
50
+ openGithub();
51
+ break;
52
+ case "gmail":
53
+ openGmail();
54
+ break;
55
+ case "leetcode":
56
+ openLeetcode();
57
+ break;
58
+ case "linkedin":
59
+ openLinkedin();
60
+ break;
61
+ case "whatsapp":
62
+ openWhatsApp();
63
+ break;
64
+ }
65
+ }
66
+ }
67
+
68
+ export const launch = new Command("launch")
69
+ .description("Open external apps like GitHub, WhatsApp, etc.")
70
+ .alias("open")
71
+ .action(openAppAction);
@@ -0,0 +1,17 @@
1
+ import { Command } from "commander";
2
+ import { exec } from "child_process";
3
+ import chalk from "chalk";
4
+
5
+ const playAction = (songParts) => {
6
+ const song = songParts.join(" ");
7
+ const encoded = encodeURIComponent(song);
8
+
9
+ console.log(chalk.green(`Searching "${song}" on Spotify ...`));
10
+
11
+ exec(`start /min "" spotify:search:${encoded}`);
12
+ };
13
+
14
+ export const play = new Command("play")
15
+ .description("Search and open Spotify minimized")
16
+ .argument("<song...>", "Song name to search")
17
+ .action(playAction);
@@ -0,0 +1,21 @@
1
+ import { Command } from "commander";
2
+ import { exec } from "child_process";
3
+ import chalk from "chalk";
4
+
5
+ const searchYoutubeAction = (query) => {
6
+ if (!query) {
7
+ console.log(chalk.red("Please provide a search query."));
8
+ return;
9
+ }
10
+
11
+ const encodedQuery = encodeURIComponent(query); //removes the special characters so that url does not break
12
+ const url = `https://www.youtube.com/results?search_query=${encodedQuery}`;
13
+
14
+ console.log(chalk.green(`Searching YouTube for: "${query}"`));
15
+ exec(`start "" "${url}"`);
16
+ };
17
+
18
+ export const search = new Command("search")
19
+ .description("Search videos on YouTube")
20
+ .argument("<query>", "Search term")
21
+ .action(searchYoutubeAction);
@@ -1,13 +1,12 @@
1
1
  import chalk from "chalk";
2
2
  import {Command} from "commander";
3
3
  import yoctoSpinner from "yocto-spinner";
4
- import {getStoredToken} from "../../../lib/token.js"
5
- import prisma from "../../../lib/db.js"
6
- import { ensureDbConnection } from "../../../lib/dbHealth.js";
4
+ import {getStoredToken} from "../../utils/token.js"
5
+
7
6
  import {select} from "@clack/prompts";
8
- import {startChat} from "../../../cli/chat/chat-with-ai.js";
9
- import {startToolChat} from "../../../cli/chat/chat-with-ai-tools.js";
10
- import {startAgentChat} from "../../../cli/chat/chat-with-ai-agent.js";
7
+ import {startChat} from "../../chat/chat-with-ai.js";
8
+ import {startToolChat} from "../../chat/chat-with-ai-tools.js";
9
+ import {startAgentChat} from "../../chat/chat-with-ai-agent.js";
11
10
 
12
11
 
13
12
  const wakeUpAction = async()=>{
@@ -17,31 +16,23 @@ const wakeUpAction = async()=>{
17
16
  return ;
18
17
  }
19
18
 
20
- const dbOk = await ensureDbConnection();
21
- if(!dbOk){
22
- return;
23
- }
24
-
25
- const spinner = yoctoSpinner({text: "Fetching user information ..."})
26
- spinner.start()
27
-
28
19
  let user;
29
20
  try{
30
- user = await prisma.user.findFirst({
31
- where : {
32
- sessions : {
33
- some : {
34
- token : token.access_token
35
- }
36
- }
37
- },
38
- select : {
39
- id : true ,
40
- name : true ,
41
- email : true ,
42
- image : true
43
- }
21
+ const response = await fetch("https://smart-cli-based-agent.onrender.com/api/me", {
22
+ headers: {
23
+ Authorization: `Bearer ${token.access_token}`
24
+ }
44
25
  });
26
+
27
+ if (!response.ok) {
28
+ throw new Error("Failed to authenticate.");
29
+ }
30
+ const data = await response.json();
31
+ user = data?.user;
32
+
33
+ if (!user) throw new Error("User not found");
34
+ } catch(err) {
35
+ console.log(chalk.red("Failed to authenticate with server."));
45
36
  }
46
37
  finally{
47
38
  spinner.stop();
@@ -1,6 +1,6 @@
1
1
  import chalk from "chalk";
2
2
  import { Command } from "commander";
3
- import { requireAuth } from "../../../lib/token.js";
3
+ import { requireAuth } from "../../utils/token.js";
4
4
 
5
5
  const DEFAULT_SERVER_URL = "https://smart-cli-based-agent.onrender.com";
6
6
 
@@ -10,7 +10,7 @@ import { createAuthClient } from "better-auth/client";
10
10
  import { deviceAuthorizationClient } from "better-auth/client/plugins";
11
11
  import { logger } from "better-auth";
12
12
  import { fileURLToPath } from "url";
13
- import { getStoredToken, isTokenExpired, storeToken ,TOKEN_FILE } from "../../../lib/token.js";
13
+ import { getStoredToken, isTokenExpired, storeToken ,TOKEN_FILE } from "../../utils/token.js";
14
14
 
15
15
 
16
16
 
@@ -1,7 +1,7 @@
1
1
  import chalk from "chalk";
2
2
  import { cancel, confirm, intro, outro, isCancel } from "@clack/prompts";
3
3
  import { Command } from "commander";
4
- import { clearStoredToken, getStoredToken } from "../../../lib/token.js";
4
+ import { clearStoredToken, getStoredToken } from "../../utils/token.js";
5
5
 
6
6
  export const logoutAction = async()=>{
7
7
  intro(chalk.bold("Logout"));
@@ -1,6 +1,6 @@
1
1
  import { Command } from "commander";
2
2
  import chalk from "chalk";
3
- import { setGeminiApiKey, ORBITAL_CONFIG_FILE } from "../../../lib/orbitalConfig.js";
3
+ import { setGeminiApiKey, ORBITAL_CONFIG_FILE } from "../../utils/orbitalConfig.js";
4
4
 
5
5
  const setKeyAction = async (apiKey) => {
6
6
  try {
@@ -0,0 +1,20 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import os from "os";
4
+
5
+ // Load Orbital user config (stored in the user's home directory) and
6
+ // hydrate env vars if they are not already set.
7
+ try {
8
+ const orbitalConfigPath = path.join(os.homedir(), ".orbital", "config.json");
9
+ if (!process.env.GOOGLE_GENERATIVE_AI_API_KEY && fs.existsSync(orbitalConfigPath)) {
10
+ const raw = fs.readFileSync(orbitalConfigPath, "utf-8");
11
+ const parsed = JSON.parse(raw);
12
+ if (parsed?.geminiApiKey && typeof parsed.geminiApiKey === "string") {
13
+ process.env.GOOGLE_GENERATIVE_AI_API_KEY = parsed.geminiApiKey.trim();
14
+ }
15
+ }
16
+ } catch {
17
+ // ignore
18
+ }
19
+
20
+
@@ -0,0 +1,21 @@
1
+ import { exec } from "child_process";
2
+ import chalk from "chalk";
3
+
4
+ const openApp = (url, name) => {
5
+ console.log(chalk.green(`Opening ${name}...`));
6
+ exec(`start "" "${url}"`);
7
+ };
8
+
9
+ export const openGithub = () => openApp("https://github.com", "GitHub");
10
+ export const openGmail = () => openApp("https://gmail.com", "Gmail");
11
+ export const openLeetcode = () => openApp("https://leetcode.com", "LeetCode");
12
+ export const openLinkedin = () => openApp("https://linkedin.com", "LinkedIn");
13
+
14
+ export const openWhatsApp = () => {
15
+ exec("start whatsapp:", (err) => {
16
+ if (err) {
17
+ exec('start "" "https://web.whatsapp.com"');
18
+ }
19
+ });
20
+ };
21
+
@@ -1,6 +1,6 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
- import "../config/env.js";
3
+ import "./config/env.js";
4
4
  import chalk from "chalk";
5
5
  import figlet from "figlet";
6
6
  import {Command} from "commander";
@@ -9,6 +9,9 @@ import {logout} from "./commands/auth/logout.js"
9
9
  import {whoAmI} from "./commands/auth/aboutMe.js"
10
10
  import {wakeUp} from "./commands/ai/wakeUp.js"
11
11
  import {setkey} from "./commands/config/setkey.js"
12
+ import {launch} from "./commands/General/openApp.js"
13
+ import {search} from "./commands/General/searchYoutube.js"
14
+ import {play} from "./commands/General/playSong.js"
12
15
 
13
16
  const main = async()=>{
14
17
 
@@ -23,6 +26,8 @@ const main = async()=>{
23
26
  console.log(chalk.yellow(" A CLI Based AI Tool \n"));
24
27
 
25
28
  const program = new Command("orbital");
29
+
30
+
26
31
 
27
32
  program.version("0.0.1").
28
33
  description("Orbital CLI - A CLI Based AI Tool").
@@ -30,7 +35,10 @@ const main = async()=>{
30
35
  addCommand(logout).
31
36
  addCommand(whoAmI).
32
37
  addCommand(wakeUp).
33
- addCommand(setkey)
38
+ addCommand(setkey).
39
+ addCommand(launch).
40
+ addCommand(search).
41
+ addCommand(play)
34
42
 
35
43
  program.action(()=>{
36
44
  program.help();
@@ -0,0 +1,40 @@
1
+ import { getStoredToken } from "./token.js";
2
+
3
+ const BASE_URL = process.env.API_URL || "https://smart-cli-based-agent.onrender.com";
4
+
5
+ export const getApiClient = async () => {
6
+ const token = await getStoredToken();
7
+ if (!token?.access_token) {
8
+ throw new Error("Not authenticated. Please run 'orbital login' first.");
9
+ }
10
+
11
+ const headers = {
12
+ "Authorization": `Bearer ${token.access_token}`,
13
+ "Content-Type": "application/json"
14
+ };
15
+
16
+ return {
17
+ get: async (endpoint) => {
18
+ const res = await fetch(`${BASE_URL}${endpoint}`, { headers });
19
+ if (!res.ok) {
20
+ let msg = res.statusText;
21
+ try { const errData = await res.json(); msg = errData.error || msg; } catch {}
22
+ throw new Error(`API Error: ${msg}`);
23
+ }
24
+ return res.json();
25
+ },
26
+ post: async (endpoint, data) => {
27
+ const res = await fetch(`${BASE_URL}${endpoint}`, {
28
+ method: "POST",
29
+ headers,
30
+ body: JSON.stringify(data)
31
+ });
32
+ if (!res.ok) {
33
+ let msg = res.statusText;
34
+ try { const errData = await res.json(); msg = errData.error || msg; } catch {}
35
+ throw new Error(`API Error: ${msg}`);
36
+ }
37
+ return res.json();
38
+ }
39
+ };
40
+ };