@bharatpanigrahi/onw-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.
@@ -0,0 +1,242 @@
1
+ import chalk from "chalk";
2
+ import boxen from "boxen";
3
+ import { text, isCancel, cancel, intro, outro, box } from "@clack/prompts";
4
+ import yoctoSpinner from "yocto-spinner";
5
+ import { marked } from "marked";
6
+ import { markedTerminal } from "marked-terminal";
7
+ import { ChatService } from "../../service/chat.service.js";
8
+ import { AIService } from "../ai/google-service.js";
9
+ import { getStoredToken } from "../../lib/token.js";
10
+ import prisma from "../../lib/db.js";
11
+
12
+ marked.use(
13
+ markedTerminal({
14
+ code: chalk.cyan,
15
+ blockquote: chalk.gray.italic,
16
+ heading: chalk.green.bold,
17
+ firstHeading: chalk.magenta.underline.bold,
18
+ hr: chalk.reset,
19
+ listitem: chalk.reset,
20
+ list: chalk.reset,
21
+ paragraph: chalk.reset,
22
+ strong: chalk.bold,
23
+ em: chalk.italic,
24
+ codespan: chalk.yellow.bgBlack,
25
+ del: chalk.dim.gray.strikethrough,
26
+ link: chalk.blue.underline,
27
+ href: chalk.blue.underline,
28
+ }),
29
+ );
30
+
31
+ const aiService = new AIService();
32
+ const chatService = new ChatService();
33
+
34
+ async function getUserFromToken() {
35
+ const token = await getStoredToken();
36
+ if (!token?.access_token) {
37
+ throw new Error("Not Authenticated Please run 'claude login' first.");
38
+ }
39
+
40
+ const spinner = yoctoSpinner({ text: "Authenticating..." }).start();
41
+ const user = await prisma.user.findFirst({
42
+ where: {
43
+ sessions: {
44
+ some: {
45
+ token: token?.access_token,
46
+ },
47
+ },
48
+ },
49
+ });
50
+ if (!user) {
51
+ spinner.error("User not found");
52
+ throw new Error("User not found please login again.");
53
+ }
54
+
55
+ spinner.success(`Welcome back, ${user.name}!`);
56
+ return user;
57
+ }
58
+
59
+ export function displayMessages(messages) {
60
+ messages.forEach((msg) => {
61
+ if (msg.role === "user") {
62
+ const userBox = boxen(chalk.white(msg.content), {
63
+ padding: 1,
64
+ margin: { left: 2, bottom: 1 },
65
+ borderStyle: "round",
66
+ borderColor: "blue",
67
+ titleAlignment: "left",
68
+ title: "šŸ‘¤ User",
69
+ });
70
+ console.log(userBox);
71
+ } else {
72
+ // render markdown content for AI messages
73
+ const renderedContent = marked.parse(msg.content);
74
+ const assistantBox = boxen(renderedContent.trim(), {
75
+ padding: 1,
76
+ margin: { left: 2, bottom: 1 },
77
+ borderStyle: "round",
78
+ borderColor: "green",
79
+ titleAlignment: "left",
80
+ title: "šŸ¤– Claude",
81
+ });
82
+ console.log(assistantBox);
83
+ }
84
+ });
85
+ }
86
+
87
+ export async function saveMessage(conversationId, role, content) {
88
+ return await chatService.addMessage(conversationId, role, content);
89
+ }
90
+
91
+ export async function getAiResponse(conversationId) {
92
+ const spinner = yoctoSpinner({
93
+ text: "Claude is thinking...",
94
+ color: "cyan",
95
+ }).start();
96
+ const dbMessages = await chatService.getMessages(conversationId);
97
+ const aiMessages = await chatService.formatMessagesForAI(dbMessages);
98
+
99
+ let fullResponse = "";
100
+ let isFirstChunk = true;
101
+
102
+ try {
103
+ const result = await aiService.sendMessage(aiMessages, (chunk) => {
104
+ if (isFirstChunk) {
105
+ console.log("\n");
106
+ const header = chalk.green.bold("šŸ¤– Claude:");
107
+ console.log(header);
108
+ console.log(chalk.gray("-".repeat(60)));
109
+ isFirstChunk = false;
110
+ }
111
+ fullResponse += chunk;
112
+ });
113
+ console.log("\n");
114
+ const renderedMarkdown = marked.parse(fullResponse);
115
+ console.log(renderedMarkdown);
116
+ console.log(chalk.gray("-".repeat(60)));
117
+ console.log("\n");
118
+ return result.content;
119
+ } catch (error) {
120
+ spinner.error("Failed to get AI response");
121
+ throw error;
122
+ } finally {
123
+ spinner.stop();
124
+ }
125
+ }
126
+
127
+ export async function updateConversationTitle(
128
+ conversationId,
129
+ userInput,
130
+ messageCount,
131
+ ) {
132
+ if (messageCount === 1) {
133
+ const title = userInput.slice(0, 50) + (userInput.length > 50 ? "..." : "");
134
+ await chatService.updateTitle(conversationId, title);
135
+ }
136
+ }
137
+
138
+ async function chatLoop(conversation) {
139
+ const helpBox = boxen(
140
+ `${chalk.gray("- Type your message and press Enter")}\n${chalk.gray("- Markdown formatting is supported in responses")}\n${chalk.gray('- Type "exit" to end the conversation')}\n${chalk.gray("- Press Ctrl+C to quit at any time")}`,
141
+ {
142
+ padding: 1,
143
+ margin: { bottom: 1 },
144
+ borderStyle: "round",
145
+ borderColor: "gray",
146
+ dimBorder: true,
147
+ },
148
+ );
149
+ console.log(helpBox);
150
+
151
+ while (true) {
152
+ const userInput = await text({
153
+ message: chalk.blue("You: "),
154
+ placeholder: "Type your message here...",
155
+ validate: (value) => {
156
+ if (!value || value.trim().length === 0) {
157
+ return "Message cannot be empty";
158
+ }
159
+ },
160
+ });
161
+
162
+ if (isCancel(userInput)) {
163
+ const exitBox = boxen(chalk.yellow("Chat session ended. Goodbye! šŸ‘‹"), {
164
+ padding: 1,
165
+ margin: 1,
166
+ borderStyle: "round",
167
+ borderColor: "yellow",
168
+ });
169
+ console.log(exitBox);
170
+ process.exit(0);
171
+ }
172
+
173
+ if (userInput.toLowerCase() === "exit") {
174
+ const exitBox = boxen(chalk.yellow("Chat session ended. Goodbye! šŸ‘‹"), {
175
+ padding: 1,
176
+ margin: 1,
177
+ borderStyle: "round",
178
+ borderColor: "yellow",
179
+ });
180
+ console.log(exitBox);
181
+ break;
182
+ }
183
+
184
+ await saveMessage(conversation.id, "user", userInput);
185
+ const messages = await chatService.getMessages(conversation.id);
186
+
187
+ const aiResponse = await getAiResponse(conversation.id);
188
+ await saveMessage(conversation.id, "assistant", aiResponse);
189
+
190
+ await updateConversationTitle(conversation.id, userInput, messages.length);
191
+ }
192
+ }
193
+
194
+ async function initConversation(userId, conversationId = null, mode = "chat") {
195
+ const spinner = yoctoSpinner({ text: "Loading conversation..." }).start();
196
+ const conversation = await chatService.getOrCreateConversations(
197
+ userId,
198
+ conversationId,
199
+ mode,
200
+ );
201
+ spinner.success("Conversation loaded");
202
+ const conversationInfo = boxen(
203
+ `${chalk.bold("Conversation")}: ${conversation.title}\n${chalk.gray("ID: " + conversation.id)}\n${chalk.gray("Mode: " + conversation.mode)}`,
204
+ {
205
+ padding: 1,
206
+ margin: { top: 1, bottom: 1 },
207
+ borderStyle: "round",
208
+ borderColor: "cyan",
209
+ title: "šŸ’­ Chat Session",
210
+ titleAlignment: "center",
211
+ },
212
+ );
213
+ console.log(conversationInfo);
214
+
215
+ if (conversation?.messages?.length > 0) {
216
+ console.log(chalk.yellow("šŸ“œ Previous messages:\n"));
217
+ displayMessages(conversation.messages);
218
+ }
219
+ return conversation;
220
+ }
221
+
222
+ export async function startChat(mode = "chat", conversationId = null) {
223
+ try {
224
+ intro(
225
+ boxen(chalk.bold.cyan("Claude AI Chat"), {
226
+ padding: 1,
227
+ borderStyle: "double",
228
+ borderColor: "cyan",
229
+ }),
230
+ );
231
+
232
+ const user = await getUserFromToken();
233
+ const conversation = await initConversation(user.id, conversationId, mode);
234
+ await chatLoop(conversation);
235
+
236
+ outro(
237
+ chalk.green(
238
+ 'Thanks for chatting! Type "claude wakeup" to start a new conversation.',
239
+ ),
240
+ );
241
+ } catch (error) {}
242
+ }
@@ -0,0 +1,83 @@
1
+ import chalk from "chalk";
2
+ import { Command } from "commander";
3
+ import yoctoSpinner from "yocto-spinner";
4
+ import { select } from "@clack/prompts";
5
+ import { getStoredToken } from "../../../lib/token.js";
6
+ import prisma from "../../../lib/db.js";
7
+ import { startChat } from "../../chat/chat-with-ai.js";
8
+ import { startToolChat } from "../../chat/chat-with-ai-tool.js";
9
+ import { startAgentChat } from "../../chat/chat-with-ai-agent.js";
10
+
11
+ const wakeUpAction = async () => {
12
+ const token = await getStoredToken();
13
+
14
+ if (!token?.access_token) {
15
+ console.log(chalk.red("Not Authenticated Please Login"));
16
+ return;
17
+ }
18
+
19
+ const spinner = yoctoSpinner({ text: "Fetching user information" });
20
+ spinner.start();
21
+
22
+ const user = await prisma.user.findFirst({
23
+ where: {
24
+ sessions: {
25
+ some: {
26
+ token: token?.access_token,
27
+ },
28
+ },
29
+ },
30
+ select: {
31
+ id: true,
32
+ name: true,
33
+ email: true,
34
+ image: true,
35
+ },
36
+ });
37
+
38
+ spinner.stop();
39
+
40
+ if (!user) {
41
+ console.log(chalk.red("User not found"));
42
+ return;
43
+ }
44
+
45
+ console.log(chalk.green(`Welcome back, ${user.name}!\n`));
46
+
47
+ const choice = await select({
48
+ message: "Select an Option",
49
+ options: [
50
+ {
51
+ value: "chat",
52
+ label: "Chat",
53
+ hint: "Simple chat with AI",
54
+ },
55
+ {
56
+ value: "tool",
57
+ label: "Tool Calling",
58
+ hint: "Chat with tools (Google Search, Code Execution)",
59
+ },
60
+ {
61
+ value: "agent",
62
+ label: "Agentic Mode",
63
+ hint: "Advanced AI agent (Coming soon)",
64
+ },
65
+ ],
66
+ });
67
+
68
+ switch (choice) {
69
+ case "chat":
70
+ await startChat("chat");
71
+ break;
72
+ case "tool":
73
+ await startToolChat();
74
+ break;
75
+ case "agent":
76
+ await startAgentChat();
77
+ break;
78
+ }
79
+ };
80
+
81
+ export const wakeUp = new Command("wakeup")
82
+ .description("Wake up the ai")
83
+ .action(wakeUpAction);
@@ -0,0 +1,287 @@
1
+ import { cancel, confirm, intro, isCancel, outro } from "@clack/prompts";
2
+ import { logger } from "better-auth";
3
+ import { createAuthClient } from "better-auth/client";
4
+ import { deviceAuthorizationClient } from "better-auth/client/plugins";
5
+
6
+ import chalk from "chalk";
7
+ import { Command } from "commander";
8
+ import fs from "node:fs/promises";
9
+ import open from "open";
10
+ import os from "os";
11
+ import path from "path";
12
+ import yoctoSpinner from "yocto-spinner";
13
+ import * as z from "zod/v4";
14
+ import prisma from "../../../lib/db.js";
15
+ import dotenv from "dotenv";
16
+ import {
17
+ clearStoredToken,
18
+ getStoredToken,
19
+ isTokenExpired,
20
+ requireAuth,
21
+ storeToken,
22
+ } from "../../../lib/token.js";
23
+
24
+ dotenv.config();
25
+
26
+ const URL = process.env.BASE_URL;
27
+ const CLIENT_ID = process.env.GITHUB_CLIENT_ID;
28
+ export const CONFIG_DIR = path.join(os.homedir(), ".better-auth");
29
+ export const TOKEN_FILE = path.join(CONFIG_DIR, "token.json");
30
+
31
+ export async function loginAction(opts) {
32
+ const options = z.object({
33
+ serverUrl: z.string().optional(),
34
+ clientId: z.string().optional(),
35
+ });
36
+
37
+ const serverUrl = options.serverUrl || URL;
38
+ const clientId = options.clientId || CLIENT_ID;
39
+
40
+ intro(chalk.bold("šŸ” Auth CLI Login"));
41
+
42
+ // TODO: CHANGE THIS WITH TOKEN MANAGEMENT UTILS
43
+ const existingToken = await getStoredToken();
44
+ const expired = await isTokenExpired();
45
+
46
+ if (existingToken && !expired) {
47
+ const shouldReAuth = await confirm({
48
+ message: "You are already logged in. Do you want to log in again?",
49
+ initialValue: false,
50
+ });
51
+
52
+ if (isCancel(shouldReAuth) || !shouldReAuth) {
53
+ cancel("Login cancelled.");
54
+ process.exit(0);
55
+ }
56
+ }
57
+
58
+ const authClient = createAuthClient({
59
+ baseURL: serverUrl,
60
+ plugins: [deviceAuthorizationClient()],
61
+ });
62
+
63
+ const spinner = yoctoSpinner({ text: "Requesting device authorization..." });
64
+ spinner.start();
65
+
66
+ try {
67
+ const { data, error } = await authClient.device.code({
68
+ client_id: clientId,
69
+ scope: "openid profile email",
70
+ });
71
+ spinner.stop();
72
+
73
+ if (error || !data) {
74
+ logger.error(
75
+ `Failed to request device authorization: ${error?.error_description}`,
76
+ );
77
+ process.exit(1);
78
+ }
79
+
80
+ const {
81
+ device_code,
82
+ user_code,
83
+ verification_uri,
84
+ expires_in,
85
+ interval = 5,
86
+ verification_uri_complete,
87
+ } = data;
88
+
89
+ console.log(chalk.cyan("Device Authorization Required!"));
90
+ console.log(
91
+ `Please visit ${chalk.underline.blue(verification_uri || verification_uri_complete)}`,
92
+ );
93
+ console.log(`Enter Code: ${chalk.bold.green(user_code)}`);
94
+
95
+ const shouldOpen = await confirm({
96
+ message: "Open browser automatically",
97
+ initialValue: true,
98
+ });
99
+
100
+ if (!isCancel(shouldOpen) && shouldOpen) {
101
+ const urlToOpen = verification_uri_complete || verification_uri;
102
+ await open(urlToOpen);
103
+ }
104
+
105
+ console.log(
106
+ chalk.gray(
107
+ `Waiting for authorization... (expires in ${Math.floor(expires_in / 60)} minutes)...`,
108
+ ),
109
+ );
110
+
111
+ const token = await pollForToken(
112
+ authClient,
113
+ device_code,
114
+ clientId,
115
+ interval,
116
+ );
117
+
118
+ if (token) {
119
+ const saved = await storeToken(token);
120
+ if (!saved) {
121
+ console.log(
122
+ chalk.yellow("\n āš ļø Warning: Could not save authentication token."),
123
+ );
124
+ console.log(chalk.yellow("You may need to login again on next use."));
125
+ }
126
+
127
+ // TODO: get the user Data
128
+
129
+ outro(chalk.green("Login successful!"));
130
+ console.log(chalk.gray(`\n Token saved to: ${TOKEN_FILE}`));
131
+ console.log(
132
+ chalk.gray("You can now use AI commands without loggin in again. \n"),
133
+ );
134
+ }
135
+ } catch (error) {
136
+ spinner.stop();
137
+ console.error(chalk.red("\nLogin failed: "), error?.message);
138
+ process.exit(1);
139
+ }
140
+ }
141
+
142
+ async function pollForToken(
143
+ authClient,
144
+ deviceCode,
145
+ clientId,
146
+ initialIntervalValue,
147
+ ) {
148
+ let pollingInterval = initialIntervalValue;
149
+ const spinner = yoctoSpinner({ text: "", color: "cyan" });
150
+
151
+ let dots = 0;
152
+
153
+ return new Promise((resolve, reject) => {
154
+ const poll = async () => {
155
+ dots = (dots + 1) % 4;
156
+ spinner.text = chalk.gray(
157
+ `Polling for authorization${".".repeat(dots)}${" ".repeat(3 - dots)}`,
158
+ );
159
+
160
+ if (!spinner.isSpinning) spinner.start();
161
+
162
+ try {
163
+ const { data, error } = await authClient.device.token({
164
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
165
+ device_code: deviceCode,
166
+ client_id: clientId,
167
+ fetchOptions: {
168
+ headers: {
169
+ "user-agent": `My CLI`,
170
+ },
171
+ },
172
+ });
173
+
174
+ if (data?.access_token) {
175
+ console.log(
176
+ chalk.bold.yellow(`Your access token: ${data.access_token}`),
177
+ );
178
+ spinner.stop();
179
+ resolve(data);
180
+ return;
181
+ } else if (error) {
182
+ switch (error.error) {
183
+ case "authorization_pending":
184
+ // Continue polling
185
+ break;
186
+ case "slow_down":
187
+ pollingInterval += 5;
188
+ break;
189
+ case "access_denied":
190
+ console.error("Access was denied by the user");
191
+ return;
192
+ case "expired_token":
193
+ console.error("The device code has expired. Please try again.");
194
+ return;
195
+ default:
196
+ spinner.stop();
197
+ logger.error(`Error: ${error.error_description}`);
198
+ process.exit(1);
199
+ }
200
+ }
201
+ } catch (error) {
202
+ spinner.stop();
203
+ logger.error(`Netowork Error: ${error.message}`);
204
+ process.exit(1);
205
+ }
206
+
207
+ setTimeout(poll, pollingInterval * 1000);
208
+ };
209
+
210
+ setTimeout(poll, pollingInterval * 1000);
211
+ });
212
+ }
213
+
214
+ export async function logoutAction() {
215
+ intro(chalk.bold("šŸ‘‹ Logout"));
216
+
217
+ const token = await getStoredToken();
218
+ if (!token) {
219
+ console.log(chalk.yellow("You are not logged in."));
220
+ process.exit(0);
221
+ }
222
+
223
+ const shouldLogout = await confirm({
224
+ message: "Are you sure you want to log out?",
225
+ initialValue: false,
226
+ });
227
+
228
+ if (isCancel(shouldLogout) || !shouldLogout) {
229
+ cancel("Logout cancelled.");
230
+ process.exit(0);
231
+ }
232
+
233
+ const cleared = await clearStoredToken();
234
+
235
+ if (cleared) {
236
+ outro(chalk.green("Successfully logged out!"));
237
+ } else {
238
+ console.log(
239
+ chalk.yellow("āš ļø Warning: Could not clear authentication token."),
240
+ );
241
+ }
242
+ }
243
+
244
+ export async function whoamiAction(opts) {
245
+ const token = await requireAuth();
246
+ if (!token.access_token) {
247
+ console.log("No access token found. Please login.");
248
+ process.exit(1);
249
+ }
250
+
251
+ const user = await prisma.user.findFirst({
252
+ where: {
253
+ sessions: {
254
+ some: {
255
+ token: token.access_token,
256
+ },
257
+ },
258
+ },
259
+ select: {
260
+ id: true,
261
+ name: true,
262
+ email: true,
263
+ image: true,
264
+ },
265
+ });
266
+
267
+ console.log(
268
+ chalk.bold.greenBright(
269
+ `\nšŸ‘¤ User: ${user.name} Email: ${user.email} ID: ${user.id}`,
270
+ ),
271
+ );
272
+ }
273
+
274
+ export const login = new Command("login")
275
+ .description("Login to your account")
276
+ .option("--server-url <url>", "URL of the authentication server")
277
+ .option("--client-id <id>", "Client ID for authentication")
278
+ .action(loginAction);
279
+
280
+ export const logout = new Command("logout")
281
+ .description("Logout of your account")
282
+ .action(logoutAction);
283
+
284
+ export const whoami = new Command("whoami")
285
+ .description("Display information about the current user")
286
+ .option("--server-url <url>", "URL of the authentication server")
287
+ .action(whoamiAction);
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+
3
+ import dotenv from "dotenv";
4
+ import chalk from "chalk";
5
+ import figlet from "figlet";
6
+ import { Command } from "commander";
7
+ import { login, logout, whoami } from "./commands/auth/login.js";
8
+ import { wakeUp } from "./commands/ai/wakeUp.js";
9
+
10
+ dotenv.config();
11
+
12
+ async function main() {
13
+ // Display banner
14
+ console.log(
15
+ chalk.cyan(
16
+ figlet.textSync("Claude CLI", {
17
+ horizontalLayout: "default",
18
+ font: "Standard",
19
+ }),
20
+ ),
21
+ );
22
+
23
+ console.log(chalk.gray("A CLI based AI Tool \n"));
24
+
25
+ const program = new Command("claude-cli");
26
+
27
+ program
28
+ .version("0.0.1")
29
+ .description("A CLI based AI Tool")
30
+ .addCommand(login)
31
+ .addCommand(logout)
32
+ .addCommand(whoami)
33
+ .addCommand(wakeUp);
34
+
35
+ program.action(() => {
36
+ program.help();
37
+ });
38
+
39
+ program.parse();
40
+ }
41
+
42
+ main().catch((error) => {
43
+ console.error(chalk.red("Error running Claude CLI:"), error);
44
+ process.exit(1);
45
+ });