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.
Files changed (48) hide show
  1. package/README.md +101 -0
  2. package/dist/auth/oauth.d.ts +13 -0
  3. package/dist/auth/oauth.d.ts.map +1 -1
  4. package/dist/auth/oauth.js +165 -140
  5. package/dist/auth/oauth.js.map +1 -1
  6. package/dist/commands/current.js +2 -2
  7. package/dist/commands/current.js.map +1 -1
  8. package/dist/commands/dash.d.ts +2 -0
  9. package/dist/commands/dash.d.ts.map +1 -0
  10. package/dist/commands/dash.js +125 -0
  11. package/dist/commands/dash.js.map +1 -0
  12. package/dist/commands/list.js +1 -1
  13. package/dist/commands/list.js.map +1 -1
  14. package/dist/commands/register.js +1 -1
  15. package/dist/commands/register.js.map +1 -1
  16. package/dist/commands/remove.js +1 -1
  17. package/dist/commands/remove.js.map +1 -1
  18. package/dist/commands/use.js +1 -1
  19. package/dist/commands/use.js.map +1 -1
  20. package/dist/index.js +16 -2
  21. package/dist/index.js.map +1 -1
  22. package/dist/server/public/app.js +181 -0
  23. package/dist/server/public/index.html +77 -0
  24. package/dist/server/public/style.css +132 -0
  25. package/dist/server/quota.d.ts +22 -0
  26. package/dist/server/quota.d.ts.map +1 -0
  27. package/dist/server/quota.js +69 -0
  28. package/dist/server/quota.js.map +1 -0
  29. package/dist/server/server.d.ts +11 -0
  30. package/dist/server/server.d.ts.map +1 -0
  31. package/dist/server/server.js +170 -0
  32. package/dist/server/server.js.map +1 -0
  33. package/dist/storage/paths.js +1 -1
  34. package/dist/storage/paths.js.map +1 -1
  35. package/package.json +20 -9
  36. package/doc/README.md +0 -90
  37. package/src/auth/oauth.ts +0 -231
  38. package/src/commands/current.ts +0 -18
  39. package/src/commands/list.ts +0 -20
  40. package/src/commands/register.ts +0 -53
  41. package/src/commands/remove.ts +0 -37
  42. package/src/commands/use.ts +0 -25
  43. package/src/index.ts +0 -59
  44. package/src/storage/accounts.ts +0 -285
  45. package/src/storage/paths.ts +0 -56
  46. package/src/utils/table.ts +0 -58
  47. package/src/utils/validation.ts +0 -27
  48. 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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"}
@@ -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 = '.gemx';
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,OAAO,CAAC;AACzB,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"}
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.1",
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
- "gemx": "./dist/index.js"
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": ["gemini", "google", "oauth", "multi-account", "cli"],
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
- "google-auth-library": "^9.15.1",
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