@hellcoder/companion 0.96.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.ts +168 -0
- package/bin/ctl.ts +528 -0
- package/bin/generate-token.ts +28 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/AgentsPage-DCFhrJ28.js +13 -0
- package/dist/assets/CronManager-EGwLJONv.js +1 -0
- package/dist/assets/IntegrationsPage-CTMRnbQS.js +1 -0
- package/dist/assets/LinearOAuthSettingsPage-CgQFMIgr.js +1 -0
- package/dist/assets/LinearSettingsPage-C9nok1qi.js +1 -0
- package/dist/assets/Playground-BV3k0RbV.js +109 -0
- package/dist/assets/PromptsPage-CFojqNKP.js +4 -0
- package/dist/assets/RunsPage-DUJ1QUSa.js +1 -0
- package/dist/assets/SandboxManager-CrVQ-VU_.js +8 -0
- package/dist/assets/SettingsPage-D1fPCL19.js +1 -0
- package/dist/assets/TailscalePage-D06cyvyC.js +1 -0
- package/dist/assets/index-BhUa1e6X.css +1 -0
- package/dist/assets/index-DkqeP-R9.js +134 -0
- package/dist/assets/sw-register-BibwRdvC.js +1 -0
- package/dist/assets/workbox-window.prod.es5-BIl4cyR9.js +2 -0
- package/dist/favicon.svg +8 -0
- package/dist/fonts/MesloLGSNerdFontMono-Bold.woff2 +0 -0
- package/dist/fonts/MesloLGSNerdFontMono-Regular.woff2 +0 -0
- package/dist/icon-192.png +0 -0
- package/dist/icon-512.png +0 -0
- package/dist/index.html +20 -0
- package/dist/logo-codex.svg +14 -0
- package/dist/logo-docker.svg +4 -0
- package/dist/logo.svg +14 -0
- package/dist/manifest.json +24 -0
- package/dist/sw.js +2 -0
- package/package.json +104 -0
- package/server/agent-cron-migrator.test.ts +610 -0
- package/server/agent-cron-migrator.ts +85 -0
- package/server/agent-executor.test.ts +1108 -0
- package/server/agent-executor.ts +346 -0
- package/server/agent-store.test.ts +588 -0
- package/server/agent-store.ts +185 -0
- package/server/agent-types.ts +138 -0
- package/server/ai-validation-settings.test.ts +128 -0
- package/server/ai-validation-settings.ts +35 -0
- package/server/ai-validator.test.ts +387 -0
- package/server/ai-validator.ts +271 -0
- package/server/auth-manager.test.ts +83 -0
- package/server/auth-manager.ts +150 -0
- package/server/auto-namer.test.ts +252 -0
- package/server/auto-namer.ts +78 -0
- package/server/backend-adapter.test.ts +38 -0
- package/server/backend-adapter.ts +54 -0
- package/server/cache-headers.test.ts +98 -0
- package/server/cache-headers.ts +61 -0
- package/server/claude-adapter.test.ts +1363 -0
- package/server/claude-adapter.ts +889 -0
- package/server/claude-container-auth.test.ts +44 -0
- package/server/claude-container-auth.ts +30 -0
- package/server/claude-protocol-contract.test.ts +71 -0
- package/server/claude-protocol-drift.test.ts +78 -0
- package/server/claude-session-discovery.test.ts +132 -0
- package/server/claude-session-discovery.ts +157 -0
- package/server/claude-session-history.test.ts +158 -0
- package/server/claude-session-history.ts +410 -0
- package/server/cli-launcher.test.ts +1343 -0
- package/server/cli-launcher.ts +1298 -0
- package/server/cli.test.ts +16 -0
- package/server/codex-adapter.test.ts +5545 -0
- package/server/codex-adapter.ts +3062 -0
- package/server/codex-container-auth.test.ts +50 -0
- package/server/codex-container-auth.ts +24 -0
- package/server/codex-home.test.ts +61 -0
- package/server/codex-home.ts +26 -0
- package/server/codex-protocol-contract.test.ts +96 -0
- package/server/codex-protocol-drift.test.ts +123 -0
- package/server/codex-ws-proxy.cjs +226 -0
- package/server/commands-discovery.test.ts +179 -0
- package/server/commands-discovery.ts +81 -0
- package/server/constants.ts +7 -0
- package/server/container-manager.test.ts +1211 -0
- package/server/container-manager.ts +1053 -0
- package/server/cron-scheduler.test.ts +957 -0
- package/server/cron-scheduler.ts +243 -0
- package/server/cron-store.test.ts +422 -0
- package/server/cron-store.ts +148 -0
- package/server/cron-types.ts +63 -0
- package/server/env-manager.test.ts +268 -0
- package/server/env-manager.ts +161 -0
- package/server/event-bus-types.ts +64 -0
- package/server/event-bus.test.ts +244 -0
- package/server/event-bus.ts +124 -0
- package/server/execution-store.test.ts +307 -0
- package/server/execution-store.ts +170 -0
- package/server/fs-utils.ts +15 -0
- package/server/git-utils.test.ts +938 -0
- package/server/git-utils.ts +421 -0
- package/server/github-pr.test.ts +498 -0
- package/server/github-pr.ts +379 -0
- package/server/image-pull-manager.test.ts +303 -0
- package/server/image-pull-manager.ts +279 -0
- package/server/index.ts +396 -0
- package/server/linear-agent-bridge.test.ts +1157 -0
- package/server/linear-agent-bridge.ts +629 -0
- package/server/linear-agent.test.ts +473 -0
- package/server/linear-agent.ts +479 -0
- package/server/linear-cache.test.ts +136 -0
- package/server/linear-cache.ts +113 -0
- package/server/linear-connections.test.ts +350 -0
- package/server/linear-connections.ts +231 -0
- package/server/linear-credential-migration.test.ts +337 -0
- package/server/linear-credential-migration.ts +63 -0
- package/server/linear-oauth-connections-migration.test.ts +268 -0
- package/server/linear-oauth-connections.test.ts +365 -0
- package/server/linear-oauth-connections.ts +294 -0
- package/server/linear-project-manager.test.ts +162 -0
- package/server/linear-project-manager.ts +111 -0
- package/server/linear-prompt-builder.test.ts +74 -0
- package/server/linear-prompt-builder.ts +61 -0
- package/server/linear-staging.test.ts +276 -0
- package/server/linear-staging.ts +142 -0
- package/server/logger.test.ts +393 -0
- package/server/logger.ts +259 -0
- package/server/metrics-collector.test.ts +413 -0
- package/server/metrics-collector.ts +350 -0
- package/server/metrics-types.ts +108 -0
- package/server/middleware/managed-auth.test.ts +264 -0
- package/server/middleware/managed-auth.ts +195 -0
- package/server/novnc-proxy.test.ts +333 -0
- package/server/novnc-proxy.ts +99 -0
- package/server/path-resolver.test.ts +552 -0
- package/server/path-resolver.ts +186 -0
- package/server/paths.test.ts +31 -0
- package/server/paths.ts +11 -0
- package/server/pr-poller.test.ts +191 -0
- package/server/pr-poller.ts +162 -0
- package/server/prompt-manager.test.ts +211 -0
- package/server/prompt-manager.ts +211 -0
- package/server/protocol/claude-upstream/README.md +19 -0
- package/server/protocol/claude-upstream/sdk.d.ts.txt +1943 -0
- package/server/protocol/codex-upstream/ClientNotification.ts.txt +5 -0
- package/server/protocol/codex-upstream/ClientRequest.ts.txt +60 -0
- package/server/protocol/codex-upstream/README.md +18 -0
- package/server/protocol/codex-upstream/ServerNotification.ts.txt +41 -0
- package/server/protocol/codex-upstream/ServerRequest.ts.txt +16 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallParams.ts.txt +6 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallResponse.ts.txt +6 -0
- package/server/protocol-monitor.ts +50 -0
- package/server/recorder.test.ts +454 -0
- package/server/recorder.ts +374 -0
- package/server/recording-hub/compat-validator.test.ts +150 -0
- package/server/recording-hub/compat-validator.ts +284 -0
- package/server/recording-hub/diagnostics.test.ts +140 -0
- package/server/recording-hub/diagnostics.ts +299 -0
- package/server/recording-hub/hub-config.test.ts +44 -0
- package/server/recording-hub/hub-config.ts +19 -0
- package/server/recording-hub/hub-routes.test.ts +417 -0
- package/server/recording-hub/hub-routes.ts +236 -0
- package/server/recording-hub/hub-store.test.ts +262 -0
- package/server/recording-hub/hub-store.ts +265 -0
- package/server/recording-hub/replay-adapter.test.ts +294 -0
- package/server/recording-hub/replay-adapter.ts +207 -0
- package/server/relay-client.test.ts +337 -0
- package/server/relay-client.ts +320 -0
- package/server/replay.test.ts +200 -0
- package/server/replay.ts +78 -0
- package/server/routes/agent-routes.test.ts +1400 -0
- package/server/routes/agent-routes.ts +409 -0
- package/server/routes/cron-routes.test.ts +881 -0
- package/server/routes/cron-routes.ts +103 -0
- package/server/routes/env-routes.test.ts +383 -0
- package/server/routes/env-routes.ts +95 -0
- package/server/routes/fs-routes.test.ts +1198 -0
- package/server/routes/fs-routes.ts +605 -0
- package/server/routes/git-routes.test.ts +813 -0
- package/server/routes/git-routes.ts +97 -0
- package/server/routes/linear-agent-routes.test.ts +721 -0
- package/server/routes/linear-agent-routes.ts +304 -0
- package/server/routes/linear-connection-routes.test.ts +927 -0
- package/server/routes/linear-connection-routes.ts +244 -0
- package/server/routes/linear-oauth-connection-routes.test.ts +406 -0
- package/server/routes/linear-oauth-connection-routes.ts +129 -0
- package/server/routes/linear-routes.test.ts +1510 -0
- package/server/routes/linear-routes.ts +953 -0
- package/server/routes/metrics-routes.test.ts +103 -0
- package/server/routes/metrics-routes.ts +13 -0
- package/server/routes/prompt-routes.ts +67 -0
- package/server/routes/sandbox-routes.test.ts +513 -0
- package/server/routes/sandbox-routes.ts +127 -0
- package/server/routes/settings-routes.ts +270 -0
- package/server/routes/skills-routes.test.ts +690 -0
- package/server/routes/skills-routes.ts +100 -0
- package/server/routes/system-routes.test.ts +637 -0
- package/server/routes/system-routes.ts +228 -0
- package/server/routes/tailscale-routes.test.ts +176 -0
- package/server/routes/tailscale-routes.ts +22 -0
- package/server/routes.test.ts +4655 -0
- package/server/routes.ts +1277 -0
- package/server/sandbox-manager.test.ts +378 -0
- package/server/sandbox-manager.ts +168 -0
- package/server/service.test.ts +1419 -0
- package/server/service.ts +718 -0
- package/server/session-creation-service.test.ts +661 -0
- package/server/session-creation-service.ts +473 -0
- package/server/session-git-info.ts +104 -0
- package/server/session-linear-issues.test.ts +118 -0
- package/server/session-linear-issues.ts +88 -0
- package/server/session-names.test.ts +94 -0
- package/server/session-names.ts +67 -0
- package/server/session-orchestrator.test.ts +1784 -0
- package/server/session-orchestrator.ts +973 -0
- package/server/session-state-machine.test.ts +606 -0
- package/server/session-state-machine.ts +207 -0
- package/server/session-store.test.ts +290 -0
- package/server/session-store.ts +146 -0
- package/server/session-types.ts +509 -0
- package/server/settings-manager.test.ts +275 -0
- package/server/settings-manager.ts +173 -0
- package/server/tailscale-manager.test.ts +553 -0
- package/server/tailscale-manager.ts +451 -0
- package/server/terminal-manager.ts +240 -0
- package/server/update-checker.test.ts +306 -0
- package/server/update-checker.ts +197 -0
- package/server/usage-limits.test.ts +536 -0
- package/server/usage-limits.ts +225 -0
- package/server/worktree-tracker.test.ts +243 -0
- package/server/worktree-tracker.ts +84 -0
- package/server/ws-auth.test.ts +59 -0
- package/server/ws-auth.ts +41 -0
- package/server/ws-bridge-browser-ingest.test.ts +272 -0
- package/server/ws-bridge-browser-ingest.ts +72 -0
- package/server/ws-bridge-browser.ts +112 -0
- package/server/ws-bridge-cli-ingest.test.ts +302 -0
- package/server/ws-bridge-cli-ingest.ts +81 -0
- package/server/ws-bridge-codex.test.ts +1837 -0
- package/server/ws-bridge-codex.ts +266 -0
- package/server/ws-bridge-controls.test.ts +124 -0
- package/server/ws-bridge-controls.ts +20 -0
- package/server/ws-bridge-persist.test.ts +296 -0
- package/server/ws-bridge-persist.ts +66 -0
- package/server/ws-bridge-publish.test.ts +234 -0
- package/server/ws-bridge-publish.ts +79 -0
- package/server/ws-bridge-replay.test.ts +44 -0
- package/server/ws-bridge-replay.ts +61 -0
- package/server/ws-bridge-types.ts +106 -0
- package/server/ws-bridge.test.ts +4777 -0
- package/server/ws-bridge.ts +1279 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
|
|
6
|
+
// Use a temp directory so tests don't touch the real ~/.companion/auth.json
|
|
7
|
+
const TEST_DIR = join(tmpdir(), `companion-auth-test-${Date.now()}`);
|
|
8
|
+
const TEST_AUTH_FILE = join(TEST_DIR, "auth.json");
|
|
9
|
+
|
|
10
|
+
// Monkey-patch the module's file path before importing
|
|
11
|
+
// We test the exported functions indirectly via env var and file manipulation
|
|
12
|
+
describe("auth-manager", () => {
|
|
13
|
+
let authManager: typeof import("./auth-manager.js");
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
17
|
+
// Clear env var
|
|
18
|
+
delete process.env.COMPANION_AUTH_TOKEN;
|
|
19
|
+
// Re-import with fresh module state
|
|
20
|
+
authManager = await import("./auth-manager.js");
|
|
21
|
+
authManager._resetForTest();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
delete process.env.COMPANION_AUTH_TOKEN;
|
|
26
|
+
try {
|
|
27
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
28
|
+
} catch {
|
|
29
|
+
// cleanup best-effort
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("generates a 64-character hex token", () => {
|
|
34
|
+
// getToken should return a valid hex string
|
|
35
|
+
const token = authManager.getToken();
|
|
36
|
+
expect(token).toMatch(/^[a-f0-9]{64}$/);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("returns the same token on repeated calls", () => {
|
|
40
|
+
// Token should be cached after first generation
|
|
41
|
+
const first = authManager.getToken();
|
|
42
|
+
const second = authManager.getToken();
|
|
43
|
+
expect(first).toBe(second);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("uses COMPANION_AUTH_TOKEN env var when set", () => {
|
|
47
|
+
// Env var should override any persisted or generated token
|
|
48
|
+
process.env.COMPANION_AUTH_TOKEN = "my-custom-token-123";
|
|
49
|
+
authManager._resetForTest();
|
|
50
|
+
expect(authManager.getToken()).toBe("my-custom-token-123");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("verifyToken returns true for correct token", () => {
|
|
54
|
+
const token = authManager.getToken();
|
|
55
|
+
expect(authManager.verifyToken(token)).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("verifyToken returns false for incorrect token", () => {
|
|
59
|
+
authManager.getToken(); // ensure token is generated
|
|
60
|
+
expect(authManager.verifyToken("wrong-token")).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("verifyToken returns false for null/undefined", () => {
|
|
64
|
+
authManager.getToken();
|
|
65
|
+
expect(authManager.verifyToken(null)).toBe(false);
|
|
66
|
+
expect(authManager.verifyToken(undefined)).toBe(false);
|
|
67
|
+
expect(authManager.verifyToken("")).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("verifyToken works with env var token", () => {
|
|
71
|
+
process.env.COMPANION_AUTH_TOKEN = "env-token-abc";
|
|
72
|
+
authManager._resetForTest();
|
|
73
|
+
expect(authManager.verifyToken("env-token-abc")).toBe(true);
|
|
74
|
+
expect(authManager.verifyToken("wrong")).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("getLanAddress returns a string", () => {
|
|
78
|
+
// Should return either an IP address or "localhost"
|
|
79
|
+
const addr = authManager.getLanAddress();
|
|
80
|
+
expect(typeof addr).toBe("string");
|
|
81
|
+
expect(addr.length).toBeGreaterThan(0);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { networkInterfaces } from "node:os";
|
|
6
|
+
|
|
7
|
+
const AUTH_FILE = join(homedir(), ".companion", "auth.json");
|
|
8
|
+
const TOKEN_BYTES = 32; // 64 hex characters
|
|
9
|
+
|
|
10
|
+
interface AuthData {
|
|
11
|
+
token: string;
|
|
12
|
+
createdAt: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let cachedToken: string | null = null;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get the auth token. Priority:
|
|
19
|
+
* 1. COMPANION_AUTH_TOKEN env var
|
|
20
|
+
* 2. Persisted token from ~/.companion/auth.json
|
|
21
|
+
* 3. Auto-generate and persist a new token
|
|
22
|
+
*/
|
|
23
|
+
export function getToken(): string {
|
|
24
|
+
// Env var override (always takes priority)
|
|
25
|
+
const envToken = process.env.COMPANION_AUTH_TOKEN;
|
|
26
|
+
if (envToken && envToken.trim()) {
|
|
27
|
+
cachedToken = envToken.trim();
|
|
28
|
+
return cachedToken;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Return cached token if available
|
|
32
|
+
if (cachedToken) return cachedToken;
|
|
33
|
+
|
|
34
|
+
// Try reading from file
|
|
35
|
+
try {
|
|
36
|
+
if (existsSync(AUTH_FILE)) {
|
|
37
|
+
const raw = readFileSync(AUTH_FILE, "utf-8");
|
|
38
|
+
const data = JSON.parse(raw) as Partial<AuthData>;
|
|
39
|
+
if (typeof data.token === "string" && data.token.length >= 32) {
|
|
40
|
+
cachedToken = data.token;
|
|
41
|
+
return cachedToken;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
// File corrupt or unreadable — generate new
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Generate new token
|
|
49
|
+
const token = randomBytes(TOKEN_BYTES).toString("hex");
|
|
50
|
+
const data: AuthData = { token, createdAt: Date.now() };
|
|
51
|
+
try {
|
|
52
|
+
mkdirSync(dirname(AUTH_FILE), { recursive: true });
|
|
53
|
+
writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error("[auth] Failed to persist auth token:", err);
|
|
56
|
+
}
|
|
57
|
+
cachedToken = token;
|
|
58
|
+
return token;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Verify a candidate token using constant-time comparison.
|
|
63
|
+
*/
|
|
64
|
+
export function verifyToken(candidate: string | null | undefined): boolean {
|
|
65
|
+
if (!candidate) return false;
|
|
66
|
+
const expected = getToken();
|
|
67
|
+
const candidateBuf = Buffer.from(candidate);
|
|
68
|
+
const expectedBuf = Buffer.from(expected);
|
|
69
|
+
if (candidateBuf.length !== expectedBuf.length) return false;
|
|
70
|
+
return timingSafeEqual(candidateBuf, expectedBuf);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get the primary LAN IP address for QR code URL generation.
|
|
75
|
+
* Falls back to "localhost" if no LAN IP is found.
|
|
76
|
+
*/
|
|
77
|
+
export function getLanAddress(): string {
|
|
78
|
+
const interfaces = networkInterfaces();
|
|
79
|
+
for (const name of Object.keys(interfaces)) {
|
|
80
|
+
const addrs = interfaces[name];
|
|
81
|
+
if (!addrs) continue;
|
|
82
|
+
for (const addr of addrs) {
|
|
83
|
+
if (addr.family === "IPv4" && !addr.internal) {
|
|
84
|
+
return addr.address;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return "localhost";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get all available access addresses: localhost, LAN IP, and Tailscale IP.
|
|
93
|
+
* Tailscale uses 100.x.x.x addresses (CGNAT range) on utun / tailscale interfaces.
|
|
94
|
+
*/
|
|
95
|
+
export function getAllAddresses(): { label: string; ip: string }[] {
|
|
96
|
+
const result: { label: string; ip: string }[] = [
|
|
97
|
+
{ label: "Localhost", ip: "localhost" },
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
const interfaces = networkInterfaces();
|
|
101
|
+
let lanIp: string | null = null;
|
|
102
|
+
let tailscaleIp: string | null = null;
|
|
103
|
+
|
|
104
|
+
for (const name of Object.keys(interfaces)) {
|
|
105
|
+
const addrs = interfaces[name];
|
|
106
|
+
if (!addrs) continue;
|
|
107
|
+
for (const addr of addrs) {
|
|
108
|
+
if (addr.family !== "IPv4" || addr.internal) continue;
|
|
109
|
+
|
|
110
|
+
// Tailscale uses 100.64.0.0/10 (CGNAT) — detect by IP range
|
|
111
|
+
if (addr.address.startsWith("100.")) {
|
|
112
|
+
const second = parseInt(addr.address.split(".")[1], 10);
|
|
113
|
+
if (second >= 64 && second <= 127) {
|
|
114
|
+
tailscaleIp = addr.address;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!lanIp) lanIp = addr.address;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (lanIp) result.push({ label: "LAN", ip: lanIp });
|
|
124
|
+
if (tailscaleIp) result.push({ label: "Tailscale", ip: tailscaleIp });
|
|
125
|
+
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Regenerate the auth token — creates a new random token, persists it,
|
|
131
|
+
* and returns the new value. Existing sessions using the old token will
|
|
132
|
+
* be invalidated on their next request.
|
|
133
|
+
*/
|
|
134
|
+
export function regenerateToken(): string {
|
|
135
|
+
const token = randomBytes(TOKEN_BYTES).toString("hex");
|
|
136
|
+
const data: AuthData = { token, createdAt: Date.now() };
|
|
137
|
+
try {
|
|
138
|
+
mkdirSync(dirname(AUTH_FILE), { recursive: true });
|
|
139
|
+
writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
140
|
+
} catch (err) {
|
|
141
|
+
console.error("[auth] Failed to persist regenerated token:", err);
|
|
142
|
+
}
|
|
143
|
+
cachedToken = token;
|
|
144
|
+
return token;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Reset cached state — for testing only */
|
|
148
|
+
export function _resetForTest(): void {
|
|
149
|
+
cachedToken = null;
|
|
150
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { vi, describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("./settings-manager.js", () => ({
|
|
4
|
+
DEFAULT_ANTHROPIC_MODEL: "claude-sonnet-4-6",
|
|
5
|
+
getSettings: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
import { generateSessionTitle } from "./auto-namer.js";
|
|
9
|
+
import * as settingsManager from "./settings-manager.js";
|
|
10
|
+
|
|
11
|
+
const mockFetch = vi.fn();
|
|
12
|
+
vi.stubGlobal("fetch", mockFetch);
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
vi.clearAllMocks();
|
|
16
|
+
vi.mocked(settingsManager.getSettings).mockReturnValue({
|
|
17
|
+
anthropicApiKey: "sk-ant-key",
|
|
18
|
+
anthropicModel: "claude-sonnet-4-6",
|
|
19
|
+
linearApiKey: "",
|
|
20
|
+
linearAutoTransition: false,
|
|
21
|
+
linearAutoTransitionStateId: "",
|
|
22
|
+
linearAutoTransitionStateName: "",
|
|
23
|
+
linearArchiveTransition: false,
|
|
24
|
+
linearArchiveTransitionStateId: "",
|
|
25
|
+
linearArchiveTransitionStateName: "",
|
|
26
|
+
linearOAuthClientId: "",
|
|
27
|
+
linearOAuthClientSecret: "",
|
|
28
|
+
linearOAuthWebhookSecret: "",
|
|
29
|
+
linearOAuthAccessToken: "",
|
|
30
|
+
linearOAuthRefreshToken: "",
|
|
31
|
+
claudeCodeOAuthToken: "",
|
|
32
|
+
openaiApiKey: "",
|
|
33
|
+
onboardingCompleted: false,
|
|
34
|
+
aiValidationEnabled: false,
|
|
35
|
+
aiValidationAutoApprove: true,
|
|
36
|
+
aiValidationAutoDeny: false,
|
|
37
|
+
publicUrl: "",
|
|
38
|
+
updateChannel: "stable",
|
|
39
|
+
dockerAutoUpdate: false,
|
|
40
|
+
updatedAt: 0,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("generateSessionTitle", () => {
|
|
45
|
+
it("returns parsed title from Anthropic response", async () => {
|
|
46
|
+
mockFetch.mockResolvedValueOnce({
|
|
47
|
+
ok: true,
|
|
48
|
+
json: async () => ({
|
|
49
|
+
content: [{ type: "text", text: "Fix Auth Flow" }],
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const title = await generateSessionTitle("Fix login", "claude-sonnet-4-6");
|
|
54
|
+
|
|
55
|
+
expect(title).toBe("Fix Auth Flow");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("returns null when Anthropic key is not configured", async () => {
|
|
59
|
+
vi.mocked(settingsManager.getSettings).mockReturnValue({
|
|
60
|
+
anthropicApiKey: "",
|
|
61
|
+
anthropicModel: "claude-sonnet-4-6",
|
|
62
|
+
linearApiKey: "",
|
|
63
|
+
linearAutoTransition: false,
|
|
64
|
+
linearAutoTransitionStateId: "",
|
|
65
|
+
linearAutoTransitionStateName: "",
|
|
66
|
+
linearArchiveTransition: false,
|
|
67
|
+
linearArchiveTransitionStateId: "",
|
|
68
|
+
linearArchiveTransitionStateName: "",
|
|
69
|
+
linearOAuthClientId: "",
|
|
70
|
+
linearOAuthClientSecret: "",
|
|
71
|
+
linearOAuthWebhookSecret: "",
|
|
72
|
+
linearOAuthAccessToken: "",
|
|
73
|
+
linearOAuthRefreshToken: "",
|
|
74
|
+
claudeCodeOAuthToken: "",
|
|
75
|
+
openaiApiKey: "",
|
|
76
|
+
onboardingCompleted: false,
|
|
77
|
+
aiValidationEnabled: false,
|
|
78
|
+
aiValidationAutoApprove: true,
|
|
79
|
+
aiValidationAutoDeny: false,
|
|
80
|
+
publicUrl: "",
|
|
81
|
+
updateChannel: "stable",
|
|
82
|
+
dockerAutoUpdate: false,
|
|
83
|
+
updatedAt: 0,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const title = await generateSessionTitle("Fix login", "claude-sonnet-4-6");
|
|
87
|
+
|
|
88
|
+
expect(title).toBeNull();
|
|
89
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("truncates message to 500 chars", async () => {
|
|
93
|
+
mockFetch.mockResolvedValueOnce({
|
|
94
|
+
ok: true,
|
|
95
|
+
json: async () => ({ content: [{ type: "text", text: "Short Title" }] }),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
await generateSessionTitle("X".repeat(1000), "claude-sonnet-4-6");
|
|
99
|
+
|
|
100
|
+
const [, req] = mockFetch.mock.calls[0] as [string, RequestInit];
|
|
101
|
+
const body = JSON.parse(String(req.body)) as { messages: Array<{ role: string; content: string }> };
|
|
102
|
+
const user = body.messages.find((m) => m.role === "user");
|
|
103
|
+
expect(user?.content).toContain("Request:");
|
|
104
|
+
expect(user?.content).toContain("X".repeat(500));
|
|
105
|
+
expect(user?.content).not.toContain("X".repeat(501));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("uses configured Anthropic model", async () => {
|
|
109
|
+
vi.mocked(settingsManager.getSettings).mockReturnValue({
|
|
110
|
+
anthropicApiKey: "sk-ant-key",
|
|
111
|
+
anthropicModel: "claude-haiku-3",
|
|
112
|
+
linearApiKey: "",
|
|
113
|
+
linearAutoTransition: false,
|
|
114
|
+
linearAutoTransitionStateId: "",
|
|
115
|
+
linearAutoTransitionStateName: "",
|
|
116
|
+
linearArchiveTransition: false,
|
|
117
|
+
linearArchiveTransitionStateId: "",
|
|
118
|
+
linearArchiveTransitionStateName: "",
|
|
119
|
+
linearOAuthClientId: "",
|
|
120
|
+
linearOAuthClientSecret: "",
|
|
121
|
+
linearOAuthWebhookSecret: "",
|
|
122
|
+
linearOAuthAccessToken: "",
|
|
123
|
+
linearOAuthRefreshToken: "",
|
|
124
|
+
claudeCodeOAuthToken: "",
|
|
125
|
+
openaiApiKey: "",
|
|
126
|
+
onboardingCompleted: false,
|
|
127
|
+
aiValidationEnabled: false,
|
|
128
|
+
aiValidationAutoApprove: true,
|
|
129
|
+
aiValidationAutoDeny: false,
|
|
130
|
+
publicUrl: "",
|
|
131
|
+
updateChannel: "stable",
|
|
132
|
+
dockerAutoUpdate: false,
|
|
133
|
+
updatedAt: 0,
|
|
134
|
+
});
|
|
135
|
+
mockFetch.mockResolvedValueOnce({
|
|
136
|
+
ok: true,
|
|
137
|
+
json: async () => ({ content: [{ type: "text", text: "Title" }] }),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await generateSessionTitle("Fix login", "ignored");
|
|
141
|
+
|
|
142
|
+
const [, req] = mockFetch.mock.calls[0] as [string, RequestInit];
|
|
143
|
+
const body = JSON.parse(String(req.body)) as { model: string };
|
|
144
|
+
expect(body.model).toBe("claude-haiku-3");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("returns null when response is non-ok", async () => {
|
|
148
|
+
mockFetch.mockResolvedValueOnce({ ok: false, status: 401, statusText: "Unauthorized" });
|
|
149
|
+
|
|
150
|
+
const title = await generateSessionTitle("Fix login", "claude-sonnet-4-6");
|
|
151
|
+
|
|
152
|
+
expect(title).toBeNull();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("returns null when fetch throws", async () => {
|
|
156
|
+
mockFetch.mockRejectedValueOnce(new Error("network"));
|
|
157
|
+
|
|
158
|
+
const title = await generateSessionTitle("Fix login", "claude-sonnet-4-6");
|
|
159
|
+
|
|
160
|
+
expect(title).toBeNull();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("strips surrounding quotes from returned title", async () => {
|
|
164
|
+
mockFetch.mockResolvedValueOnce({
|
|
165
|
+
ok: true,
|
|
166
|
+
json: async () => ({
|
|
167
|
+
content: [{ type: "text", text: "\"Refactor API Layer\"" }],
|
|
168
|
+
}),
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const title = await generateSessionTitle("Refactor API", "ignored");
|
|
172
|
+
expect(title).toBe("Refactor API Layer");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("returns null for titles >= 100 chars", async () => {
|
|
176
|
+
mockFetch.mockResolvedValueOnce({
|
|
177
|
+
ok: true,
|
|
178
|
+
json: async () => ({
|
|
179
|
+
content: [{ type: "text", text: "A".repeat(100) }],
|
|
180
|
+
}),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const title = await generateSessionTitle("Do a thing", "ignored");
|
|
184
|
+
expect(title).toBeNull();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("uses default model when configured model is empty", async () => {
|
|
188
|
+
vi.mocked(settingsManager.getSettings).mockReturnValue({
|
|
189
|
+
anthropicApiKey: "sk-ant-key",
|
|
190
|
+
anthropicModel: "",
|
|
191
|
+
linearApiKey: "",
|
|
192
|
+
linearAutoTransition: false,
|
|
193
|
+
linearAutoTransitionStateId: "",
|
|
194
|
+
linearAutoTransitionStateName: "",
|
|
195
|
+
linearArchiveTransition: false,
|
|
196
|
+
linearArchiveTransitionStateId: "",
|
|
197
|
+
linearArchiveTransitionStateName: "",
|
|
198
|
+
linearOAuthClientId: "",
|
|
199
|
+
linearOAuthClientSecret: "",
|
|
200
|
+
linearOAuthWebhookSecret: "",
|
|
201
|
+
linearOAuthAccessToken: "",
|
|
202
|
+
linearOAuthRefreshToken: "",
|
|
203
|
+
claudeCodeOAuthToken: "",
|
|
204
|
+
openaiApiKey: "",
|
|
205
|
+
onboardingCompleted: false,
|
|
206
|
+
aiValidationEnabled: false,
|
|
207
|
+
aiValidationAutoApprove: true,
|
|
208
|
+
aiValidationAutoDeny: false,
|
|
209
|
+
publicUrl: "",
|
|
210
|
+
updateChannel: "stable",
|
|
211
|
+
dockerAutoUpdate: false,
|
|
212
|
+
updatedAt: 0,
|
|
213
|
+
});
|
|
214
|
+
mockFetch.mockResolvedValueOnce({
|
|
215
|
+
ok: true,
|
|
216
|
+
json: async () => ({ content: [{ type: "text", text: "Title" }] }),
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
await generateSessionTitle("Fix login", "ignored");
|
|
220
|
+
|
|
221
|
+
const [, req] = mockFetch.mock.calls[0] as [string, RequestInit];
|
|
222
|
+
const body = JSON.parse(String(req.body)) as { model: string };
|
|
223
|
+
expect(body.model).toBe("claude-sonnet-4-6");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("calls Anthropic endpoint with x-api-key and anthropic-version headers", async () => {
|
|
227
|
+
mockFetch.mockResolvedValueOnce({
|
|
228
|
+
ok: true,
|
|
229
|
+
json: async () => ({ content: [{ type: "text", text: "Title" }] }),
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
await generateSessionTitle("Fix login", "ignored");
|
|
233
|
+
|
|
234
|
+
const [url, req] = mockFetch.mock.calls[0] as [string, RequestInit];
|
|
235
|
+
expect(url).toBe("https://api.anthropic.com/v1/messages");
|
|
236
|
+
expect((req.headers as Record<string, string>)["x-api-key"]).toBe("sk-ant-key");
|
|
237
|
+
expect((req.headers as Record<string, string>)["anthropic-version"]).toBe("2023-06-01");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("includes max_tokens in request body", async () => {
|
|
241
|
+
mockFetch.mockResolvedValueOnce({
|
|
242
|
+
ok: true,
|
|
243
|
+
json: async () => ({ content: [{ type: "text", text: "Title" }] }),
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
await generateSessionTitle("Fix login", "ignored");
|
|
247
|
+
|
|
248
|
+
const [, req] = mockFetch.mock.calls[0] as [string, RequestInit];
|
|
249
|
+
const body = JSON.parse(String(req.body)) as { max_tokens: number };
|
|
250
|
+
expect(body.max_tokens).toBe(256);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { DEFAULT_ANTHROPIC_MODEL, getSettings } from "./settings-manager.js";
|
|
2
|
+
|
|
3
|
+
const ANTHROPIC_URL = "https://api.anthropic.com/v1/messages";
|
|
4
|
+
|
|
5
|
+
function sanitizeTitle(raw: string): string | null {
|
|
6
|
+
const title = raw.replace(/^"|"$/g, "").replace(/^'|'$/g, "").trim();
|
|
7
|
+
if (!title || title.length >= 100) return null;
|
|
8
|
+
return title;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generates a short session title using the Anthropic Messages API.
|
|
13
|
+
* Returns null if Anthropic isn't configured or if generation fails.
|
|
14
|
+
*/
|
|
15
|
+
export async function generateSessionTitle(
|
|
16
|
+
firstUserMessage: string,
|
|
17
|
+
_model: string,
|
|
18
|
+
options?: {
|
|
19
|
+
timeoutMs?: number;
|
|
20
|
+
},
|
|
21
|
+
): Promise<string | null> {
|
|
22
|
+
const timeout = options?.timeoutMs || 15_000;
|
|
23
|
+
const settings = getSettings();
|
|
24
|
+
const apiKey = settings.anthropicApiKey.trim();
|
|
25
|
+
|
|
26
|
+
if (!apiKey) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const model = settings.anthropicModel?.trim() || DEFAULT_ANTHROPIC_MODEL;
|
|
31
|
+
const truncated = firstUserMessage.slice(0, 500);
|
|
32
|
+
const userPrompt = `Generate a concise 3-5 word session title for this user request. Output only the title.\n\nRequest: ${truncated}`;
|
|
33
|
+
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const res = await fetch(ANTHROPIC_URL, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: {
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
"x-api-key": apiKey,
|
|
43
|
+
"anthropic-version": "2023-06-01",
|
|
44
|
+
},
|
|
45
|
+
body: JSON.stringify({
|
|
46
|
+
model,
|
|
47
|
+
max_tokens: 256,
|
|
48
|
+
messages: [
|
|
49
|
+
{
|
|
50
|
+
role: "user",
|
|
51
|
+
content: userPrompt,
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
temperature: 0.2,
|
|
55
|
+
}),
|
|
56
|
+
signal: controller.signal,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
console.warn(`[auto-namer] Anthropic request failed: ${res.status} ${res.statusText}`);
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const data = await res.json() as {
|
|
65
|
+
content?: Array<{ type: string; text?: string }>;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const raw = data.content?.[0]?.type === "text"
|
|
69
|
+
? (data.content[0].text ?? "")
|
|
70
|
+
: "";
|
|
71
|
+
return sanitizeTitle(raw);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.warn("[auto-namer] Failed to generate session title via Anthropic:", err);
|
|
74
|
+
return null;
|
|
75
|
+
} finally {
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
// Bare side-effect import so the coverage tool sees the module as "loaded".
|
|
4
|
+
// A type-only import (`import type { ... }`) is erased at compile-time and
|
|
5
|
+
// does not count towards coverage.
|
|
6
|
+
import "./backend-adapter.js";
|
|
7
|
+
|
|
8
|
+
import type { IBackendAdapter } from "./backend-adapter.js";
|
|
9
|
+
|
|
10
|
+
describe("IBackendAdapter interface", () => {
|
|
11
|
+
it("exists as a type-only export (no runtime code to test)", () => {
|
|
12
|
+
// IBackendAdapter is a pure TypeScript interface — it compiles away to
|
|
13
|
+
// nothing at runtime. This test exists solely so the coverage gate sees
|
|
14
|
+
// at least one test file importing the module.
|
|
15
|
+
const satisfiesInterface: boolean = true;
|
|
16
|
+
expect(satisfiesInterface).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("can be structurally satisfied by a mock object", () => {
|
|
20
|
+
// Verify the interface shape is valid by creating a mock that satisfies it.
|
|
21
|
+
// This acts as a compile-time check: if the interface changes, this test
|
|
22
|
+
// will fail to compile, alerting us to update downstream adapters.
|
|
23
|
+
const mock: IBackendAdapter = {
|
|
24
|
+
send: () => true,
|
|
25
|
+
isConnected: () => false,
|
|
26
|
+
disconnect: async () => {},
|
|
27
|
+
onBrowserMessage: () => {},
|
|
28
|
+
onSessionMeta: () => {},
|
|
29
|
+
onDisconnect: () => {},
|
|
30
|
+
};
|
|
31
|
+
expect(mock.send({ type: "user_message", content: "hi" })).toBe(true);
|
|
32
|
+
expect(mock.isConnected()).toBe(false);
|
|
33
|
+
expect(typeof mock.disconnect).toBe("function");
|
|
34
|
+
expect(typeof mock.onBrowserMessage).toBe("function");
|
|
35
|
+
expect(typeof mock.onSessionMeta).toBe("function");
|
|
36
|
+
expect(typeof mock.onDisconnect).toBe("function");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { BrowserIncomingMessage, BrowserOutgoingMessage } from "./session-types.js";
|
|
2
|
+
|
|
3
|
+
// ─── Unified Backend Adapter Interface ───────────────────────────────────────
|
|
4
|
+
// Both Claude Code (NDJSON WebSocket) and Codex (JSON-RPC stdio/WS) implement
|
|
5
|
+
// this so that application code never branches on BackendType for message routing.
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Unified interface for backend communication.
|
|
9
|
+
*
|
|
10
|
+
* Adapters translate between the backend-native protocol and the common
|
|
11
|
+
* BrowserIncomingMessage / BrowserOutgoingMessage types used by the bridge
|
|
12
|
+
* and the frontend.
|
|
13
|
+
*/
|
|
14
|
+
export interface IBackendAdapter {
|
|
15
|
+
/** Send a browser-originated message to the backend. Returns true if accepted. */
|
|
16
|
+
send(msg: BrowserOutgoingMessage): boolean;
|
|
17
|
+
|
|
18
|
+
/** Whether the backend transport is currently connected and ready. */
|
|
19
|
+
isConnected(): boolean;
|
|
20
|
+
|
|
21
|
+
/** Gracefully disconnect the backend transport. */
|
|
22
|
+
disconnect(): Promise<void>;
|
|
23
|
+
|
|
24
|
+
// ── Event registration (called once at attachment time) ──
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Register callback for messages to forward to browsers.
|
|
28
|
+
* The adapter translates backend-native protocol into BrowserIncomingMessage.
|
|
29
|
+
*/
|
|
30
|
+
onBrowserMessage(cb: (msg: BrowserIncomingMessage) => void): void;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Register callback for session metadata updates (CLI session ID, model, cwd).
|
|
34
|
+
* Used for --resume tracking and state synchronization.
|
|
35
|
+
*/
|
|
36
|
+
onSessionMeta(cb: (meta: { cliSessionId?: string; model?: string; cwd?: string }) => void): void;
|
|
37
|
+
|
|
38
|
+
/** Register callback for transport disconnection. */
|
|
39
|
+
onDisconnect(cb: () => void): void;
|
|
40
|
+
|
|
41
|
+
/** Register callback for initialization errors. */
|
|
42
|
+
onInitError?(cb: (error: string) => void): void;
|
|
43
|
+
|
|
44
|
+
// ── Optional capabilities (not all backends support these) ──
|
|
45
|
+
|
|
46
|
+
/** Return backend-specific rate limits, if available (Codex only). */
|
|
47
|
+
getRateLimits?(): {
|
|
48
|
+
primary: { usedPercent: number; windowDurationMins: number; resetsAt: number } | null;
|
|
49
|
+
secondary: { usedPercent: number; windowDurationMins: number; resetsAt: number } | null;
|
|
50
|
+
} | null;
|
|
51
|
+
|
|
52
|
+
/** Handle transport-level close (used when WS proxy drops). */
|
|
53
|
+
handleTransportClose?(): void;
|
|
54
|
+
}
|