aigetwey 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.
- package/CHANGELOG.md +84 -0
- package/LICENSE +21 -0
- package/README.md +302 -0
- package/assets/logo.svg +8 -0
- package/assets/screenshot.png +0 -0
- package/assets/wordmark.svg +9 -0
- package/config.example.yaml +56 -0
- package/dashboard/.env.example +12 -0
- package/dashboard/next-env.d.ts +6 -0
- package/dashboard/next.config.ts +12 -0
- package/dashboard/package-lock.json +1771 -0
- package/dashboard/package.json +29 -0
- package/dashboard/postcss.config.mjs +5 -0
- package/dashboard/src/app/(console)/combos/page.tsx +10 -0
- package/dashboard/src/app/(console)/config/page.tsx +5 -0
- package/dashboard/src/app/(console)/console/page.tsx +92 -0
- package/dashboard/src/app/(console)/endpoint/page.tsx +5 -0
- package/dashboard/src/app/(console)/layout.tsx +17 -0
- package/dashboard/src/app/(console)/page.tsx +8 -0
- package/dashboard/src/app/(console)/providers/[id]/page.tsx +6 -0
- package/dashboard/src/app/(console)/providers/page.tsx +5 -0
- package/dashboard/src/app/(console)/quota/page.tsx +5 -0
- package/dashboard/src/app/(console)/tools/[id]/page.tsx +6 -0
- package/dashboard/src/app/(console)/tools/page.tsx +5 -0
- package/dashboard/src/app/(console)/usage/page.tsx +24 -0
- package/dashboard/src/app/api/cli-detect/[tool]/route.ts +253 -0
- package/dashboard/src/app/api/gw/[...path]/route.ts +89 -0
- package/dashboard/src/app/api/login/route.ts +30 -0
- package/dashboard/src/app/api/logout/route.ts +9 -0
- package/dashboard/src/app/api/password/route.ts +34 -0
- package/dashboard/src/app/globals.css +340 -0
- package/dashboard/src/app/icon.svg +8 -0
- package/dashboard/src/app/layout.tsx +28 -0
- package/dashboard/src/app/login/page.tsx +60 -0
- package/dashboard/src/components/AreaChart.tsx +115 -0
- package/dashboard/src/components/Badge.tsx +32 -0
- package/dashboard/src/components/Button.tsx +60 -0
- package/dashboard/src/components/CapacityBadges.tsx +40 -0
- package/dashboard/src/components/Checkbox.tsx +40 -0
- package/dashboard/src/components/CliToolConfig.tsx +63 -0
- package/dashboard/src/components/ConfigEditor.tsx +199 -0
- package/dashboard/src/components/ConfirmModal.tsx +36 -0
- package/dashboard/src/components/CooldownTimer.tsx +42 -0
- package/dashboard/src/components/EndpointView.tsx +439 -0
- package/dashboard/src/components/Icon.tsx +25 -0
- package/dashboard/src/components/KeyReveal.tsx +78 -0
- package/dashboard/src/components/Lamp.tsx +8 -0
- package/dashboard/src/components/LogTable.tsx +223 -0
- package/dashboard/src/components/LogoutButton.tsx +20 -0
- package/dashboard/src/components/ModelPicker.tsx +121 -0
- package/dashboard/src/components/ModelSelectModal.tsx +126 -0
- package/dashboard/src/components/PasswordEditor.tsx +86 -0
- package/dashboard/src/components/PricingEditor.tsx +171 -0
- package/dashboard/src/components/ProviderDetail.tsx +566 -0
- package/dashboard/src/components/ProviderManager.tsx +311 -0
- package/dashboard/src/components/QuotaView.tsx +78 -0
- package/dashboard/src/components/Rail.tsx +82 -0
- package/dashboard/src/components/RichCard.tsx +46 -0
- package/dashboard/src/components/RoutingView.tsx +329 -0
- package/dashboard/src/components/ThemeProvider.tsx +36 -0
- package/dashboard/src/components/ToastProvider.tsx +58 -0
- package/dashboard/src/components/ToolDetail.tsx +475 -0
- package/dashboard/src/components/TopBar.tsx +128 -0
- package/dashboard/src/components/UsageView.tsx +151 -0
- package/dashboard/src/components/ui.tsx +54 -0
- package/dashboard/src/lib/capabilities.ts +318 -0
- package/dashboard/src/lib/cliTools.ts +120 -0
- package/dashboard/src/lib/client.ts +190 -0
- package/dashboard/src/lib/gateway.ts +269 -0
- package/dashboard/src/lib/session.ts +71 -0
- package/dashboard/src/middleware.ts +37 -0
- package/dashboard/tsconfig.json +21 -0
- package/dist/adapters/anthropic.js +289 -0
- package/dist/adapters/anthropic.js.map +1 -0
- package/dist/adapters/gemini.js +268 -0
- package/dist/adapters/gemini.js.map +1 -0
- package/dist/adapters/index.js +8 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/openai.js +13 -0
- package/dist/adapters/openai.js.map +1 -0
- package/dist/cli/tray/autostart.js +152 -0
- package/dist/cli/tray/autostart.js.map +1 -0
- package/dist/cli/tray/icon.js +4 -0
- package/dist/cli/tray/icon.js.map +1 -0
- package/dist/cli/tray/tray.js +141 -0
- package/dist/cli/tray/tray.js.map +1 -0
- package/dist/cli/tray/trayRuntime.js +91 -0
- package/dist/cli/tray/trayRuntime.js.map +1 -0
- package/dist/cli.js +361 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.js +728 -0
- package/dist/config.js.map +1 -0
- package/dist/core/authStore.js +78 -0
- package/dist/core/authStore.js.map +1 -0
- package/dist/core/canonical.js +9 -0
- package/dist/core/canonical.js.map +1 -0
- package/dist/core/console-buffer.js +25 -0
- package/dist/core/console-buffer.js.map +1 -0
- package/dist/core/fallback.js +62 -0
- package/dist/core/fallback.js.map +1 -0
- package/dist/core/handler.js +174 -0
- package/dist/core/handler.js.map +1 -0
- package/dist/core/keypool.js +105 -0
- package/dist/core/keypool.js.map +1 -0
- package/dist/core/quota.js +165 -0
- package/dist/core/quota.js.map +1 -0
- package/dist/core/state.js +52 -0
- package/dist/core/state.js.map +1 -0
- package/dist/db.js +193 -0
- package/dist/db.js.map +1 -0
- package/dist/headroom/compress.js +44 -0
- package/dist/headroom/compress.js.map +1 -0
- package/dist/headroom/detect.js +108 -0
- package/dist/headroom/detect.js.map +1 -0
- package/dist/headroom/process.js +158 -0
- package/dist/headroom/process.js.map +1 -0
- package/dist/inject/caveman.js +30 -0
- package/dist/inject/caveman.js.map +1 -0
- package/dist/inject/index.js +24 -0
- package/dist/inject/index.js.map +1 -0
- package/dist/inject/ponytail.js +19 -0
- package/dist/inject/ponytail.js.map +1 -0
- package/dist/middleware/auth.js +66 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/providers/capabilities.js +246 -0
- package/dist/providers/capabilities.js.map +1 -0
- package/dist/providers/free.js +43 -0
- package/dist/providers/free.js.map +1 -0
- package/dist/providers/pricing.js +224 -0
- package/dist/providers/pricing.js.map +1 -0
- package/dist/providers/vertex.js +97 -0
- package/dist/providers/vertex.js.map +1 -0
- package/dist/routes/admin.js +622 -0
- package/dist/routes/admin.js.map +1 -0
- package/dist/routes/health.js +4 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/index.js +12 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/v1.js +75 -0
- package/dist/routes/v1.js.map +1 -0
- package/dist/rtk/detect.js +50 -0
- package/dist/rtk/detect.js.map +1 -0
- package/dist/rtk/filters.js +85 -0
- package/dist/rtk/filters.js.map +1 -0
- package/dist/rtk/index.js +39 -0
- package/dist/rtk/index.js.map +1 -0
- package/dist/server.js +100 -0
- package/dist/server.js.map +1 -0
- package/dist/stream/anthropic-stream.js +239 -0
- package/dist/stream/anthropic-stream.js.map +1 -0
- package/dist/stream/chunk.js +7 -0
- package/dist/stream/chunk.js.map +1 -0
- package/dist/stream/gemini-stream.js +135 -0
- package/dist/stream/gemini-stream.js.map +1 -0
- package/dist/stream/index.js +12 -0
- package/dist/stream/index.js.map +1 -0
- package/dist/stream/openai-stream.js +34 -0
- package/dist/stream/openai-stream.js.map +1 -0
- package/dist/stream/sse.js +64 -0
- package/dist/stream/sse.js.map +1 -0
- package/dist/translator/thinking.js +70 -0
- package/dist/translator/thinking.js.map +1 -0
- package/dist/translator/thinkingUnified.js +322 -0
- package/dist/translator/thinkingUnified.js.map +1 -0
- package/dist/upstream/client.js +120 -0
- package/dist/upstream/client.js.map +1 -0
- package/package.json +76 -0
- package/run.sh +27 -0
- package/src/adapters/anthropic.ts +377 -0
- package/src/adapters/gemini.ts +341 -0
- package/src/adapters/index.ts +17 -0
- package/src/adapters/openai.ts +22 -0
- package/src/cli/tray/autostart.ts +133 -0
- package/src/cli/tray/icon.ts +4 -0
- package/src/cli/tray/tray.ts +156 -0
- package/src/cli/tray/trayRuntime.ts +90 -0
- package/src/cli.ts +379 -0
- package/src/config.ts +777 -0
- package/src/core/authStore.ts +86 -0
- package/src/core/canonical.ts +93 -0
- package/src/core/console-buffer.ts +39 -0
- package/src/core/fallback.ts +116 -0
- package/src/core/handler.ts +236 -0
- package/src/core/keypool.ts +152 -0
- package/src/core/quota.ts +214 -0
- package/src/core/state.ts +65 -0
- package/src/db.ts +280 -0
- package/src/headroom/compress.ts +78 -0
- package/src/headroom/detect.ts +119 -0
- package/src/headroom/process.ts +166 -0
- package/src/inject/caveman.ts +35 -0
- package/src/inject/index.ts +46 -0
- package/src/inject/ponytail.ts +31 -0
- package/src/middleware/auth.ts +76 -0
- package/src/providers/capabilities.ts +297 -0
- package/src/providers/free.ts +53 -0
- package/src/providers/pricing.ts +261 -0
- package/src/providers/vertex.ts +117 -0
- package/src/routes/admin.ts +716 -0
- package/src/routes/health.ts +5 -0
- package/src/routes/index.ts +24 -0
- package/src/routes/v1.ts +87 -0
- package/src/rtk/detect.ts +55 -0
- package/src/rtk/filters.ts +94 -0
- package/src/rtk/index.ts +58 -0
- package/src/server.ts +108 -0
- package/src/stream/anthropic-stream.ts +310 -0
- package/src/stream/chunk.ts +46 -0
- package/src/stream/gemini-stream.ts +158 -0
- package/src/stream/index.ts +23 -0
- package/src/stream/openai-stream.ts +41 -0
- package/src/stream/sse.ts +72 -0
- package/src/translator/thinking.ts +64 -0
- package/src/translator/thinkingUnified.ts +319 -0
- package/src/upstream/client.ts +155 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CanonicalRequest, CanonicalResponse, WireFormat } from "../core/canonical.js";
|
|
2
|
+
import * as openai from "./openai.js";
|
|
3
|
+
import * as anthropic from "./anthropic.js";
|
|
4
|
+
import * as gemini from "./gemini.js";
|
|
5
|
+
|
|
6
|
+
export interface Adapter {
|
|
7
|
+
requestToCanonical(body: unknown): CanonicalRequest;
|
|
8
|
+
requestFromCanonical(req: CanonicalRequest): unknown;
|
|
9
|
+
responseToCanonical(resp: unknown): CanonicalResponse;
|
|
10
|
+
responseFromCanonical(resp: CanonicalResponse): unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const ADAPTERS: Record<WireFormat, Adapter> = { openai, anthropic, gemini };
|
|
14
|
+
|
|
15
|
+
export function adapterFor(format: WireFormat): Adapter {
|
|
16
|
+
return ADAPTERS[format];
|
|
17
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI adapter. The canonical format IS the OpenAI Chat Completions shape, so
|
|
3
|
+
* these are near-identity passes — they exist so every format flows through the
|
|
4
|
+
* same adapter interface.
|
|
5
|
+
*/
|
|
6
|
+
import type { CanonicalRequest, CanonicalResponse } from "../core/canonical.js";
|
|
7
|
+
|
|
8
|
+
export function requestToCanonical(body: unknown): CanonicalRequest {
|
|
9
|
+
return body as CanonicalRequest;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function requestFromCanonical(req: CanonicalRequest): unknown {
|
|
13
|
+
return req;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function responseToCanonical(resp: unknown): CanonicalResponse {
|
|
17
|
+
return resp as CanonicalResponse;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function responseFromCanonical(resp: CanonicalResponse): unknown {
|
|
21
|
+
return resp;
|
|
22
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run-on-OS-startup, toggled from the tray menu — registers the aigetwey CLI to
|
|
3
|
+
* launch with `--tray` at login.
|
|
4
|
+
*
|
|
5
|
+
* macOS → ~/Library/LaunchAgents/com.aigetwey.autostart.plist (launchd)
|
|
6
|
+
* Windows→ %APPDATA%/.../Startup/aigetwey.vbs
|
|
7
|
+
* Linux → ~/.config/autostart/aigetwey.desktop
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, mkdirSync, writeFileSync, unlinkSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join, dirname, basename, resolve } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { execSync } from "node:child_process";
|
|
14
|
+
|
|
15
|
+
const APP_NAME = "aigetwey";
|
|
16
|
+
const APP_LABEL = "com.aigetwey.autostart";
|
|
17
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
|
|
19
|
+
/** Absolute path to the launcher script (dist/cli.js). */
|
|
20
|
+
function getCliPath(explicit?: string): string | null {
|
|
21
|
+
if (explicit && existsSync(resolve(explicit))) return resolve(explicit);
|
|
22
|
+
if (process.argv[1]) {
|
|
23
|
+
const r = resolve(process.argv[1]);
|
|
24
|
+
if (/cli\.(js|ts)$/.test(basename(r)) && existsSync(r)) return r;
|
|
25
|
+
}
|
|
26
|
+
// dist/cli/tray/autostart.js → up two → dist/cli.js
|
|
27
|
+
const computed = resolve(here, "..", "..", "cli.js");
|
|
28
|
+
return existsSync(computed) ? computed : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isAutoStartEnabled(): boolean {
|
|
32
|
+
try {
|
|
33
|
+
if (process.platform === "darwin") {
|
|
34
|
+
const plist = join(homedir(), "Library", "LaunchAgents", `${APP_LABEL}.plist`);
|
|
35
|
+
return existsSync(plist);
|
|
36
|
+
}
|
|
37
|
+
if (process.platform === "win32") {
|
|
38
|
+
return existsSync(join(process.env.APPDATA || "", "Microsoft", "Windows", "Start Menu", "Programs", "Startup", `${APP_NAME}.vbs`));
|
|
39
|
+
}
|
|
40
|
+
return existsSync(join(homedir(), ".config", "autostart", `${APP_NAME}.desktop`));
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function enableAutoStart(cliPath?: string): boolean {
|
|
47
|
+
if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return false;
|
|
48
|
+
const script = getCliPath(cliPath);
|
|
49
|
+
if (!script) return false;
|
|
50
|
+
const node = process.execPath;
|
|
51
|
+
try {
|
|
52
|
+
if (process.platform === "darwin") return enableMac(node, script);
|
|
53
|
+
if (process.platform === "win32") return enableWin(node, script);
|
|
54
|
+
return enableLinux(node, script);
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function disableAutoStart(): boolean {
|
|
61
|
+
try {
|
|
62
|
+
if (process.platform === "darwin") return disableMac();
|
|
63
|
+
if (process.platform === "win32") return disableWin();
|
|
64
|
+
return disableLinux();
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── macOS ──
|
|
71
|
+
function enableMac(node: string, script: string): boolean {
|
|
72
|
+
const dir = join(homedir(), "Library", "LaunchAgents");
|
|
73
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
74
|
+
const plistPath = join(dir, `${APP_LABEL}.plist`);
|
|
75
|
+
const path = `${dirname(node)}:/usr/local/bin:/usr/bin:/bin`;
|
|
76
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
77
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
78
|
+
<plist version="1.0"><dict>
|
|
79
|
+
<key>Label</key><string>${APP_LABEL}</string>
|
|
80
|
+
<key>ProgramArguments</key><array>
|
|
81
|
+
<string>${node}</string><string>${script}</string><string>--tray</string>
|
|
82
|
+
</array>
|
|
83
|
+
<key>EnvironmentVariables</key><dict><key>PATH</key><string>${path}</string></dict>
|
|
84
|
+
<key>RunAtLoad</key><true/>
|
|
85
|
+
<key>KeepAlive</key><false/>
|
|
86
|
+
</dict></plist>`;
|
|
87
|
+
writeFileSync(plistPath, plist);
|
|
88
|
+
try { execSync(`launchctl unload "${plistPath}"`, { stdio: "ignore" }); } catch { /* not loaded */ }
|
|
89
|
+
try { execSync(`launchctl load -w "${plistPath}"`, { stdio: "ignore" }); } catch { /* picked up next login */ }
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
function disableMac(): boolean {
|
|
93
|
+
const plistPath = join(homedir(), "Library", "LaunchAgents", `${APP_LABEL}.plist`);
|
|
94
|
+
try { execSync(`launchctl unload "${plistPath}"`, { stdio: "ignore" }); } catch { /* not loaded */ }
|
|
95
|
+
if (existsSync(plistPath)) unlinkSync(plistPath);
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── Windows ──
|
|
100
|
+
function enableWin(node: string, script: string): boolean {
|
|
101
|
+
const dir = join(process.env.APPDATA || "", "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
|
|
102
|
+
if (!existsSync(dir)) return false;
|
|
103
|
+
const vbs = `Set WshShell = CreateObject("WScript.Shell")\nWshShell.Run """${node}"" ""${script}"" --tray", 0, False\n`;
|
|
104
|
+
writeFileSync(join(dir, `${APP_NAME}.vbs`), vbs);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
function disableWin(): boolean {
|
|
108
|
+
const vbs = join(process.env.APPDATA || "", "Microsoft", "Windows", "Start Menu", "Programs", "Startup", `${APP_NAME}.vbs`);
|
|
109
|
+
if (existsSync(vbs)) unlinkSync(vbs);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Linux ──
|
|
114
|
+
function enableLinux(node: string, script: string): boolean {
|
|
115
|
+
const dir = join(homedir(), ".config", "autostart");
|
|
116
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
117
|
+
const desktop = `[Desktop Entry]
|
|
118
|
+
Type=Application
|
|
119
|
+
Name=aigetwey
|
|
120
|
+
Comment=Personal AI gateway
|
|
121
|
+
Exec=${node} ${script} --tray
|
|
122
|
+
Hidden=false
|
|
123
|
+
NoDisplay=false
|
|
124
|
+
X-GNOME-Autostart-enabled=true
|
|
125
|
+
`;
|
|
126
|
+
writeFileSync(join(dir, `${APP_NAME}.desktop`), desktop);
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
function disableLinux(): boolean {
|
|
130
|
+
const desktop = join(homedir(), ".config", "autostart", `${APP_NAME}.desktop`);
|
|
131
|
+
if (existsSync(desktop)) unlinkSync(desktop);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** The aigetwey tray icon ("a»" mark), embedded so it ships without an asset
|
|
2
|
+
* copy step. 64×64 RGBA PNG, base64. */
|
|
3
|
+
export const TRAY_ICON_PNG_BASE64 =
|
|
4
|
+
"iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABmJLR0QA/wD/AP+gvaeTAAAEZklEQVR4nO2ZX2hbVRzHv79zb1qnjq46TVItOrc6xAlBmzqGD6s6BkJxIqXmtk5QEARfnG7OP+geHNiVCr6U+eS2mtR1LyqIuAfdg4zWpNvErSI+adfmD+KmDatdkvOTEzNIk3uTZs3WZD2fp5vf79d7z/fbc3N+5wTQaDQajUaj0Wg0Go1Go9GsLGi5BxCO9m0lyEcZYtzv/eyEXc3Jqe7bGhpcPer68uXU0S2tx/66IQyIxAO7mGnwymcGfdjhCb5VKN7lcp0CcE8uNJUReGLznaHfqjEGgWWEmfbmfybw3nA80J8fM03zuTzxilZT8vdjCaut7g2whWlPoQlFJaC7qmWCwHLCPFDOhHQ6/bma9tfKBKqk+Ny57oa55sZNkjIbSNCa7EAkXyQyJ9vd6yeJ9slKBxCO9X4A8Du2ScZHfm/odSVSiVWibSTEhEw//kjL0V8qffaiDRiPPb9OIP0uQM8CaHIoixIw1DR7YaCt7Zt51IkJVK4gErW6mXAYwKpF3vMMSd7W3jLyZz2YIEolI4nAY0wIOYi/BCBjE/exwDFUiN8TVDNsv22SsCsctQbV0pcW1Eng6eIi9khhfjcx0/NA1Qxgpo/VSrRwLPxThulBvyd0S3Ku4VYm9AFIFYx4azhqPYU6MEE4JX5MWD4wHrb5i1c3e4OT6rJz3aF/O9yhIAGjxQNm9X2BWjdBOCYkZYjwHgj9AH0CxjCBRnHH6vGiRxKOFw+WfLhKrqcJhCoQjge2ganQhN/9ntC9S7xvv+oJbJPMe/zekYHSX4yYSqVSvlJ7B7OSAZ2c6l5FpuE2DLHGSMPIBgU3SaZNNuUuLBG/e+TNcDyQbYyKkkS7AQyomTCWsDodTGjNtdJDV23A2Pneuw2TXyFgBwMbARiQgBQ1s6FcEmapZCTWazH4IIDVjOtPmVcg20aXewVyrXTlBkzEre2S+Uj2P76QFECnQPwHGBdyMS+ALlSRcMzaD4ateGYe7Cj7/mcbo+3lzg5Mp4RkDBSJJ0ykDPPpLWuPTBceaoBkV1XFA2+XEP9GtbpCYRecSPRtAPBQUXFGvFYoPvs44hbUoXhHA5j5Pru4KyXP2taDn0Qdinc0QLJstIvP34TmwlhuP66WmkJuRo2Ld54BkqN2cZL0QuE22ZD4ymGz1DR+fuftqGHxjot4hF92cTyZAJA99MhDrYZfMuNnItwPYAcANVtmwTgOwoL+n4kPGywOSoik3zN8ttbEl+xiwlFrHwjvYzEQdoLxD4Av7NM02u4J9hQ/I7AbRAfs78kHVCeYOxU+o7q64vvytNoPLOWEWDglkp4ZdUChpncp5pn5Rb87NJx0z3wNxumKnv5/O+soXl3mWtlrIr6kAZ10It3u3vgMQC+p3bE6n8xLzxDREEn2dXhHPr1Sn3KZXUQ4BMKvAC4CiIPxAwPfYrHkiXcsqZL4ihp5dSA6u9ZsnrvU+Lc6B8AN8sMIYYX/NKbRaDQajUaj0Wg0Go1Go8EK4z+xLpJ3fRAUmgAAAABJRU5ErkJggg==";
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System tray icon. macOS/Linux use the lazy-installed `systray2` Go binary;
|
|
3
|
+
* Windows uses a PowerShell NotifyIcon (see trayWin). Menu: status · Open
|
|
4
|
+
* Dashboard · Auto-start toggle · Quit. The launcher owns the gateway +
|
|
5
|
+
* dashboard child processes, so Quit tears those down too.
|
|
6
|
+
*/
|
|
7
|
+
import { exec } from "node:child_process";
|
|
8
|
+
import { createRequire } from "node:module";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { getRuntimeNodeModules } from "./trayRuntime.js";
|
|
11
|
+
import { TRAY_ICON_PNG_BASE64 } from "./icon.js";
|
|
12
|
+
import { isAutoStartEnabled, enableAutoStart, disableAutoStart } from "./autostart.js";
|
|
13
|
+
|
|
14
|
+
export interface TrayOptions {
|
|
15
|
+
/** dashboard URL opened by "Open Dashboard". */
|
|
16
|
+
dashboardUrl: string;
|
|
17
|
+
port: number;
|
|
18
|
+
onQuit: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface SysTrayInstance {
|
|
22
|
+
onClick(cb: (action: { seq_id: number }) => void): void;
|
|
23
|
+
sendAction(action: unknown): void;
|
|
24
|
+
ready?: () => Promise<void>;
|
|
25
|
+
onReady?: (cb: () => void) => void;
|
|
26
|
+
onError?: (cb: () => void) => void;
|
|
27
|
+
kill(graceful?: boolean): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const MENU = { STATUS: 0, DASHBOARD: 1, AUTOSTART: 2, QUIT: 3 };
|
|
31
|
+
let tray: SysTrayInstance | null = null;
|
|
32
|
+
let winTray: { kill(): void; updateItem(i: number, t: string, e: boolean): void } | null = null;
|
|
33
|
+
|
|
34
|
+
function openBrowser(url: string): void {
|
|
35
|
+
const cmd =
|
|
36
|
+
process.platform === "darwin" ? `open "${url}"` : process.platform === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`;
|
|
37
|
+
exec(cmd);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isTraySupported(): boolean {
|
|
41
|
+
if (!["darwin", "win32", "linux"].includes(process.platform)) return false;
|
|
42
|
+
if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return false;
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function menuItems(autostart: boolean): Array<{ title: string; tooltip: string; enabled: boolean }> {
|
|
47
|
+
return [
|
|
48
|
+
{ title: `aigetwey (port ${0})`, tooltip: "gateway + dashboard running", enabled: false },
|
|
49
|
+
{ title: "Open Dashboard", tooltip: "open the console in your browser", enabled: true },
|
|
50
|
+
{ title: autostart ? "✓ Auto-start enabled" : "Enable auto-start", tooltip: "run on OS startup", enabled: true },
|
|
51
|
+
{ title: "Quit", tooltip: "stop the gateway + dashboard and exit", enabled: true },
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function handleClick(index: number, opts: TrayOptions, onToggle: (enabled: boolean) => void): void {
|
|
56
|
+
if (index === MENU.DASHBOARD) {
|
|
57
|
+
openBrowser(opts.dashboardUrl);
|
|
58
|
+
} else if (index === MENU.AUTOSTART) {
|
|
59
|
+
const enabled = isAutoStartEnabled();
|
|
60
|
+
if (enabled) disableAutoStart();
|
|
61
|
+
else enableAutoStart();
|
|
62
|
+
onToggle(!enabled);
|
|
63
|
+
} else if (index === MENU.QUIT) {
|
|
64
|
+
opts.onQuit();
|
|
65
|
+
void killTray();
|
|
66
|
+
setTimeout(() => process.exit(0), 400);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Show the tray icon. Returns true if it started, false if unsupported/failed. */
|
|
71
|
+
export function initTray(opts: TrayOptions): boolean {
|
|
72
|
+
if (!isTraySupported()) return false;
|
|
73
|
+
if (process.platform === "win32") return initWindowsTray(opts);
|
|
74
|
+
return initUnixTray(opts);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function initUnixTray(opts: TrayOptions): boolean {
|
|
78
|
+
try {
|
|
79
|
+
const require = createRequire(import.meta.url);
|
|
80
|
+
let SysTray: new (cfg: unknown) => SysTrayInstance;
|
|
81
|
+
try {
|
|
82
|
+
SysTray = require(join(getRuntimeNodeModules(), "systray2")).default;
|
|
83
|
+
} catch {
|
|
84
|
+
SysTray = require("systray2").default; // fallback to a local install
|
|
85
|
+
}
|
|
86
|
+
const autostart = isAutoStartEnabled();
|
|
87
|
+
const items = menuItems(autostart).map((it, i) =>
|
|
88
|
+
i === MENU.STATUS ? { ...it, title: `aigetwey (port ${opts.port})` } : it,
|
|
89
|
+
);
|
|
90
|
+
tray = new SysTray({
|
|
91
|
+
menu: {
|
|
92
|
+
icon: TRAY_ICON_PNG_BASE64,
|
|
93
|
+
isTemplateIcon: false,
|
|
94
|
+
title: "",
|
|
95
|
+
tooltip: `aigetwey — port ${opts.port}`,
|
|
96
|
+
items,
|
|
97
|
+
},
|
|
98
|
+
debug: false,
|
|
99
|
+
copyDir: true,
|
|
100
|
+
});
|
|
101
|
+
tray.onClick((action) => {
|
|
102
|
+
handleClick(action.seq_id, opts, (enabled) => {
|
|
103
|
+
tray?.sendAction({
|
|
104
|
+
type: "update-item",
|
|
105
|
+
item: { title: enabled ? "✓ Auto-start enabled" : "Enable auto-start", tooltip: "run on OS startup", enabled: true },
|
|
106
|
+
seq_id: MENU.AUTOSTART,
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
tray.ready?.().catch((e: unknown) =>
|
|
111
|
+
process.stderr.write(` tray failed to start: ${(e as Error)?.message ?? e}\n`),
|
|
112
|
+
);
|
|
113
|
+
return true;
|
|
114
|
+
} catch (e) {
|
|
115
|
+
process.stderr.write(` tray init error: ${(e as Error).message}\n`);
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function initWindowsTray(opts: TrayOptions): boolean {
|
|
121
|
+
try {
|
|
122
|
+
const require = createRequire(import.meta.url);
|
|
123
|
+
const { initWinTray } = require("./trayWin.js") as {
|
|
124
|
+
initWinTray: (cfg: unknown) => { kill(): void; updateItem(i: number, t: string, e: boolean): void };
|
|
125
|
+
};
|
|
126
|
+
const autostart = isAutoStartEnabled();
|
|
127
|
+
winTray = initWinTray({
|
|
128
|
+
tooltip: `aigetwey - port ${opts.port}`,
|
|
129
|
+
items: menuItems(autostart).map((it, i) =>
|
|
130
|
+
i === MENU.STATUS ? { ...it, title: `aigetwey (port ${opts.port})` } : it,
|
|
131
|
+
),
|
|
132
|
+
onClick: (index: number) =>
|
|
133
|
+
handleClick(index, opts, (enabled) =>
|
|
134
|
+
winTray?.updateItem(MENU.AUTOSTART, enabled ? "✓ Auto-start enabled" : "Enable auto-start", true),
|
|
135
|
+
),
|
|
136
|
+
});
|
|
137
|
+
return true;
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Tear down the tray icon (graceful so macOS releases the menubar item). */
|
|
144
|
+
export function killTray(): Promise<void> {
|
|
145
|
+
if (winTray) {
|
|
146
|
+
try { winTray.kill(); } catch { /* gone */ }
|
|
147
|
+
winTray = null;
|
|
148
|
+
return Promise.resolve();
|
|
149
|
+
}
|
|
150
|
+
const inst = tray;
|
|
151
|
+
tray = null;
|
|
152
|
+
if (!inst) return Promise.resolve();
|
|
153
|
+
try { inst.kill(true); } catch { /* gone */ }
|
|
154
|
+
try { inst.kill(false); } catch { /* gone */ }
|
|
155
|
+
return Promise.resolve();
|
|
156
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy-install the system-tray runtime (`systray2`) into a user data dir rather
|
|
3
|
+
* than the published npm tarball. systray2 ships a small Go binary; keeping it
|
|
4
|
+
* out of the tarball avoids antivirus false positives (e.g. Kaspersky) and
|
|
5
|
+
* per-arch packaging in the published package.
|
|
6
|
+
*
|
|
7
|
+
* macOS/Linux: install systray2 into ~/.aigetwey/runtime/node_modules.
|
|
8
|
+
* Windows: no binary — a PowerShell NotifyIcon is used instead (see trayWin).
|
|
9
|
+
*/
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { existsSync, mkdirSync, writeFileSync, chmodSync } from "node:fs";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
|
|
15
|
+
const SYSTRAY_PKG = "systray2";
|
|
16
|
+
const SYSTRAY_VERSION = "2.1.4";
|
|
17
|
+
|
|
18
|
+
/** ~/.aigetwey (or %APPDATA%/aigetwey on Windows) — holds the tray runtime. */
|
|
19
|
+
export function getRuntimeDir(): string {
|
|
20
|
+
const base =
|
|
21
|
+
process.platform === "win32"
|
|
22
|
+
? process.env.APPDATA || homedir()
|
|
23
|
+
: homedir();
|
|
24
|
+
return join(base, process.platform === "win32" ? "aigetwey" : ".aigetwey", "runtime");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getRuntimeNodeModules(): string {
|
|
28
|
+
return join(getRuntimeDir(), "node_modules");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function trayBinName(): string | null {
|
|
32
|
+
if (process.platform === "darwin") return "tray_darwin_release";
|
|
33
|
+
if (process.platform === "linux") return "tray_linux_release";
|
|
34
|
+
return null; // windows uses powershell, no binary
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** systray2's tarball sometimes drops the +x bit on the Go binary → EACCES. */
|
|
38
|
+
function chmodTrayBin(): void {
|
|
39
|
+
const bin = trayBinName();
|
|
40
|
+
if (!bin) return;
|
|
41
|
+
const p = join(getRuntimeNodeModules(), SYSTRAY_PKG, "traybin", bin);
|
|
42
|
+
try {
|
|
43
|
+
if (existsSync(p)) chmodSync(p, 0o755);
|
|
44
|
+
} catch {
|
|
45
|
+
/* best-effort */
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function hasSystray(): boolean {
|
|
50
|
+
return existsSync(join(getRuntimeNodeModules(), SYSTRAY_PKG, "package.json"));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function ensureRuntimeDir(): string {
|
|
54
|
+
const dir = getRuntimeDir();
|
|
55
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
56
|
+
const pkg = join(dir, "package.json");
|
|
57
|
+
if (!existsSync(pkg)) {
|
|
58
|
+
writeFileSync(pkg, JSON.stringify({ name: "aigetwey-runtime", version: "1.0.0", private: true }, null, 2));
|
|
59
|
+
}
|
|
60
|
+
return dir;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Make sure systray2 is installed (Windows skips — PowerShell needs no binary).
|
|
65
|
+
* Returns true if the tray runtime is ready. Best-effort: install failure just
|
|
66
|
+
* disables the tray, never crashes the launcher.
|
|
67
|
+
*/
|
|
68
|
+
export function ensureTrayRuntime({ silent = false } = {}): boolean {
|
|
69
|
+
if (process.platform === "win32") return true;
|
|
70
|
+
if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
|
|
71
|
+
return false; // no GUI session — a tray icon has nowhere to live
|
|
72
|
+
}
|
|
73
|
+
if (hasSystray()) {
|
|
74
|
+
chmodTrayBin();
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
const cwd = ensureRuntimeDir();
|
|
78
|
+
if (!silent) console.log(" installing system tray (first run)…");
|
|
79
|
+
const res = spawnSync("npm", ["install", `${SYSTRAY_PKG}@${SYSTRAY_VERSION}`, "--no-save", "--no-audit", "--no-fund"], {
|
|
80
|
+
cwd,
|
|
81
|
+
stdio: silent ? "ignore" : "inherit",
|
|
82
|
+
timeout: 120_000,
|
|
83
|
+
});
|
|
84
|
+
if (res.status !== 0) {
|
|
85
|
+
if (!silent) console.warn(" system tray install failed — tray disabled (everything else works).");
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
chmodTrayBin();
|
|
89
|
+
return hasSystray();
|
|
90
|
+
}
|