aryx-cli 1.0.0
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/bin/aryx +15 -0
- package/bin/aryx.cjs +15 -0
- package/package.json +42 -0
- package/src/components/CommandSuggestions.tsx +62 -0
- package/src/components/Header.tsx +77 -0
- package/src/components/HelpView.tsx +36 -0
- package/src/components/IAmAryx.tsx +51 -0
- package/src/components/Loader.tsx +57 -0
- package/src/components/LoginView.tsx +129 -0
- package/src/components/Message.tsx +65 -0
- package/src/components/ThemeSelector.tsx +92 -0
- package/src/components/UsageView.tsx +52 -0
- package/src/constants.ts +32 -0
- package/src/hooks/useChat.ts +306 -0
- package/src/index.tsx +18 -0
- package/src/screens/ChatScreen.tsx +277 -0
- package/src/screens/SetupScreen.tsx +85 -0
- package/src/services/ai.ts +187 -0
- package/src/services/auth.ts +100 -0
- package/src/services/config.ts +30 -0
- package/src/services/fileTools.ts +257 -0
- package/src/services/firestoreRest.ts +119 -0
- package/src/services/loginServer.ts +91 -0
- package/src/services/platform.ts +26 -0
- package/src/theme.ts +46 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { UsageData } from './ai.js';
|
|
2
|
+
import { FIREBASE_PROJECT_ID } from '../constants.js';
|
|
3
|
+
|
|
4
|
+
const BASE = `https://firestore.googleapis.com/v1/projects/${FIREBASE_PROJECT_ID}/databases/(default)/documents`;
|
|
5
|
+
|
|
6
|
+
type FVal = string | number | boolean | Date;
|
|
7
|
+
|
|
8
|
+
const toValue = (v: FVal): object => {
|
|
9
|
+
if (v instanceof Date) return { timestampValue: v.toISOString() };
|
|
10
|
+
if (typeof v === 'string') return { stringValue: v };
|
|
11
|
+
if (typeof v === 'boolean') return { booleanValue: v };
|
|
12
|
+
if (Number.isInteger(v)) return { integerValue: String(v) };
|
|
13
|
+
|
|
14
|
+
return { doubleValue: v };
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const patch = async (
|
|
18
|
+
path: string,
|
|
19
|
+
fields: Record<string, FVal>,
|
|
20
|
+
token: string
|
|
21
|
+
): Promise<void> => {
|
|
22
|
+
const mask = Object.keys(fields)
|
|
23
|
+
.map(k => `updateMask.fieldPaths=${k}`)
|
|
24
|
+
.join('&');
|
|
25
|
+
|
|
26
|
+
const fieldsEntries = Object.entries(fields)
|
|
27
|
+
.map(([k, v]) => [k, toValue(v)]);
|
|
28
|
+
|
|
29
|
+
await fetch(`${BASE}/${path}?${mask}`, {
|
|
30
|
+
method: 'PATCH',
|
|
31
|
+
headers: {
|
|
32
|
+
Authorization: `Bearer ${token}`,
|
|
33
|
+
'Content-Type': 'application/json'
|
|
34
|
+
},
|
|
35
|
+
body: JSON.stringify({
|
|
36
|
+
fields: Object.fromEntries(fieldsEntries)
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type SessionTotals = {
|
|
42
|
+
inputTokens: number;
|
|
43
|
+
outputTokens: number;
|
|
44
|
+
totalTokens: number;
|
|
45
|
+
cost: number;
|
|
46
|
+
totalResponseMs: number;
|
|
47
|
+
messagesCount: number;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type UserPlan = {
|
|
51
|
+
planMessages: number;
|
|
52
|
+
messageCount: number;
|
|
53
|
+
subscriptionPlan: string
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const getUserPlan = async (uid: string, token: string): Promise<UserPlan | null> => {
|
|
57
|
+
const res = await fetch(`${BASE}/users/${uid}`, {
|
|
58
|
+
headers: {
|
|
59
|
+
Authorization: `Bearer ${token}`
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (!res.ok) return null;
|
|
64
|
+
|
|
65
|
+
const data = await res.json() as {
|
|
66
|
+
fields?: Record<string, { integerValue?: string; stringValue?: string }>
|
|
67
|
+
};
|
|
68
|
+
const f = data.fields ?? {};
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
planMessages: Number(f.planMessages?.integerValue ?? 0),
|
|
72
|
+
messageCount: Number(f.messageCount?.integerValue ?? 0),
|
|
73
|
+
subscriptionPlan: f.subscriptionPlan?.stringValue ?? 'free',
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const incrementMessageCount = (uid: string, count: number, token: string): Promise<void> =>
|
|
78
|
+
patch(`users/${uid}`, { messageCount: count }, token);
|
|
79
|
+
|
|
80
|
+
export const saveMessageToFirestore = async (
|
|
81
|
+
uid: string,
|
|
82
|
+
sessionId: string,
|
|
83
|
+
sessionCreatedAt: Date,
|
|
84
|
+
messageId: string,
|
|
85
|
+
prompt: string,
|
|
86
|
+
response: string,
|
|
87
|
+
usage: UsageData,
|
|
88
|
+
totals: SessionTotals,
|
|
89
|
+
token: string
|
|
90
|
+
): Promise<void> => {
|
|
91
|
+
const now = new Date();
|
|
92
|
+
|
|
93
|
+
await Promise.all([
|
|
94
|
+
patch(`users/${uid}/sessions/${sessionId}`, {
|
|
95
|
+
createdAt: sessionCreatedAt,
|
|
96
|
+
lastUpdated: now,
|
|
97
|
+
model: usage.model,
|
|
98
|
+
totalInputTokens: totals.inputTokens,
|
|
99
|
+
totalOutputTokens: totals.outputTokens,
|
|
100
|
+
totalTokens: totals.totalTokens,
|
|
101
|
+
totalCost: totals.cost,
|
|
102
|
+
totalResponseMs: totals.totalResponseMs,
|
|
103
|
+
messagesCount: totals.messagesCount,
|
|
104
|
+
}, token),
|
|
105
|
+
|
|
106
|
+
patch(`users/${uid}/sessions/${sessionId}/messages/${messageId}`, {
|
|
107
|
+
createdAt: now,
|
|
108
|
+
prompt,
|
|
109
|
+
response,
|
|
110
|
+
inputTokens: usage.inputTokens,
|
|
111
|
+
outputTokens: usage.outputTokens,
|
|
112
|
+
totalTokens: usage.totalTokens,
|
|
113
|
+
cost: usage.cost,
|
|
114
|
+
model: usage.model,
|
|
115
|
+
responseTimeMs: usage.responseTimeMs,
|
|
116
|
+
}, token),
|
|
117
|
+
]);
|
|
118
|
+
};
|
|
119
|
+
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createServer, IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import { URL } from 'node:url';
|
|
3
|
+
import { WEB_URL } from '../constants.js';
|
|
4
|
+
|
|
5
|
+
export type CallbackData = {
|
|
6
|
+
uid: string;
|
|
7
|
+
email: string;
|
|
8
|
+
displayName: string;
|
|
9
|
+
photoURL: string;
|
|
10
|
+
idToken: string;
|
|
11
|
+
refreshToken: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export async function startCallbackServer(): Promise<{
|
|
15
|
+
port: number;
|
|
16
|
+
waitForCallback: Promise<CallbackData>;
|
|
17
|
+
}> {
|
|
18
|
+
let resolveCallback: (data: CallbackData) => void;
|
|
19
|
+
let rejectCallback: (err: Error) => void;
|
|
20
|
+
|
|
21
|
+
const waitForCallback = new Promise<CallbackData>((resolve, reject) => {
|
|
22
|
+
resolveCallback = resolve;
|
|
23
|
+
rejectCallback = reject;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
27
|
+
if (!req.url) return;
|
|
28
|
+
|
|
29
|
+
const parsed = new URL(req.url, 'http://localhost');
|
|
30
|
+
|
|
31
|
+
if (parsed.pathname !== '/callback') {
|
|
32
|
+
res.writeHead(404);
|
|
33
|
+
res.end();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const uid = parsed.searchParams.get('uid');
|
|
38
|
+
const email = parsed.searchParams.get('email');
|
|
39
|
+
const displayName = parsed.searchParams.get('displayName') ?? '';
|
|
40
|
+
const photoURL = parsed.searchParams.get('photoURL') ?? '';
|
|
41
|
+
const idToken = parsed.searchParams.get('idToken');
|
|
42
|
+
const refreshToken = parsed.searchParams.get('refreshToken');
|
|
43
|
+
|
|
44
|
+
if (!uid || !email || !idToken || !refreshToken) {
|
|
45
|
+
res.writeHead(302, {
|
|
46
|
+
Location: `${WEB_URL}/auth/error`
|
|
47
|
+
});
|
|
48
|
+
res.end();
|
|
49
|
+
|
|
50
|
+
rejectCallback(new Error('Missing required params in callback'));
|
|
51
|
+
server.close();
|
|
52
|
+
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
res.writeHead(302, {
|
|
57
|
+
Location: `${WEB_URL}/auth/success`
|
|
58
|
+
});
|
|
59
|
+
res.end();
|
|
60
|
+
|
|
61
|
+
server.close();
|
|
62
|
+
resolveCallback({
|
|
63
|
+
uid,
|
|
64
|
+
email,
|
|
65
|
+
displayName,
|
|
66
|
+
photoURL,
|
|
67
|
+
idToken,
|
|
68
|
+
refreshToken
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await new Promise<void>((resolve) => {
|
|
73
|
+
server.listen(0, '127.0.0.1', resolve);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const addr = server.address() as { port: number };
|
|
77
|
+
const port = addr.port;
|
|
78
|
+
|
|
79
|
+
// 5-minute timeout
|
|
80
|
+
const timeout = setTimeout(() => {
|
|
81
|
+
server.close();
|
|
82
|
+
rejectCallback(new Error('Login timed out. Please try again.'));
|
|
83
|
+
}, 5 * 60 * 1000);
|
|
84
|
+
|
|
85
|
+
const waitWithCleanup = waitForCallback.finally(() => {
|
|
86
|
+
clearTimeout(timeout);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return { port, waitForCallback: waitWithCleanup };
|
|
90
|
+
}
|
|
91
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { exec } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
/** Open a URL in the user's default browser (cross-platform). */
|
|
4
|
+
export const openBrowser = (url: string): void => {
|
|
5
|
+
const cmd =
|
|
6
|
+
process.platform === 'win32'
|
|
7
|
+
? `start "" "${url}"`
|
|
8
|
+
: process.platform === 'darwin'
|
|
9
|
+
? `open "${url}"`
|
|
10
|
+
: `xdg-open "${url}"`;
|
|
11
|
+
|
|
12
|
+
exec(cmd);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Copy text to the system clipboard (cross-platform). */
|
|
16
|
+
export const copyToClipboard = (text: string): void => {
|
|
17
|
+
const cmd =
|
|
18
|
+
process.platform === 'win32'
|
|
19
|
+
? `powershell -command "Set-Clipboard '${text}'"`
|
|
20
|
+
: process.platform === 'darwin'
|
|
21
|
+
? `printf '%s' "${text}" | pbcopy`
|
|
22
|
+
: `printf '%s' "${text}" | xclip -selection clipboard`;
|
|
23
|
+
|
|
24
|
+
exec(cmd);
|
|
25
|
+
};
|
|
26
|
+
|
package/src/theme.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readConfig, writeConfig } from './services/config.js';
|
|
2
|
+
|
|
3
|
+
// ─── Config Persistence ──────────────────────────────────────────────────────
|
|
4
|
+
const load = (): 1 | 2 | 3 | 4 => (readConfig().diffTheme as 1 | 2 | 3 | 4) ?? 1;
|
|
5
|
+
|
|
6
|
+
export const saveTheme = (n: 1 | 2 | 3 | 4) => writeConfig({ ...readConfig(), diffTheme: n });
|
|
7
|
+
|
|
8
|
+
export const isSetupDone = (): boolean => readConfig().setupDone === true;
|
|
9
|
+
export const markSetupDone = () => writeConfig({ ...readConfig(), setupDone: true });
|
|
10
|
+
|
|
11
|
+
// ─── Diff Colors ─────────────────────────────────────────────────────────────
|
|
12
|
+
export const DIFF_COLORS = {
|
|
13
|
+
1: { add: '#07224b', rm: '#480c0b' }, // Modern (background)
|
|
14
|
+
2: { add: '#083418', rm: '#480c0b' }, // Classic (background)
|
|
15
|
+
3: { add: '#609ee9', rm: '#e34242' }, // Modern (text only)
|
|
16
|
+
4: { add: '#29af42', rm: '#e34242' }, // Classic (text only)
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
// ─── App Theme ───────────────────────────────────────────────────────────────
|
|
20
|
+
export const theme = {
|
|
21
|
+
colors: { primary: '#70b0ff', highlight: '#bcff92' },
|
|
22
|
+
diffTheme: load(),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// ─── Subscription Plan Colors ────────────────────────────────────────────────
|
|
26
|
+
export const PLAN_COLORS: Record<string, string> = {
|
|
27
|
+
free: '#c9c9c9',
|
|
28
|
+
pro: '#19c8b4',
|
|
29
|
+
max: '#ff9f43',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// ─── Message Colors ──────────────────────────────────────────────────────────
|
|
33
|
+
export const USER_MESSAGE_BG = '#3a3a3a';
|
|
34
|
+
|
|
35
|
+
// ─── Loader Shimmer Colors ──────────────────────────────────────────────────
|
|
36
|
+
export const SHIMMER_COLORS = {
|
|
37
|
+
bright: '#ebffeb',
|
|
38
|
+
mid: '#eaffea',
|
|
39
|
+
dim: '#d6ffd6',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// ─── Gradient Palettes ───────────────────────────────────────────────────────
|
|
43
|
+
/** Gradient used for brand text (IAmAryx, exit message). */
|
|
44
|
+
export const GRADIENT_BRAND = ['#ff6b6b', '#6bcb77', '#4d96ff', '#ff9f43', '#ffd93d', '#a29bfe'];
|
|
45
|
+
/** Gradient used for command suggestions (/aryx). */
|
|
46
|
+
export const GRADIENT_COMMAND = ['#ff6b6b', '#ff9f43', '#ffd93d', '#6bcb77', '#4d96ff', '#a29bfe'];
|