gemix 0.0.1 → 0.1.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/README.md +101 -0
- package/dist/auth/oauth.d.ts +13 -0
- package/dist/auth/oauth.d.ts.map +1 -1
- package/dist/auth/oauth.js +165 -140
- package/dist/auth/oauth.js.map +1 -1
- package/dist/commands/current.js +2 -2
- package/dist/commands/current.js.map +1 -1
- package/dist/commands/dash.d.ts +2 -0
- package/dist/commands/dash.d.ts.map +1 -0
- package/dist/commands/dash.js +125 -0
- package/dist/commands/dash.js.map +1 -0
- package/dist/commands/list.js +1 -1
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/register.js +1 -1
- package/dist/commands/register.js.map +1 -1
- package/dist/commands/remove.js +1 -1
- package/dist/commands/remove.js.map +1 -1
- package/dist/commands/use.js +1 -1
- package/dist/commands/use.js.map +1 -1
- package/dist/index.js +16 -2
- package/dist/index.js.map +1 -1
- package/dist/server/public/app.js +181 -0
- package/dist/server/public/index.html +77 -0
- package/dist/server/public/style.css +132 -0
- package/dist/server/quota.d.ts +22 -0
- package/dist/server/quota.d.ts.map +1 -0
- package/dist/server/quota.js +69 -0
- package/dist/server/quota.js.map +1 -0
- package/dist/server/server.d.ts +11 -0
- package/dist/server/server.d.ts.map +1 -0
- package/dist/server/server.js +170 -0
- package/dist/server/server.js.map +1 -0
- package/dist/storage/paths.js +1 -1
- package/dist/storage/paths.js.map +1 -1
- package/package.json +20 -9
- package/doc/README.md +0 -90
- package/src/auth/oauth.ts +0 -231
- package/src/commands/current.ts +0 -18
- package/src/commands/list.ts +0 -20
- package/src/commands/register.ts +0 -53
- package/src/commands/remove.ts +0 -37
- package/src/commands/use.ts +0 -25
- package/src/index.ts +0 -59
- package/src/storage/accounts.ts +0 -285
- package/src/storage/paths.ts +0 -56
- package/src/utils/table.ts +0 -58
- package/src/utils/validation.ts +0 -27
- package/tsconfig.json +0 -20
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/* Square corners, roomy spacing. Colors = Gemini app's Material 3 palette, dark + light. */
|
|
2
|
+
:root, :root[data-theme="dark"] {
|
|
3
|
+
color-scheme: dark;
|
|
4
|
+
--background: #131314; --card: #1b1c1d; --panel: #232425;
|
|
5
|
+
--foreground: #e3e3e3; --muted-foreground: #a8abae;
|
|
6
|
+
--border: #2f3133; --border-strong: #444746; --track: #303234;
|
|
7
|
+
--primary: #a8c7fa; --primary-hover: #c2d7fb; --on-primary: #062e6f;
|
|
8
|
+
--destructive: #f2b8b5; --ok: #6dd58c; --warn: #fdd663;
|
|
9
|
+
--gemini: linear-gradient(90deg, #4285f4, #9b72cb, #d96570);
|
|
10
|
+
--mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
11
|
+
}
|
|
12
|
+
:root[data-theme="light"] {
|
|
13
|
+
color-scheme: light;
|
|
14
|
+
--background: #f8fafd; --card: #ffffff; --panel: #f0f4f9;
|
|
15
|
+
--foreground: #1f1f1f; --muted-foreground: #5f6368;
|
|
16
|
+
--border: #e1e5ea; --border-strong: #c4c7c5; --track: #e3e8ef;
|
|
17
|
+
--primary: #0b57d0; --primary-hover: #0842a0; --on-primary: #ffffff;
|
|
18
|
+
--destructive: #b3261e; --ok: #146c2e; --warn: #8a5a00;
|
|
19
|
+
}
|
|
20
|
+
@media (prefers-color-scheme: light) {
|
|
21
|
+
:root:not([data-theme]) {
|
|
22
|
+
color-scheme: light;
|
|
23
|
+
--background: #f8fafd; --card: #ffffff; --panel: #f0f4f9;
|
|
24
|
+
--foreground: #1f1f1f; --muted-foreground: #5f6368;
|
|
25
|
+
--border: #e1e5ea; --border-strong: #c4c7c5; --track: #e3e8ef;
|
|
26
|
+
--primary: #0b57d0; --primary-hover: #0842a0; --on-primary: #ffffff;
|
|
27
|
+
--destructive: #b3261e; --ok: #146c2e; --warn: #8a5a00;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
* { box-sizing: border-box; border-radius: 0; }
|
|
31
|
+
body { margin: 0; background: var(--background); color: var(--foreground);
|
|
32
|
+
font: 14px/1.55 Inter, system-ui, -apple-system, sans-serif; -webkit-font-smoothing: antialiased; }
|
|
33
|
+
.wrap { max-width: 1240px; margin: 0 auto; padding: 0 32px; }
|
|
34
|
+
.muted { color: var(--muted-foreground); }
|
|
35
|
+
.mono { font-family: var(--mono); font-size: 12px; }
|
|
36
|
+
.hidden { display: none; }
|
|
37
|
+
code { font-family: var(--mono); font-size: 12px; padding: 1px 6px; background: var(--panel); border: 1px solid var(--border); }
|
|
38
|
+
|
|
39
|
+
.topbar { border-bottom: 1px solid var(--border); }
|
|
40
|
+
.topbar .wrap { display: flex; justify-content: space-between; align-items: center; height: 60px; }
|
|
41
|
+
.wordmark { display: flex; align-items: center; gap: 10px; font: 700 15px var(--mono); letter-spacing: .18em; text-transform: uppercase; }
|
|
42
|
+
.wordmark::before { content: ""; width: 14px; height: 14px; background: var(--gemini); }
|
|
43
|
+
|
|
44
|
+
.pagehead { display: flex; justify-content: space-between; align-items: flex-end; gap: 24px; flex-wrap: wrap; padding: 48px 0 28px; }
|
|
45
|
+
.pagehead h1 { margin: 0 0 6px; font-size: 30px; font-weight: 650; letter-spacing: -.02em; }
|
|
46
|
+
.pagehead p { margin: 0; }
|
|
47
|
+
.actions { display: flex; align-items: center; gap: 10px; }
|
|
48
|
+
.actions .mono { margin-right: 8px; }
|
|
49
|
+
|
|
50
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); gap: 20px; padding-bottom: 64px; }
|
|
51
|
+
|
|
52
|
+
.card { position: relative; display: flex; flex-direction: column; background: var(--card); border: 1px solid var(--border);
|
|
53
|
+
transition: border-color .15s, box-shadow .15s; }
|
|
54
|
+
.card:hover { border-color: var(--border-strong); }
|
|
55
|
+
.card.active { border-color: color-mix(in srgb, var(--primary) 55%, transparent); box-shadow: 0 0 0 1px color-mix(in srgb, var(--primary) 25%, transparent); }
|
|
56
|
+
.card.active::before { content: ""; position: absolute; left: -1px; right: -1px; top: -1px; height: 3px; background: var(--gemini); }
|
|
57
|
+
.card-head { display: flex; gap: 14px; align-items: center; padding: 24px 24px 0; }
|
|
58
|
+
.avatar { width: 44px; height: 44px; flex: none; object-fit: cover; background: var(--panel); }
|
|
59
|
+
.who { min-width: 0; flex: 1; }
|
|
60
|
+
.who b { display: block; font-size: 16px; font-weight: 600; }
|
|
61
|
+
.who span { display: block; font-size: 13px; color: var(--muted-foreground); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
62
|
+
|
|
63
|
+
.badges { display: flex; gap: 6px; flex-wrap: wrap; padding: 14px 24px 0; }
|
|
64
|
+
.badge { font: 600 10px var(--mono); letter-spacing: .08em; text-transform: uppercase; padding: 4px 8px;
|
|
65
|
+
border: 1px solid var(--border-strong); color: var(--muted-foreground); }
|
|
66
|
+
.badge.primary { align-self: flex-start; background: var(--primary); border-color: var(--primary); color: var(--on-primary); }
|
|
67
|
+
.badge.ok { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 35%, transparent); background: color-mix(in srgb, var(--ok) 8%, transparent); }
|
|
68
|
+
.badge.warn { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 35%, transparent); background: color-mix(in srgb, var(--warn) 8%, transparent); }
|
|
69
|
+
.badge.bad { color: var(--destructive); border-color: color-mix(in srgb, var(--destructive) 35%, transparent); background: color-mix(in srgb, var(--destructive) 8%, transparent); }
|
|
70
|
+
|
|
71
|
+
.card-body { flex: 1; display: grid; grid-template-columns: 1fr 1fr; gap: 12px; padding: 20px 24px 24px; }
|
|
72
|
+
.card-body > .muted { grid-column: 1 / -1; padding: 14px; background: var(--panel); border: 1px solid var(--border); }
|
|
73
|
+
.model { display: grid; grid-template-columns: 1fr auto; align-items: baseline; row-gap: 10px;
|
|
74
|
+
padding: 14px; background: var(--panel); border: 1px solid var(--border); }
|
|
75
|
+
.lbl { font: 600 11px var(--mono); letter-spacing: .08em; text-transform: uppercase; color: var(--muted-foreground); }
|
|
76
|
+
.pct { font: 700 22px/1 var(--mono); }
|
|
77
|
+
.pct small { font-size: 11px; color: var(--muted-foreground); margin-left: 1px; }
|
|
78
|
+
.seg { grid-column: 1 / -1; display: grid; grid-template-columns: repeat(24, 1fr); gap: 2px; height: 8px; }
|
|
79
|
+
.seg i { background: var(--track); }
|
|
80
|
+
.reset { grid-column: 1 / -1; font-size: 12px; color: var(--muted-foreground); }
|
|
81
|
+
|
|
82
|
+
.card-foot { display: flex; align-items: center; gap: 6px; padding: 14px 24px; border-top: 1px solid var(--border); }
|
|
83
|
+
.card-foot .grow { flex: 1; }
|
|
84
|
+
.in-use { font: 600 11px var(--mono); letter-spacing: .08em; text-transform: uppercase; color: var(--primary); padding: 0 4px; }
|
|
85
|
+
|
|
86
|
+
.btn { height: 38px; padding: 0 16px; border: 1px solid var(--border-strong); background: transparent; color: var(--foreground);
|
|
87
|
+
font: 500 13px Inter, system-ui, sans-serif; cursor: pointer; transition: background .12s, border-color .12s, color .12s; }
|
|
88
|
+
.btn:hover:not(:disabled) { background: var(--panel); border-color: var(--muted-foreground); }
|
|
89
|
+
.btn:disabled { opacity: .4; cursor: not-allowed; }
|
|
90
|
+
.btn.default { background: var(--primary); border-color: var(--primary); color: var(--on-primary); }
|
|
91
|
+
.btn.default:hover:not(:disabled) { background: var(--primary-hover); border-color: var(--primary-hover); }
|
|
92
|
+
.btn.destructive { background: var(--destructive); border-color: var(--destructive); color: var(--on-primary); }
|
|
93
|
+
.btn.ghost { border-color: transparent; color: var(--muted-foreground); }
|
|
94
|
+
.btn.ghost:hover:not(:disabled) { color: var(--foreground); background: var(--panel); border-color: transparent; }
|
|
95
|
+
.btn.ghost.danger:hover:not(:disabled) { color: var(--destructive); }
|
|
96
|
+
.btn.sm { height: 34px; padding: 0 12px; }
|
|
97
|
+
.btn:focus-visible, .input:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
|
|
98
|
+
|
|
99
|
+
.skeleton { height: 290px; background: linear-gradient(90deg, var(--card), var(--panel), var(--card)); background-size: 200% 100%;
|
|
100
|
+
border: 1px solid var(--border); animation: sk 1.4s infinite; }
|
|
101
|
+
@keyframes sk { to { background-position: -200% 0; } }
|
|
102
|
+
.empty { grid-column: 1 / -1; text-align: center; padding: 80px 20px; color: var(--muted-foreground); border: 1px dashed var(--border-strong); }
|
|
103
|
+
|
|
104
|
+
.dialog { background: var(--card); color: var(--foreground); border: 1px solid var(--border-strong); padding: 28px; width: min(500px, 92vw); }
|
|
105
|
+
.dialog::backdrop { background: rgb(0 0 0 / .6); backdrop-filter: blur(2px); }
|
|
106
|
+
.dialog h2 { margin: 0 0 6px; font-size: 18px; font-weight: 600; }
|
|
107
|
+
.dialog > p { margin: 0; font-size: 13px; }
|
|
108
|
+
.label { display: block; margin: 18px 0 8px; font: 600 11px var(--mono); letter-spacing: .08em; text-transform: uppercase; color: var(--muted-foreground); }
|
|
109
|
+
.input { width: 100%; height: 40px; padding: 0 12px; border: 1px solid var(--border-strong); background: var(--background); color: var(--foreground); font: inherit; }
|
|
110
|
+
.input:focus { border-color: var(--primary); outline: none; }
|
|
111
|
+
.input.mono { font: 12px var(--mono); }
|
|
112
|
+
.row { display: flex; gap: 8px; } .row .input { flex: 1; min-width: 0; }
|
|
113
|
+
.footer { display: flex; justify-content: flex-end; gap: 8px; margin-top: 28px; }
|
|
114
|
+
|
|
115
|
+
.toasts { position: fixed; right: 24px; bottom: 24px; display: flex; flex-direction: column; gap: 8px; }
|
|
116
|
+
.toast { background: var(--panel); border: 1px solid var(--border-strong); border-left: 3px solid var(--primary);
|
|
117
|
+
padding: 12px 16px; max-width: 360px; font-size: 13px; }
|
|
118
|
+
.toast.err { border-left-color: var(--destructive); }
|
|
119
|
+
|
|
120
|
+
@media (max-width: 640px) {
|
|
121
|
+
.wrap { padding: 0 16px; }
|
|
122
|
+
.grid { grid-template-columns: 1fr; }
|
|
123
|
+
.card-body { grid-template-columns: 1fr; }
|
|
124
|
+
}
|
|
125
|
+
.btn.use { color: var(--primary); border-color: color-mix(in srgb, var(--primary) 45%, transparent); }
|
|
126
|
+
.btn.use:hover:not(:disabled) { background: var(--primary); border-color: var(--primary); color: var(--on-primary); }
|
|
127
|
+
.theme-toggle { display: flex; border: 1px solid var(--border-strong); }
|
|
128
|
+
.theme-toggle button { height: 30px; padding: 0 10px; border: 0; background: transparent; color: var(--muted-foreground);
|
|
129
|
+
font: 600 10px var(--mono); letter-spacing: .08em; text-transform: uppercase; cursor: pointer; }
|
|
130
|
+
.theme-toggle button + button { border-left: 1px solid var(--border-strong); }
|
|
131
|
+
.theme-toggle button[aria-pressed="true"] { background: var(--panel); color: var(--foreground); }
|
|
132
|
+
.topbar .right { display: flex; align-items: center; gap: 16px; }
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { TokenData } from '../storage/accounts.js';
|
|
2
|
+
export interface ModelQuota {
|
|
3
|
+
id: string;
|
|
4
|
+
remaining: number;
|
|
5
|
+
resetTime: string | null;
|
|
6
|
+
}
|
|
7
|
+
export type QuotaResult = {
|
|
8
|
+
status: 'ok';
|
|
9
|
+
tier: string | null;
|
|
10
|
+
tokenExpiry: number;
|
|
11
|
+
models: ModelQuota[];
|
|
12
|
+
} | {
|
|
13
|
+
status: 'relogin';
|
|
14
|
+
} | {
|
|
15
|
+
status: 'error';
|
|
16
|
+
error: string;
|
|
17
|
+
tokenExpiry: number;
|
|
18
|
+
};
|
|
19
|
+
/** Returns a usable access token, refreshing (and persisting) if near expiry. null = dead login. */
|
|
20
|
+
export declare function freshToken(username: string): Promise<TokenData | null>;
|
|
21
|
+
export declare function fetchQuota(username: string): Promise<QuotaResult>;
|
|
22
|
+
//# sourceMappingURL=quota.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quota.d.ts","sourceRoot":"","sources":["../../src/server/quota.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAmBxD,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,MAAM,WAAW,GACnB;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,UAAU,EAAE,CAAA;CAAE,GAChF;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC;AAgB5D,oGAAoG;AACpG,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAU5E;AAED,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CA8BvE"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetches per-model weekly quota info from Google's Cloud Code API.
|
|
3
|
+
* Same data source as `agy /usage` — weekly buckets via retrieveUserQuota.
|
|
4
|
+
*/
|
|
5
|
+
import { loadTokens, saveTokens } from '../storage/accounts.js';
|
|
6
|
+
import { refreshTokens } from '../auth/oauth.js';
|
|
7
|
+
const HOST = 'https://daily-cloudcode-pa.googleapis.com/v1internal:';
|
|
8
|
+
async function call(method, token, body) {
|
|
9
|
+
const r = await fetch(HOST + method, {
|
|
10
|
+
method: 'POST',
|
|
11
|
+
headers: {
|
|
12
|
+
Authorization: `Bearer ${token}`,
|
|
13
|
+
'Content-Type': 'application/json',
|
|
14
|
+
'User-Agent': 'antigravity',
|
|
15
|
+
},
|
|
16
|
+
body: JSON.stringify(body),
|
|
17
|
+
});
|
|
18
|
+
if (!r.ok)
|
|
19
|
+
throw new Error(`${method} ${r.status}`);
|
|
20
|
+
return r.json();
|
|
21
|
+
}
|
|
22
|
+
/** Returns a usable access token, refreshing (and persisting) if near expiry. null = dead login. */
|
|
23
|
+
export async function freshToken(username) {
|
|
24
|
+
let t = await loadTokens(username);
|
|
25
|
+
if (!t)
|
|
26
|
+
return null;
|
|
27
|
+
if (!t.expiry_date || t.expiry_date - Date.now() < 5 * 60 * 1000) {
|
|
28
|
+
const n = await refreshTokens(t);
|
|
29
|
+
if (!n)
|
|
30
|
+
return null;
|
|
31
|
+
t = n;
|
|
32
|
+
await saveTokens(username, t);
|
|
33
|
+
}
|
|
34
|
+
return t;
|
|
35
|
+
}
|
|
36
|
+
export async function fetchQuota(username) {
|
|
37
|
+
const t = await freshToken(username);
|
|
38
|
+
if (!t)
|
|
39
|
+
return { status: 'relogin' };
|
|
40
|
+
try {
|
|
41
|
+
const a = await call('loadCodeAssist', t.access_token, {
|
|
42
|
+
metadata: { ideType: 'ANTIGRAVITY', platform: 'PLATFORM_UNSPECIFIED', pluginType: 'GEMINI' },
|
|
43
|
+
});
|
|
44
|
+
const q = await call('retrieveUserQuota', t.access_token, {
|
|
45
|
+
project: a.cloudaicompanionProject,
|
|
46
|
+
});
|
|
47
|
+
const models = (q.buckets || [])
|
|
48
|
+
.filter((b) => /^(gemini|claude|gpt)/i.test(b.modelId)) // skip internal chat_*/tab_* buckets
|
|
49
|
+
.map((b) => ({
|
|
50
|
+
id: b.modelId,
|
|
51
|
+
remaining: b.remainingFraction ?? 0,
|
|
52
|
+
resetTime: b.resetTime || null,
|
|
53
|
+
}));
|
|
54
|
+
return {
|
|
55
|
+
status: 'ok',
|
|
56
|
+
tier: a.currentTier?.id || null,
|
|
57
|
+
tokenExpiry: t.expiry_date,
|
|
58
|
+
models,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
return {
|
|
63
|
+
status: 'error',
|
|
64
|
+
error: e instanceof Error ? e.message : String(e),
|
|
65
|
+
tokenExpiry: t.expiry_date,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=quota.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quota.js","sourceRoot":"","sources":["../../src/server/quota.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local HTTP server for the gemix dashboard.
|
|
3
|
+
* Serves static files from ./public/ and handles /api/* REST endpoints.
|
|
4
|
+
*
|
|
5
|
+
* Ported from gemix-dash/server.mjs — all lib/ imports replaced with
|
|
6
|
+
* gemix's own storage/auth/utils modules.
|
|
7
|
+
*/
|
|
8
|
+
import http from 'node:http';
|
|
9
|
+
export declare const PORT: number;
|
|
10
|
+
export declare const server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
|
|
11
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server/server.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,IAAI,MAAM,WAAW,CAAC;AAmB7B,eAAO,MAAM,IAAI,QAAmC,CAAC;AA4IrD,eAAO,MAAM,MAAM,sEAmBjB,CAAC"}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local HTTP server for the gemix dashboard.
|
|
3
|
+
* Serves static files from ./public/ and handles /api/* REST endpoints.
|
|
4
|
+
*
|
|
5
|
+
* Ported from gemix-dash/server.mjs — all lib/ imports replaced with
|
|
6
|
+
* gemix's own storage/auth/utils modules.
|
|
7
|
+
*/
|
|
8
|
+
import http from 'node:http';
|
|
9
|
+
import { promises as fs } from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import crypto from 'node:crypto';
|
|
13
|
+
import { loadAccounts, saveAccounts, addAccount, removeAccount, switchAccount, getActiveUsername, setActiveUsername, } from '../storage/accounts.js';
|
|
14
|
+
import { getAccountDir } from '../storage/paths.js';
|
|
15
|
+
import { buildAuthUrl, exchangeCode } from '../auth/oauth.js';
|
|
16
|
+
import { fetchQuota } from './quota.js';
|
|
17
|
+
import { validateUsername } from '../utils/validation.js';
|
|
18
|
+
export const PORT = Number(process.env.PORT || 4577);
|
|
19
|
+
const PUBLIC = path.join(path.dirname(fileURLToPath(import.meta.url)), 'public');
|
|
20
|
+
const MIME = {
|
|
21
|
+
'.html': 'text/html',
|
|
22
|
+
'.js': 'text/javascript',
|
|
23
|
+
'.css': 'text/css',
|
|
24
|
+
};
|
|
25
|
+
const CACHE_MS = 60_000;
|
|
26
|
+
const quotaCache = new Map();
|
|
27
|
+
const pending = new Map();
|
|
28
|
+
const send = (res, code, body) => {
|
|
29
|
+
res.writeHead(code, { 'Content-Type': 'application/json' });
|
|
30
|
+
res.end(JSON.stringify(body));
|
|
31
|
+
};
|
|
32
|
+
/** Block DNS-rebinding / cross-site calls: only loopback Host, Origin must match if sent. */
|
|
33
|
+
const hostOk = (req) => {
|
|
34
|
+
const h = (req.headers.host || '').replace(/:\d+$/, '');
|
|
35
|
+
if (h !== '127.0.0.1' && h !== 'localhost')
|
|
36
|
+
return false;
|
|
37
|
+
const o = req.headers.origin;
|
|
38
|
+
return !o || o === `http://${req.headers.host}`;
|
|
39
|
+
};
|
|
40
|
+
const readJson = async (req) => {
|
|
41
|
+
let s = '';
|
|
42
|
+
for await (const c of req) {
|
|
43
|
+
s += c;
|
|
44
|
+
if (s.length > 1e5)
|
|
45
|
+
throw new Error('too large');
|
|
46
|
+
}
|
|
47
|
+
return s ? JSON.parse(s) : {};
|
|
48
|
+
};
|
|
49
|
+
async function quotaFor(name, force) {
|
|
50
|
+
const hit = quotaCache.get(name);
|
|
51
|
+
if (!force && hit && Date.now() - hit.at < CACHE_MS)
|
|
52
|
+
return hit.data;
|
|
53
|
+
const data = await fetchQuota(name);
|
|
54
|
+
quotaCache.set(name, { at: Date.now(), data });
|
|
55
|
+
return data;
|
|
56
|
+
}
|
|
57
|
+
async function listAccounts(force) {
|
|
58
|
+
const { accounts } = await loadAccounts();
|
|
59
|
+
const active = (await getActiveUsername())?.trim() || null;
|
|
60
|
+
const out = await Promise.all(Object.entries(accounts).map(async ([username, a]) => ({
|
|
61
|
+
username,
|
|
62
|
+
email: a.email,
|
|
63
|
+
name: a.name,
|
|
64
|
+
picture: a.picture,
|
|
65
|
+
created: a.created,
|
|
66
|
+
lastUsed: a.lastUsed,
|
|
67
|
+
active: username === active,
|
|
68
|
+
quota: await quotaFor(username, force),
|
|
69
|
+
})));
|
|
70
|
+
return { active, accounts: out };
|
|
71
|
+
}
|
|
72
|
+
async function rename(oldName, newName) {
|
|
73
|
+
const v = validateUsername(newName);
|
|
74
|
+
if (!v.valid)
|
|
75
|
+
throw Object.assign(new Error(v.error), { code: 400 });
|
|
76
|
+
const data = await loadAccounts();
|
|
77
|
+
if (!data.accounts[oldName])
|
|
78
|
+
throw Object.assign(new Error('not found'), { code: 404 });
|
|
79
|
+
if (data.accounts[newName])
|
|
80
|
+
throw Object.assign(new Error('name taken'), { code: 409 });
|
|
81
|
+
await fs.rename(getAccountDir(oldName), getAccountDir(newName));
|
|
82
|
+
data.accounts[newName] = data.accounts[oldName];
|
|
83
|
+
delete data.accounts[oldName];
|
|
84
|
+
if (data.active === oldName)
|
|
85
|
+
data.active = newName;
|
|
86
|
+
await saveAccounts(data);
|
|
87
|
+
if ((await getActiveUsername())?.trim() === oldName)
|
|
88
|
+
await setActiveUsername(newName);
|
|
89
|
+
quotaCache.delete(oldName);
|
|
90
|
+
}
|
|
91
|
+
async function api(req, res, url) {
|
|
92
|
+
const m = req.method;
|
|
93
|
+
const seg = url.pathname.split('/').filter(Boolean); // ['api','accounts',name?,action?]
|
|
94
|
+
const name = seg[2] && decodeURIComponent(seg[2]);
|
|
95
|
+
if (m === 'GET' && url.pathname === '/api/accounts') {
|
|
96
|
+
return send(res, 200, await listAccounts(url.searchParams.get('refresh') === '1'));
|
|
97
|
+
}
|
|
98
|
+
if (m === 'POST' && url.pathname === '/api/auth-url') {
|
|
99
|
+
const { url: authUrl, codeVerifier } = buildAuthUrl();
|
|
100
|
+
const id = crypto.randomUUID();
|
|
101
|
+
// Expire sessions older than 10 minutes
|
|
102
|
+
for (const [k, v] of pending) {
|
|
103
|
+
if (Date.now() - v.at > 10 * 60_000)
|
|
104
|
+
pending.delete(k);
|
|
105
|
+
}
|
|
106
|
+
pending.set(id, { codeVerifier, at: Date.now() });
|
|
107
|
+
return send(res, 200, { id, url: authUrl });
|
|
108
|
+
}
|
|
109
|
+
if (m === 'POST' && url.pathname === '/api/accounts') {
|
|
110
|
+
const { name: n, code, id } = (await readJson(req));
|
|
111
|
+
if (!n)
|
|
112
|
+
return send(res, 400, { error: 'name required' });
|
|
113
|
+
const v = validateUsername(n);
|
|
114
|
+
if (!v.valid)
|
|
115
|
+
return send(res, 400, { error: v.error });
|
|
116
|
+
if ((await loadAccounts()).accounts[n]) {
|
|
117
|
+
return send(res, 409, { error: `'${n}' already exists` });
|
|
118
|
+
}
|
|
119
|
+
const sess = id ? pending.get(id) : undefined;
|
|
120
|
+
if (!sess || !code?.trim()) {
|
|
121
|
+
return send(res, 400, { error: 'Auth session expired, reopen the link' });
|
|
122
|
+
}
|
|
123
|
+
const r = await exchangeCode(code.trim(), sess.codeVerifier);
|
|
124
|
+
pending.delete(id);
|
|
125
|
+
await addAccount(n, r.email, r.name, r.picture, r.tokens);
|
|
126
|
+
return send(res, 200, { ok: true, email: r.email });
|
|
127
|
+
}
|
|
128
|
+
if (name && m === 'POST' && seg[3] === 'switch') {
|
|
129
|
+
return (await switchAccount(name))
|
|
130
|
+
? send(res, 200, { ok: true })
|
|
131
|
+
: send(res, 404, { error: 'not found or no tokens' });
|
|
132
|
+
}
|
|
133
|
+
if (name && m === 'DELETE' && !seg[3]) {
|
|
134
|
+
quotaCache.delete(name);
|
|
135
|
+
return (await removeAccount(name))
|
|
136
|
+
? send(res, 200, { ok: true })
|
|
137
|
+
: send(res, 404, { error: 'not found' });
|
|
138
|
+
}
|
|
139
|
+
if (name && m === 'PATCH' && !seg[3]) {
|
|
140
|
+
const body = (await readJson(req));
|
|
141
|
+
await rename(name, body.name ?? '');
|
|
142
|
+
return send(res, 200, { ok: true });
|
|
143
|
+
}
|
|
144
|
+
send(res, 404, { error: 'not found' });
|
|
145
|
+
}
|
|
146
|
+
export const server = http.createServer(async (req, res) => {
|
|
147
|
+
try {
|
|
148
|
+
if (!hostOk(req))
|
|
149
|
+
return send(res, 403, { error: 'forbidden' });
|
|
150
|
+
const url = new URL(req.url ?? '/', 'http://x');
|
|
151
|
+
if (url.pathname.startsWith('/api/'))
|
|
152
|
+
return await api(req, res, url);
|
|
153
|
+
// Serve static files from public/
|
|
154
|
+
const file = path.join(PUBLIC, url.pathname === '/' ? 'index.html' : url.pathname);
|
|
155
|
+
if (!file.startsWith(PUBLIC + path.sep))
|
|
156
|
+
return send(res, 403, { error: 'forbidden' });
|
|
157
|
+
const body = await fs.readFile(file).catch(() => null);
|
|
158
|
+
if (!body)
|
|
159
|
+
return send(res, 404, { error: 'not found' });
|
|
160
|
+
res.writeHead(200, { 'Content-Type': MIME[path.extname(file)] || 'application/octet-stream' });
|
|
161
|
+
res.end(body);
|
|
162
|
+
}
|
|
163
|
+
catch (e) {
|
|
164
|
+
const err = e;
|
|
165
|
+
send(res, err.code && err.code >= 400 && err.code < 500 ? err.code : 500, {
|
|
166
|
+
error: err.message,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/server/server.ts"],"names":[],"mappings":"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"}
|
package/dist/storage/paths.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as path from 'node:path';
|
|
2
2
|
import * as os from 'node:os';
|
|
3
3
|
import { promises as fs } from 'node:fs';
|
|
4
|
-
const GEMX_DIR = '.
|
|
4
|
+
const GEMX_DIR = '.gemix';
|
|
5
5
|
const GEMINI_DIR = '.gemini';
|
|
6
6
|
const ACCOUNTS_FILE = 'accounts.json';
|
|
7
7
|
const ACTIVE_FILE = 'active';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/storage/paths.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AAEzC,MAAM,QAAQ,GAAG,
|
|
1
|
+
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/storage/paths.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AAEzC,MAAM,QAAQ,GAAG,QAAQ,CAAC;AAC1B,MAAM,UAAU,GAAG,SAAS,CAAC;AAC7B,MAAM,aAAa,GAAG,eAAe,CAAC;AACtC,MAAM,WAAW,GAAG,QAAQ,CAAC;AAC7B,MAAM,YAAY,GAAG,UAAU,CAAC;AAChC,MAAM,gBAAgB,GAAG,kBAAkB,CAAC;AAC5C,MAAM,oBAAoB,GAAG,sBAAsB,CAAC;AAEpD,MAAM,UAAU,UAAU;IACxB,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,QAAQ,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,mBAAmB;IACjC,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,aAAa,CAAC,CAAC;AAChD,CAAC;AAED,MAAM,UAAU,iBAAiB;IAC/B,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,WAAW,CAAC,CAAC;AAC9C,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,YAAY,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,QAAgB;IAC5C,OAAO,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,EAAE,QAAQ,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,QAAgB;IACnD,OAAO,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,EAAE,aAAa,CAAC,CAAC;AAC3D,CAAC;AAED,MAAM,UAAU,uBAAuB;IACrC,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,UAAU,EAAE,gBAAgB,CAAC,CAAC;AAC/D,CAAC;AAED,MAAM,UAAU,2BAA2B;IACzC,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,UAAU,EAAE,oBAAoB,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,UAAU,4BAA4B;IAC1C,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,UAAU,EAAE,iBAAiB,EAAE,yBAAyB,CAAC,CAAC;AAC3F,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa;IACjC,MAAM,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAClD,MAAM,EAAE,CAAC,KAAK,CAAC,cAAc,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AACxD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,QAAgB;IACrD,MAAM,EAAE,CAAC,KAAK,CAAC,aAAa,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AAC/D,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,26 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gemix",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Gemini multi-account CLI - manage multiple Google accounts for Gemini API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
|
-
"bin":
|
|
8
|
-
|
|
7
|
+
"bin": "./dist/index.js",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=18.0.0"
|
|
9
13
|
},
|
|
10
14
|
"scripts": {
|
|
11
|
-
"build": "tsc",
|
|
15
|
+
"build": "tsc && node scripts/copy-public.mjs",
|
|
16
|
+
"postbuild": "node scripts/copy-public.mjs",
|
|
12
17
|
"dev": "tsx src/index.ts",
|
|
13
18
|
"start": "node dist/index.js",
|
|
14
|
-
"link": "npm run build && npm link"
|
|
19
|
+
"link": "npm run build && npm link",
|
|
20
|
+
"test": "node test/smoke.mjs"
|
|
15
21
|
},
|
|
16
|
-
"keywords": [
|
|
22
|
+
"keywords": [
|
|
23
|
+
"gemini",
|
|
24
|
+
"google",
|
|
25
|
+
"oauth",
|
|
26
|
+
"multi-account",
|
|
27
|
+
"cli"
|
|
28
|
+
],
|
|
17
29
|
"author": "",
|
|
18
30
|
"license": "MIT",
|
|
19
31
|
"dependencies": {
|
|
32
|
+
"cli-table3": "^0.6.5",
|
|
20
33
|
"commander": "^12.1.0",
|
|
21
|
-
"
|
|
22
|
-
"open": "^10.1.0",
|
|
23
|
-
"cli-table3": "^0.6.5"
|
|
34
|
+
"open": "^10.1.0"
|
|
24
35
|
},
|
|
25
36
|
"devDependencies": {
|
|
26
37
|
"@types/node": "^22.10.0",
|
package/doc/README.md
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
# gemx - Gemini Multi-Account CLI
|
|
2
|
-
|
|
3
|
-
Manage multiple Google accounts for Gemini API access from a single CLI tool.
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
### From Source
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
git clone <repo-url>
|
|
11
|
-
cd gemx
|
|
12
|
-
npm install
|
|
13
|
-
npm run build
|
|
14
|
-
npm link
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
### Requirements
|
|
18
|
-
|
|
19
|
-
- Node.js >= 18
|
|
20
|
-
- npm
|
|
21
|
-
|
|
22
|
-
## Quick Start
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
# Register your first account
|
|
26
|
-
gemx register personal
|
|
27
|
-
|
|
28
|
-
# Register another account
|
|
29
|
-
gemx register work
|
|
30
|
-
|
|
31
|
-
# List all accounts
|
|
32
|
-
gemx list
|
|
33
|
-
|
|
34
|
-
# Switch accounts
|
|
35
|
-
gemx use work
|
|
36
|
-
|
|
37
|
-
# Check active account
|
|
38
|
-
gemx current
|
|
39
|
-
|
|
40
|
-
# Remove an account
|
|
41
|
-
gemx remove personal
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
## Commands
|
|
45
|
-
|
|
46
|
-
| Command | Description |
|
|
47
|
-
|---------|-------------|
|
|
48
|
-
| `gemx register <username>` | Register a new Google account via OAuth |
|
|
49
|
-
| `gemx list` | List all registered accounts |
|
|
50
|
-
| `gemx list --json` | Output in JSON format |
|
|
51
|
-
| `gemx use <username>` | Switch active account |
|
|
52
|
-
| `gemx current` | Show active account |
|
|
53
|
-
| `gemx remove <username>` | Remove an account |
|
|
54
|
-
|
|
55
|
-
## How It Works
|
|
56
|
-
|
|
57
|
-
### Storage
|
|
58
|
-
|
|
59
|
-
- `~/.gemx/accounts.json` - Master registry of all accounts
|
|
60
|
-
- `~/.gemx/accounts/<username>/tokens.json` - Per-account OAuth tokens
|
|
61
|
-
- `~/.gemini/oauth_creds.json` - Gemini CLI reads from here
|
|
62
|
-
|
|
63
|
-
### Account Switching
|
|
64
|
-
|
|
65
|
-
When you run `gemx use <username>`, it copies that account's tokens to `~/.gemini/oauth_creds.json` where Gemini CLI expects them.
|
|
66
|
-
|
|
67
|
-
### OAuth Flow
|
|
68
|
-
|
|
69
|
-
Uses the same OAuth Client ID as Gemini CLI (public for installed apps). Your credentials never leave your machine.
|
|
70
|
-
|
|
71
|
-
## Username Rules
|
|
72
|
-
|
|
73
|
-
- 3-30 characters long
|
|
74
|
-
- Letters, numbers, and hyphens only
|
|
75
|
-
- Must start and end with letter or number
|
|
76
|
-
- No consecutive hyphens
|
|
77
|
-
|
|
78
|
-
## Troubleshooting
|
|
79
|
-
|
|
80
|
-
| Issue | Solution |
|
|
81
|
-
|-------|----------|
|
|
82
|
-
| "Account already exists" | Run `gemx remove <username>` first |
|
|
83
|
-
| "Account not found" | Run `gemx list` to see available accounts |
|
|
84
|
-
| Browser doesn't open | Visit the URL shown in the terminal manually |
|
|
85
|
-
| Auth timed out | Try again, complete login within 5 minutes |
|
|
86
|
-
| "No active account" | Run `gemx register <username>` to add one |
|
|
87
|
-
|
|
88
|
-
## License
|
|
89
|
-
|
|
90
|
-
MIT
|