@pix3/agent-bridge 0.2.0 → 0.2.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/README.md +75 -75
- package/dist/cli.js +171 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.js +140 -0
- package/dist/config.js.map +1 -0
- package/dist/index.js +293 -0
- package/dist/index.js.map +1 -0
- package/dist/proxy.js +45 -0
- package/dist/proxy.js.map +1 -0
- package/dist/sessions.js +517 -0
- package/dist/sessions.js.map +1 -0
- package/dist/wire.js +121 -0
- package/dist/wire.js.map +1 -0
- package/package.json +47 -38
- package/src/cli.ts +195 -195
- package/src/config.ts +179 -179
- package/src/index.ts +350 -350
- package/src/proxy.ts +68 -68
- package/src/sessions.ts +597 -597
- package/src/wire.ts +161 -161
- package/tsconfig.json +16 -16
package/src/config.ts
CHANGED
|
@@ -1,179 +1,179 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Persistent configuration for Pix3AgentBridge, stored in `~/.pix3/agent-bridge.json`.
|
|
3
|
-
*
|
|
4
|
-
* Beyond the pairing token / port / origins the bridge always had, this now holds the **provider
|
|
5
|
-
* table**: for each metered LLM provider (OpenAI, Anthropic API, OpenCode Zen, or a user-defined
|
|
6
|
-
* OpenAI-compatible endpoint) the upstream base URL, the API key, and an enabled flag. Keys live
|
|
7
|
-
* ONLY here on the user's machine — the editor never receives them; it authenticates to the bridge
|
|
8
|
-
* with the pairing token and the bridge injects the real key when forwarding upstream.
|
|
9
|
-
*
|
|
10
|
-
* The provider table is managed through the CLI (`pix3-agent-bridge provider add|remove|enable|…`),
|
|
11
|
-
* never by hand-editing (though the file is plain JSON if you must).
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { randomBytes } from 'node:crypto';
|
|
15
|
-
import fs from 'node:fs';
|
|
16
|
-
import os from 'node:os';
|
|
17
|
-
import path from 'node:path';
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Provider ids the bridge reserves for its own intrinsic lanes (e.g. the Agent-SDK lane exposed as
|
|
21
|
-
* `claude-bridge` in discovery). The CLI rejects adding a provider under one of these so `GET
|
|
22
|
-
* /v1/providers` can never emit duplicate ids.
|
|
23
|
-
*/
|
|
24
|
-
export const RESERVED_PROVIDER_IDS = ['claude-bridge'] as const;
|
|
25
|
-
|
|
26
|
-
/** Upstream auth scheme: how the bridge presents the stored key to the provider. */
|
|
27
|
-
export type ProviderKind = 'openai' | 'anthropic';
|
|
28
|
-
|
|
29
|
-
export interface ProviderConfig {
|
|
30
|
-
/**
|
|
31
|
-
* `openai` → forward `Authorization: Bearer <key>` (OpenAI Chat Completions, OpenCode Zen, most
|
|
32
|
-
* gateways, local Ollama/LM Studio). `anthropic` → forward `x-api-key: <key>` + `anthropic-version`
|
|
33
|
-
* (native Anthropic Messages API).
|
|
34
|
-
*/
|
|
35
|
-
kind: ProviderKind;
|
|
36
|
-
/** Human label shown in the editor picker and `provider list`. */
|
|
37
|
-
label: string;
|
|
38
|
-
/** Upstream base URL INCLUDING the version segment, e.g. `https://api.openai.com/v1`. */
|
|
39
|
-
baseUrl: string;
|
|
40
|
-
/** Provider API key. Empty string = not yet configured (provider stays effectively unusable). */
|
|
41
|
-
apiKey: string;
|
|
42
|
-
/** When false the provider is hidden from discovery and its proxy route 404s. */
|
|
43
|
-
enabled: boolean;
|
|
44
|
-
/** True for the shipped presets (openai/anthropic/opencode-zen/cerebras); false for user-added. */
|
|
45
|
-
builtin?: boolean;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface BridgeConfig {
|
|
49
|
-
token: string;
|
|
50
|
-
port: number;
|
|
51
|
-
origins: string[];
|
|
52
|
-
providers: Record<string, ProviderConfig>;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Shipped presets: `provider add <id> --key <k>` for one of these fills in kind/label/baseUrl so the
|
|
57
|
-
* common providers are one command away. A custom OpenAI-compatible endpoint is added with an
|
|
58
|
-
* arbitrary id plus an explicit `--base-url`.
|
|
59
|
-
*/
|
|
60
|
-
export const PROVIDER_PRESETS: Record<string, Omit<ProviderConfig, 'apiKey' | 'enabled'>> = {
|
|
61
|
-
openai: {
|
|
62
|
-
kind: 'openai',
|
|
63
|
-
label: 'OpenAI',
|
|
64
|
-
baseUrl: 'https://api.openai.com/v1',
|
|
65
|
-
builtin: true,
|
|
66
|
-
},
|
|
67
|
-
anthropic: {
|
|
68
|
-
kind: 'anthropic',
|
|
69
|
-
label: 'Anthropic (Claude)',
|
|
70
|
-
baseUrl: 'https://api.anthropic.com/v1',
|
|
71
|
-
builtin: true,
|
|
72
|
-
},
|
|
73
|
-
'opencode-zen': {
|
|
74
|
-
kind: 'openai',
|
|
75
|
-
label: 'OpenCode Zen',
|
|
76
|
-
baseUrl: 'https://opencode.ai/zen/v1',
|
|
77
|
-
builtin: true,
|
|
78
|
-
},
|
|
79
|
-
cerebras: {
|
|
80
|
-
kind: 'openai',
|
|
81
|
-
label: 'Cerebras',
|
|
82
|
-
baseUrl: 'https://api.cerebras.ai/v1',
|
|
83
|
-
builtin: true,
|
|
84
|
-
},
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
export const DEFAULT_PORT = 8484;
|
|
88
|
-
|
|
89
|
-
export const DEFAULT_ORIGINS = [
|
|
90
|
-
'https://editor.pix3.dev',
|
|
91
|
-
'https://cloud.pix3.dev',
|
|
92
|
-
'http://localhost:8123',
|
|
93
|
-
'http://127.0.0.1:8123',
|
|
94
|
-
];
|
|
95
|
-
|
|
96
|
-
const CONFIG_DIR = path.join(os.homedir(), '.pix3');
|
|
97
|
-
const CONFIG_PATH = path.join(CONFIG_DIR, 'agent-bridge.json');
|
|
98
|
-
/** Pre-rename config file. Read once to carry the existing pairing token forward on first run. */
|
|
99
|
-
const LEGACY_CONFIG_PATH = path.join(CONFIG_DIR, 'claude-bridge.json');
|
|
100
|
-
|
|
101
|
-
export const configPath = (): string => CONFIG_PATH;
|
|
102
|
-
|
|
103
|
-
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
104
|
-
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
105
|
-
|
|
106
|
-
const parseProviders = (raw: unknown): Record<string, ProviderConfig> => {
|
|
107
|
-
if (!isRecord(raw)) return {};
|
|
108
|
-
const providers: Record<string, ProviderConfig> = {};
|
|
109
|
-
for (const [id, value] of Object.entries(raw)) {
|
|
110
|
-
if (!isRecord(value)) continue;
|
|
111
|
-
const kind = value.kind === 'anthropic' ? 'anthropic' : 'openai';
|
|
112
|
-
if (typeof value.baseUrl !== 'string' || !value.baseUrl) continue;
|
|
113
|
-
providers[id] = {
|
|
114
|
-
kind,
|
|
115
|
-
label: typeof value.label === 'string' && value.label ? value.label : id,
|
|
116
|
-
baseUrl: value.baseUrl.replace(/\/$/, ''),
|
|
117
|
-
apiKey: typeof value.apiKey === 'string' ? value.apiKey : '',
|
|
118
|
-
enabled: value.enabled !== false,
|
|
119
|
-
builtin: value.builtin === true,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
return providers;
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
const readLegacyToken = (): string | null => {
|
|
126
|
-
try {
|
|
127
|
-
const legacy = JSON.parse(fs.readFileSync(LEGACY_CONFIG_PATH, 'utf8')) as unknown;
|
|
128
|
-
if (isRecord(legacy) && typeof legacy.token === 'string' && legacy.token.length >= 16) {
|
|
129
|
-
return legacy.token;
|
|
130
|
-
}
|
|
131
|
-
} catch {
|
|
132
|
-
/* no legacy file */
|
|
133
|
-
}
|
|
134
|
-
return null;
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
/** Load config, creating the file (and a fresh pairing token) on first run. */
|
|
138
|
-
export const loadConfig = (): BridgeConfig => {
|
|
139
|
-
let stored: Partial<BridgeConfig> = {};
|
|
140
|
-
let existed = false;
|
|
141
|
-
try {
|
|
142
|
-
stored = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')) as Partial<BridgeConfig>;
|
|
143
|
-
existed = true;
|
|
144
|
-
} catch {
|
|
145
|
-
/* first run */
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const config: BridgeConfig = {
|
|
149
|
-
token:
|
|
150
|
-
typeof stored.token === 'string' && stored.token.length >= 16
|
|
151
|
-
? stored.token
|
|
152
|
-
: (readLegacyToken() ?? randomBytes(24).toString('base64url')),
|
|
153
|
-
port: typeof stored.port === 'number' ? stored.port : DEFAULT_PORT,
|
|
154
|
-
origins: [
|
|
155
|
-
...DEFAULT_ORIGINS,
|
|
156
|
-
...(Array.isArray(stored.origins) ? stored.origins.filter(o => typeof o === 'string') : []),
|
|
157
|
-
],
|
|
158
|
-
providers: parseProviders(stored.providers),
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
// Persist on first run (or after a legacy-token migration) so the token is stable across restarts.
|
|
162
|
-
if (!existed || !stored.token) {
|
|
163
|
-
saveConfig(config);
|
|
164
|
-
}
|
|
165
|
-
return config;
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
/** Persist only the durable fields (token, port, custom origins, providers) — never the merged defaults. */
|
|
169
|
-
export const saveConfig = (config: BridgeConfig): void => {
|
|
170
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
171
|
-
const customOrigins = config.origins.filter(o => !DEFAULT_ORIGINS.includes(o));
|
|
172
|
-
const persisted = {
|
|
173
|
-
token: config.token,
|
|
174
|
-
...(config.port !== DEFAULT_PORT ? { port: config.port } : {}),
|
|
175
|
-
...(customOrigins.length > 0 ? { origins: customOrigins } : {}),
|
|
176
|
-
providers: config.providers,
|
|
177
|
-
};
|
|
178
|
-
fs.writeFileSync(CONFIG_PATH, `${JSON.stringify(persisted, null, 2)}\n`, 'utf8');
|
|
179
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Persistent configuration for Pix3AgentBridge, stored in `~/.pix3/agent-bridge.json`.
|
|
3
|
+
*
|
|
4
|
+
* Beyond the pairing token / port / origins the bridge always had, this now holds the **provider
|
|
5
|
+
* table**: for each metered LLM provider (OpenAI, Anthropic API, OpenCode Zen, or a user-defined
|
|
6
|
+
* OpenAI-compatible endpoint) the upstream base URL, the API key, and an enabled flag. Keys live
|
|
7
|
+
* ONLY here on the user's machine — the editor never receives them; it authenticates to the bridge
|
|
8
|
+
* with the pairing token and the bridge injects the real key when forwarding upstream.
|
|
9
|
+
*
|
|
10
|
+
* The provider table is managed through the CLI (`pix3-agent-bridge provider add|remove|enable|…`),
|
|
11
|
+
* never by hand-editing (though the file is plain JSON if you must).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { randomBytes } from 'node:crypto';
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import os from 'node:os';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Provider ids the bridge reserves for its own intrinsic lanes (e.g. the Agent-SDK lane exposed as
|
|
21
|
+
* `claude-bridge` in discovery). The CLI rejects adding a provider under one of these so `GET
|
|
22
|
+
* /v1/providers` can never emit duplicate ids.
|
|
23
|
+
*/
|
|
24
|
+
export const RESERVED_PROVIDER_IDS = ['claude-bridge'] as const;
|
|
25
|
+
|
|
26
|
+
/** Upstream auth scheme: how the bridge presents the stored key to the provider. */
|
|
27
|
+
export type ProviderKind = 'openai' | 'anthropic';
|
|
28
|
+
|
|
29
|
+
export interface ProviderConfig {
|
|
30
|
+
/**
|
|
31
|
+
* `openai` → forward `Authorization: Bearer <key>` (OpenAI Chat Completions, OpenCode Zen, most
|
|
32
|
+
* gateways, local Ollama/LM Studio). `anthropic` → forward `x-api-key: <key>` + `anthropic-version`
|
|
33
|
+
* (native Anthropic Messages API).
|
|
34
|
+
*/
|
|
35
|
+
kind: ProviderKind;
|
|
36
|
+
/** Human label shown in the editor picker and `provider list`. */
|
|
37
|
+
label: string;
|
|
38
|
+
/** Upstream base URL INCLUDING the version segment, e.g. `https://api.openai.com/v1`. */
|
|
39
|
+
baseUrl: string;
|
|
40
|
+
/** Provider API key. Empty string = not yet configured (provider stays effectively unusable). */
|
|
41
|
+
apiKey: string;
|
|
42
|
+
/** When false the provider is hidden from discovery and its proxy route 404s. */
|
|
43
|
+
enabled: boolean;
|
|
44
|
+
/** True for the shipped presets (openai/anthropic/opencode-zen/cerebras); false for user-added. */
|
|
45
|
+
builtin?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface BridgeConfig {
|
|
49
|
+
token: string;
|
|
50
|
+
port: number;
|
|
51
|
+
origins: string[];
|
|
52
|
+
providers: Record<string, ProviderConfig>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Shipped presets: `provider add <id> --key <k>` for one of these fills in kind/label/baseUrl so the
|
|
57
|
+
* common providers are one command away. A custom OpenAI-compatible endpoint is added with an
|
|
58
|
+
* arbitrary id plus an explicit `--base-url`.
|
|
59
|
+
*/
|
|
60
|
+
export const PROVIDER_PRESETS: Record<string, Omit<ProviderConfig, 'apiKey' | 'enabled'>> = {
|
|
61
|
+
openai: {
|
|
62
|
+
kind: 'openai',
|
|
63
|
+
label: 'OpenAI',
|
|
64
|
+
baseUrl: 'https://api.openai.com/v1',
|
|
65
|
+
builtin: true,
|
|
66
|
+
},
|
|
67
|
+
anthropic: {
|
|
68
|
+
kind: 'anthropic',
|
|
69
|
+
label: 'Anthropic (Claude)',
|
|
70
|
+
baseUrl: 'https://api.anthropic.com/v1',
|
|
71
|
+
builtin: true,
|
|
72
|
+
},
|
|
73
|
+
'opencode-zen': {
|
|
74
|
+
kind: 'openai',
|
|
75
|
+
label: 'OpenCode Zen',
|
|
76
|
+
baseUrl: 'https://opencode.ai/zen/v1',
|
|
77
|
+
builtin: true,
|
|
78
|
+
},
|
|
79
|
+
cerebras: {
|
|
80
|
+
kind: 'openai',
|
|
81
|
+
label: 'Cerebras',
|
|
82
|
+
baseUrl: 'https://api.cerebras.ai/v1',
|
|
83
|
+
builtin: true,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const DEFAULT_PORT = 8484;
|
|
88
|
+
|
|
89
|
+
export const DEFAULT_ORIGINS = [
|
|
90
|
+
'https://editor.pix3.dev',
|
|
91
|
+
'https://cloud.pix3.dev',
|
|
92
|
+
'http://localhost:8123',
|
|
93
|
+
'http://127.0.0.1:8123',
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
const CONFIG_DIR = path.join(os.homedir(), '.pix3');
|
|
97
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, 'agent-bridge.json');
|
|
98
|
+
/** Pre-rename config file. Read once to carry the existing pairing token forward on first run. */
|
|
99
|
+
const LEGACY_CONFIG_PATH = path.join(CONFIG_DIR, 'claude-bridge.json');
|
|
100
|
+
|
|
101
|
+
export const configPath = (): string => CONFIG_PATH;
|
|
102
|
+
|
|
103
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
104
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
105
|
+
|
|
106
|
+
const parseProviders = (raw: unknown): Record<string, ProviderConfig> => {
|
|
107
|
+
if (!isRecord(raw)) return {};
|
|
108
|
+
const providers: Record<string, ProviderConfig> = {};
|
|
109
|
+
for (const [id, value] of Object.entries(raw)) {
|
|
110
|
+
if (!isRecord(value)) continue;
|
|
111
|
+
const kind = value.kind === 'anthropic' ? 'anthropic' : 'openai';
|
|
112
|
+
if (typeof value.baseUrl !== 'string' || !value.baseUrl) continue;
|
|
113
|
+
providers[id] = {
|
|
114
|
+
kind,
|
|
115
|
+
label: typeof value.label === 'string' && value.label ? value.label : id,
|
|
116
|
+
baseUrl: value.baseUrl.replace(/\/$/, ''),
|
|
117
|
+
apiKey: typeof value.apiKey === 'string' ? value.apiKey : '',
|
|
118
|
+
enabled: value.enabled !== false,
|
|
119
|
+
builtin: value.builtin === true,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return providers;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const readLegacyToken = (): string | null => {
|
|
126
|
+
try {
|
|
127
|
+
const legacy = JSON.parse(fs.readFileSync(LEGACY_CONFIG_PATH, 'utf8')) as unknown;
|
|
128
|
+
if (isRecord(legacy) && typeof legacy.token === 'string' && legacy.token.length >= 16) {
|
|
129
|
+
return legacy.token;
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
/* no legacy file */
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/** Load config, creating the file (and a fresh pairing token) on first run. */
|
|
138
|
+
export const loadConfig = (): BridgeConfig => {
|
|
139
|
+
let stored: Partial<BridgeConfig> = {};
|
|
140
|
+
let existed = false;
|
|
141
|
+
try {
|
|
142
|
+
stored = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')) as Partial<BridgeConfig>;
|
|
143
|
+
existed = true;
|
|
144
|
+
} catch {
|
|
145
|
+
/* first run */
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const config: BridgeConfig = {
|
|
149
|
+
token:
|
|
150
|
+
typeof stored.token === 'string' && stored.token.length >= 16
|
|
151
|
+
? stored.token
|
|
152
|
+
: (readLegacyToken() ?? randomBytes(24).toString('base64url')),
|
|
153
|
+
port: typeof stored.port === 'number' ? stored.port : DEFAULT_PORT,
|
|
154
|
+
origins: [
|
|
155
|
+
...DEFAULT_ORIGINS,
|
|
156
|
+
...(Array.isArray(stored.origins) ? stored.origins.filter(o => typeof o === 'string') : []),
|
|
157
|
+
],
|
|
158
|
+
providers: parseProviders(stored.providers),
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Persist on first run (or after a legacy-token migration) so the token is stable across restarts.
|
|
162
|
+
if (!existed || !stored.token) {
|
|
163
|
+
saveConfig(config);
|
|
164
|
+
}
|
|
165
|
+
return config;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/** Persist only the durable fields (token, port, custom origins, providers) — never the merged defaults. */
|
|
169
|
+
export const saveConfig = (config: BridgeConfig): void => {
|
|
170
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
171
|
+
const customOrigins = config.origins.filter(o => !DEFAULT_ORIGINS.includes(o));
|
|
172
|
+
const persisted = {
|
|
173
|
+
token: config.token,
|
|
174
|
+
...(config.port !== DEFAULT_PORT ? { port: config.port } : {}),
|
|
175
|
+
...(customOrigins.length > 0 ? { origins: customOrigins } : {}),
|
|
176
|
+
providers: config.providers,
|
|
177
|
+
};
|
|
178
|
+
fs.writeFileSync(CONFIG_PATH, `${JSON.stringify(persisted, null, 2)}\n`, 'utf8');
|
|
179
|
+
};
|