@gakim-digital/dexter-bridge 0.5.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/src/cli.js ADDED
@@ -0,0 +1,337 @@
1
+ import os from 'node:os';
2
+ import readline from 'node:readline/promises';
3
+ import {
4
+ BRIDGE_CAPABILITIES,
5
+ clearConfig,
6
+ defaultConfigDir,
7
+ normalizeAgentName,
8
+ readConfig,
9
+ resolveAgentName,
10
+ resolveApiBaseUrl,
11
+ resolveCompanionModelName,
12
+ saveConfigPatch,
13
+ } from './config.js';
14
+ import { checkAllAgents, executeRun } from './agent.js';
15
+ import { claimPairing, heartbeat, pollRun } from './api.js';
16
+ import {
17
+ createRunLogger,
18
+ errorMeta,
19
+ logLocationHint,
20
+ } from './logger.js';
21
+ import { createLocalAgentAdapter } from './providers/index.js';
22
+
23
+ function usage() {
24
+ return [
25
+ 'Dexter Bridge',
26
+ '',
27
+ 'Usage:',
28
+ ' dexter-bridge connect [code-or-token] [--agent claude-code|codex|dry-run] [--model <model>] [--once] [--api <url>]',
29
+ ' dexter-bridge pair <code-or-token> [--api <url>]',
30
+ ' dexter-bridge start [--agent claude-code|codex|dry-run] [--model <model>] [--once] [--api <url>]',
31
+ ' dexter-bridge status [--api <url>]',
32
+ ' dexter-bridge doctor',
33
+ ' dexter-bridge logout',
34
+ '',
35
+ 'Environment:',
36
+ ' DEXTER_API_URL API base URL, e.g. https://api.example.com/iwm-api/0.0.1',
37
+ ' DEXTER_BRIDGE_AGENT claude-code, codex, or dry-run',
38
+ ' DEXTER_BRIDGE_MODEL Companion model, e.g. claude-code:sonnet or codex:gpt-5.5',
39
+ ' DEXTER_BRIDGE_CLAUDE_BIN Claude CLI binary name/path',
40
+ ' DEXTER_BRIDGE_CLAUDE_ARGS Claude CLI args, default: -p',
41
+ ' DEXTER_BRIDGE_CODEX_BIN Codex CLI binary name/path',
42
+ ' DEXTER_BRIDGE_CODEX_ARGS Legacy codex exec args used only when App Server is disabled',
43
+ ' DEXTER_BRIDGE_CODEX_APP_SERVER Set false to force the legacy codex exec path',
44
+ ' DEXTER_BRIDGE_CODEX_MAX_THREADS Maximum retained App Server threads, default: 32',
45
+ ' DEXTER_BRIDGE_LOG_DIR Log directory, default: ~/.dexter-bridge/logs',
46
+ ].join('\n');
47
+ }
48
+
49
+ function parseArgv(argv) {
50
+ const flags = {};
51
+ const positional = [];
52
+ for (let i = 0; i < argv.length; i += 1) {
53
+ const arg = argv[i];
54
+ if (!arg.startsWith('--')) {
55
+ positional.push(arg);
56
+ continue;
57
+ }
58
+ const key = arg.slice(2);
59
+ if (key === 'once' || key === 'help') {
60
+ flags[key] = true;
61
+ continue;
62
+ }
63
+ flags[key] = argv[i + 1];
64
+ i += 1;
65
+ }
66
+ return {
67
+ command: positional[0] || 'start',
68
+ args: positional.slice(1),
69
+ flags,
70
+ };
71
+ }
72
+
73
+ function requireDeviceToken(config) {
74
+ if (!config.deviceToken) {
75
+ const error = new Error('Dexter Bridge is not paired. Run `dexter-bridge pair <code>` from Dexter Settings.');
76
+ error.exitCode = 2;
77
+ throw error;
78
+ }
79
+ return config.deviceToken;
80
+ }
81
+
82
+ async function promptForPairingCode() {
83
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
84
+ try {
85
+ const answer = await rl.question('Paste the pairing code from the Dexter plugin and press Enter: ');
86
+ return answer.trim();
87
+ } catch {
88
+ return '';
89
+ } finally {
90
+ rl.close();
91
+ }
92
+ }
93
+
94
+ function deviceName() {
95
+ return process.env.DEXTER_BRIDGE_DEVICE_NAME || `${os.hostname()} (${process.platform})`;
96
+ }
97
+
98
+ function pollBackoffMs(failureCount, baseMs = 1000, maxMs = 30000) {
99
+ const failures = Math.max(1, Number(failureCount) || 1);
100
+ const base = Math.max(100, Number(baseMs) || 1000);
101
+ const maximum = Math.max(base, Number(maxMs) || 30000);
102
+ return Math.min(maximum, base * (2 ** Math.min(10, failures - 1)));
103
+ }
104
+
105
+ function wait(delayMs) {
106
+ return new Promise((resolve) => setTimeout(resolve, delayMs));
107
+ }
108
+
109
+ function agentEnvironment(config = {}, baseEnv = process.env) {
110
+ const env = { ...baseEnv };
111
+ const commands = config.agentCommands && typeof config.agentCommands === 'object'
112
+ ? config.agentCommands
113
+ : {};
114
+ if (!env.DEXTER_BRIDGE_CLAUDE_BIN && commands['claude-code']) {
115
+ env.DEXTER_BRIDGE_CLAUDE_BIN = commands['claude-code'];
116
+ }
117
+ if (!env.DEXTER_BRIDGE_CODEX_BIN && commands.codex) {
118
+ env.DEXTER_BRIDGE_CODEX_BIN = commands.codex;
119
+ }
120
+ return env;
121
+ }
122
+
123
+ async function inspectAvailability(config = {}) {
124
+ try {
125
+ const checks = await checkAllAgents({ env: agentEnvironment(config) });
126
+ const available = checks.agents.filter((check) => check.ok);
127
+ return {
128
+ metadata: {
129
+ availableAgents: available.map((check) => check.agent).join(','),
130
+ availableModels: available.flatMap((check) => check.models || []).join(','),
131
+ agentVersions: available.map((check) => `${check.agent}=${check.version || 'unknown'}`).join(','),
132
+ bridgeCapabilities: BRIDGE_CAPABILITIES.join(','),
133
+ },
134
+ agentCommands: Object.fromEntries(
135
+ available
136
+ .filter((check) => typeof check.command === 'string' && check.command.trim())
137
+ .map((check) => [check.agent, check.command]),
138
+ ),
139
+ };
140
+ } catch {
141
+ return {
142
+ metadata: { bridgeCapabilities: BRIDGE_CAPABILITIES.join(',') },
143
+ agentCommands: {},
144
+ };
145
+ }
146
+ }
147
+
148
+ async function availabilityMetadata(config = {}) {
149
+ return (await inspectAvailability(config)).metadata;
150
+ }
151
+
152
+ async function pairCommand({ apiBaseUrl, args, flags, configDir }) {
153
+ const codeOrToken = args[0];
154
+ if (!codeOrToken) throw new Error('Pairing code or token is required.');
155
+ const isToken = /^dcpp_/i.test(codeOrToken);
156
+ const config = readConfig(configDir);
157
+ const agent = resolveAgentName({ flagValue: flags.agent, config });
158
+ const model = resolveCompanionModelName({ flagValue: flags.model, config, agent });
159
+ const availability = await inspectAvailability(config);
160
+ const result = await claimPairing(apiBaseUrl, {
161
+ pairingCode: isToken ? undefined : codeOrToken,
162
+ pairingToken: isToken ? codeOrToken : undefined,
163
+ deviceName: flags['device-name'] || deviceName(),
164
+ agent,
165
+ model,
166
+ metadata: availability.metadata,
167
+ });
168
+ saveConfigPatch({
169
+ apiBaseUrl,
170
+ agent,
171
+ model,
172
+ deviceToken: result.deviceToken,
173
+ device: result.device,
174
+ agentCommands: availability.agentCommands,
175
+ pairedAt: new Date().toISOString(),
176
+ }, configDir);
177
+ console.log(`Paired Dexter Bridge: ${result.device?.name || 'device'}`);
178
+ console.log(`API: ${apiBaseUrl}`);
179
+ }
180
+
181
+ async function statusCommand({ apiBaseUrl, config }) {
182
+ const deviceToken = requireDeviceToken(config);
183
+ const agent = resolveAgentName({ config });
184
+ const model = resolveCompanionModelName({ config, agent });
185
+ const result = await heartbeat(apiBaseUrl, {
186
+ deviceToken,
187
+ status: 'ready',
188
+ agent,
189
+ model,
190
+ metadata: await availabilityMetadata(config),
191
+ });
192
+ console.log(`Status: ${result.device?.online ? 'online' : 'paired'}`);
193
+ console.log(`Device: ${result.device?.name || 'Dexter Bridge'}`);
194
+ console.log(`Model: ${result.device?.model?.displayName || model}`);
195
+ console.log(`API: ${apiBaseUrl}`);
196
+ }
197
+
198
+ async function startCommand({ apiBaseUrl, config, flags, configDir }) {
199
+ let deviceToken = config.deviceToken;
200
+ if (!deviceToken) {
201
+ // First run: pair interactively so `npx @gakim-digital/dexter-bridge` alone is
202
+ // enough — the Dexter plugin shows the code, the user pastes it here.
203
+ if (!process.stdin.isTTY) requireDeviceToken(config);
204
+ console.log('Dexter Bridge is not paired with your Dexter account yet.');
205
+ console.log('Open the Dexter plugin in Framer and pick Connect → Codex to get a pairing code.');
206
+ const code = await promptForPairingCode();
207
+ if (!code) requireDeviceToken(config);
208
+ await pairCommand({ apiBaseUrl, args: [code], flags, configDir });
209
+ deviceToken = readConfig(configDir).deviceToken;
210
+ }
211
+ const agent = resolveAgentName({ flagValue: flags.agent, config });
212
+ const model = resolveCompanionModelName({ flagValue: flags.model, config, agent });
213
+ const waitMs = Number(flags['wait-ms'] || 25000);
214
+ const once = Boolean(flags.once);
215
+ const bridgeEnv = agentEnvironment(config);
216
+ const metadata = await availabilityMetadata(config);
217
+ const pollLogger = createRunLogger({ runId: 'bridge-poll' });
218
+ const providerAdapters = new Map();
219
+ const providerAdapterForAgent = (runAgent) => {
220
+ if (providerAdapters.has(runAgent)) return providerAdapters.get(runAgent);
221
+ const adapter = createLocalAgentAdapter(runAgent, {
222
+ env: bridgeEnv,
223
+ trace: pollLogger,
224
+ });
225
+ providerAdapters.set(runAgent, adapter);
226
+ return adapter;
227
+ };
228
+ let pollFailureCount = 0;
229
+
230
+ console.log(`Dexter Bridge connected to ${apiBaseUrl}`);
231
+ console.log(`Agent: ${agent}`);
232
+ console.log(`Model: ${model}`);
233
+ console.log(`Logs: ${logLocationHint()}`);
234
+ console.log('Waiting for runs...');
235
+
236
+ try {
237
+ while (true) {
238
+ let poll;
239
+ try {
240
+ poll = await pollRun(apiBaseUrl, { deviceToken, waitMs, agent, model, metadata });
241
+ pollFailureCount = 0;
242
+ } catch (error) {
243
+ if (once) throw error;
244
+ pollFailureCount += 1;
245
+ const retryInMs = pollBackoffMs(pollFailureCount);
246
+ pollLogger.error('poll_failed', { error: errorMeta(error) });
247
+ if (pollFailureCount === 1 || (pollFailureCount & (pollFailureCount - 1)) === 0) {
248
+ console.error(`Dexter poll failed; retrying in ${Math.round(retryInMs / 1000)}s.`);
249
+ }
250
+ await wait(retryInMs);
251
+ continue;
252
+ }
253
+ if (poll.run) {
254
+ console.log(`Claimed run ${poll.run.runId}.`);
255
+ const runAgent = normalizeAgentName(poll.run?.companion?.agent || agent);
256
+ await executeRun(poll.run, {
257
+ apiBaseUrl,
258
+ deviceToken,
259
+ agent: runAgent,
260
+ selectedModel: model,
261
+ providerAdapter: providerAdapterForAgent(runAgent),
262
+ env: bridgeEnv,
263
+ maxSteps: flags['max-steps'],
264
+ });
265
+ if (once) return;
266
+ continue;
267
+ }
268
+ if (once) {
269
+ console.log('No pending Dexter runs.');
270
+ return;
271
+ }
272
+ }
273
+ } finally {
274
+ for (const adapter of providerAdapters.values()) adapter?.close?.();
275
+ providerAdapters.clear();
276
+ }
277
+ }
278
+
279
+ async function doctorCommand() {
280
+ const checks = await checkAllAgents();
281
+ for (const check of checks.agents) {
282
+ if (check.ok) console.log(`${check.command}: ok ${check.output || ''}`.trim());
283
+ else console.log(`${check.command}: unavailable (${check.error})`);
284
+ }
285
+ }
286
+
287
+ export async function runCli(argv) {
288
+ const parsed = parseArgv(argv);
289
+ if (parsed.flags.help) {
290
+ console.log(usage());
291
+ return;
292
+ }
293
+
294
+ const configDir = parsed.flags['config-dir'] || defaultConfigDir();
295
+ const config = readConfig(configDir);
296
+ const apiBaseUrl = resolveApiBaseUrl({
297
+ flagValue: parsed.flags.api,
298
+ config,
299
+ });
300
+
301
+ switch (parsed.command) {
302
+ // The one-command production path: claim the pairing code (when given or
303
+ // not yet paired), save config, and go straight into the polling loop.
304
+ case 'connect':
305
+ if (parsed.args[0]) {
306
+ await pairCommand({ apiBaseUrl, args: parsed.args, flags: parsed.flags, configDir });
307
+ }
308
+ await startCommand({
309
+ apiBaseUrl,
310
+ config: { ...readConfig(configDir), apiBaseUrl },
311
+ flags: parsed.flags,
312
+ configDir,
313
+ });
314
+ return;
315
+ case 'pair':
316
+ await pairCommand({ apiBaseUrl, args: parsed.args, flags: parsed.flags, configDir });
317
+ console.log('Run `dexter-bridge start` to go online.');
318
+ return;
319
+ case 'start':
320
+ await startCommand({ apiBaseUrl, config: { ...config, apiBaseUrl }, flags: parsed.flags, configDir });
321
+ return;
322
+ case 'status':
323
+ await statusCommand({ apiBaseUrl, config });
324
+ return;
325
+ case 'doctor':
326
+ await doctorCommand();
327
+ return;
328
+ case 'logout':
329
+ clearConfig(configDir);
330
+ console.log('Dexter Bridge local pairing removed.');
331
+ return;
332
+ default:
333
+ throw new Error(`Unknown command "${parsed.command}".\n\n${usage()}`);
334
+ }
335
+ }
336
+
337
+ export const __private__ = { agentEnvironment, parseArgv, pollBackoffMs, usage };
package/src/config.js ADDED
@@ -0,0 +1,207 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ export const DEFAULT_API_BASE_URL = 'http://localhost:3800/iwm-api/0.0.1';
6
+ export const BRIDGE_VERSION = '0.5.0';
7
+ export const BRIDGE_CAPABILITIES = [
8
+ 'model-turn-v1',
9
+ ];
10
+ // Codex is the default local agent: driving Claude Code from a user's Claude.ai
11
+ // subscription needs prior written approval from Anthropic for commercial use, so
12
+ // 'claude-code' only runs when the server offers it (see policy gating in
13
+ // framerCompanionModels.ts and docs/dexter-connect-onboarding-plan.md §0).
14
+ export const DEFAULT_BRIDGE_AGENT = 'codex';
15
+ export const BRIDGE_AGENTS = ['claude-code', 'codex', 'dry-run'];
16
+ export const COMPANION_MODEL_DEFINITIONS = [
17
+ {
18
+ id: 'claude-code:fable',
19
+ agent: 'claude-code',
20
+ provider: 'anthropic',
21
+ displayName: 'Claude Fable 5',
22
+ invocationName: 'claude-fable-5',
23
+ minimumAgentVersion: '2.1.170',
24
+ costTier: '$$$',
25
+ description: 'Current highest-capability Claude tier via Claude Code.',
26
+ },
27
+ {
28
+ id: 'claude-code:opus',
29
+ agent: 'claude-code',
30
+ provider: 'anthropic',
31
+ displayName: 'Claude Opus 4.8',
32
+ invocationName: 'claude-opus-4-8',
33
+ costTier: '$$$',
34
+ description: 'Latest Opus-class model via Claude Code.',
35
+ },
36
+ {
37
+ id: 'claude-code:sonnet',
38
+ agent: 'claude-code',
39
+ provider: 'anthropic',
40
+ displayName: 'Claude Sonnet 5',
41
+ invocationName: 'claude-sonnet-5',
42
+ costTier: '$$',
43
+ description: 'Current balanced Claude model via Claude Code.',
44
+ },
45
+ {
46
+ id: 'claude-code:haiku',
47
+ agent: 'claude-code',
48
+ provider: 'anthropic',
49
+ displayName: 'Claude Haiku',
50
+ invocationName: 'claude-haiku-4-5-20251001',
51
+ costTier: '$',
52
+ description: 'Fast Claude model via Claude Code.',
53
+ },
54
+ {
55
+ id: 'codex:gpt-5.5',
56
+ agent: 'codex',
57
+ provider: 'openai',
58
+ displayName: 'GPT-5.5',
59
+ invocationName: 'gpt-5.5',
60
+ costTier: '$$$',
61
+ description: 'Current frontier model for Codex runs.',
62
+ },
63
+ {
64
+ id: 'codex:gpt-5.4',
65
+ agent: 'codex',
66
+ provider: 'openai',
67
+ displayName: 'GPT-5.4',
68
+ invocationName: 'gpt-5.4',
69
+ costTier: '$$',
70
+ description: 'Flagship general-purpose model for Codex runs.',
71
+ },
72
+ {
73
+ id: 'codex:gpt-5.4-mini',
74
+ agent: 'codex',
75
+ provider: 'openai',
76
+ displayName: 'GPT-5.4 mini',
77
+ invocationName: 'gpt-5.4-mini',
78
+ costTier: '$',
79
+ description: 'Fast, efficient model for Codex runs.',
80
+ },
81
+ {
82
+ id: 'codex:gpt-5.3-codex-spark',
83
+ agent: 'codex',
84
+ provider: 'openai',
85
+ displayName: 'GPT-5.3 Codex Spark',
86
+ invocationName: 'gpt-5.3-codex-spark',
87
+ costTier: '$',
88
+ description: 'Low-latency Codex model for quick coding iterations.',
89
+ },
90
+ {
91
+ id: 'dry-run:default',
92
+ agent: 'dry-run',
93
+ provider: 'companion',
94
+ displayName: 'Dry run',
95
+ invocationName: 'dry-run',
96
+ costTier: '$',
97
+ description: 'Debug mode that does not call a local AI agent.',
98
+ },
99
+ ];
100
+
101
+ const DEFAULT_MODEL_BY_AGENT = {
102
+ 'claude-code': 'claude-code:sonnet',
103
+ codex: 'codex:gpt-5.5',
104
+ 'dry-run': 'dry-run:default',
105
+ };
106
+
107
+ export function defaultConfigDir(env = process.env) {
108
+ return env.DEXTER_BRIDGE_CONFIG_DIR || path.join(os.homedir(), '.dexter-bridge');
109
+ }
110
+
111
+ export function configFilePath(configDir = defaultConfigDir()) {
112
+ return path.join(configDir, 'config.json');
113
+ }
114
+
115
+ export function normalizeApiBaseUrl(value) {
116
+ const raw = String(value || DEFAULT_API_BASE_URL).trim();
117
+ return raw.replace(/\/+$/, '');
118
+ }
119
+
120
+ export function normalizeAgentName(value, fallback = DEFAULT_BRIDGE_AGENT) {
121
+ const raw = String(value || '').trim().toLowerCase();
122
+ if (raw === 'claude' || raw === 'claude_code' || raw === 'claude-code') return 'claude-code';
123
+ if (raw === 'openai-codex' || raw === 'openai_codex' || raw === 'codex') return 'codex';
124
+ if (raw === 'dryrun' || raw === 'dry_run' || raw === 'dry-run') return 'dry-run';
125
+ return BRIDGE_AGENTS.includes(fallback) ? fallback : DEFAULT_BRIDGE_AGENT;
126
+ }
127
+
128
+ export function companionModelsForAgent(agent) {
129
+ const normalizedAgent = normalizeAgentName(agent);
130
+ return COMPANION_MODEL_DEFINITIONS.filter((model) => model.agent === normalizedAgent);
131
+ }
132
+
133
+ export function companionModelDefinition(value, agent = DEFAULT_BRIDGE_AGENT) {
134
+ const normalizedAgent = normalizeAgentName(agent);
135
+ const raw = String(value || '').trim().toLowerCase();
136
+ const candidates = companionModelsForAgent(normalizedAgent);
137
+ if (!raw || raw === 'local-companion') {
138
+ return COMPANION_MODEL_DEFINITIONS.find((model) => model.id === DEFAULT_MODEL_BY_AGENT[normalizedAgent]);
139
+ }
140
+ return (
141
+ candidates.find((model) => model.id.toLowerCase() === raw) ||
142
+ candidates.find((model) => model.invocationName.toLowerCase() === raw) ||
143
+ candidates.find((model) => model.id.toLowerCase() === `${normalizedAgent}:${raw}`) ||
144
+ COMPANION_MODEL_DEFINITIONS.find((model) => model.id === DEFAULT_MODEL_BY_AGENT[normalizedAgent])
145
+ );
146
+ }
147
+
148
+ export function normalizeCompanionModelName(value, agent = DEFAULT_BRIDGE_AGENT) {
149
+ return companionModelDefinition(value, agent)?.id || DEFAULT_MODEL_BY_AGENT[normalizeAgentName(agent)];
150
+ }
151
+
152
+ export function companionModelMetadata(agent, model) {
153
+ const normalizedAgent = normalizeAgentName(agent);
154
+ const selectedModel = companionModelDefinition(model, normalizedAgent);
155
+ return {
156
+ model: selectedModel?.id || DEFAULT_MODEL_BY_AGENT[normalizedAgent],
157
+ modelLabel: selectedModel?.displayName || '',
158
+ models: companionModelsForAgent(normalizedAgent),
159
+ };
160
+ }
161
+
162
+ export function readConfig(configDir = defaultConfigDir()) {
163
+ const file = configFilePath(configDir);
164
+ try {
165
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
166
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
167
+ } catch (error) {
168
+ if (error?.code === 'ENOENT') return {};
169
+ throw error;
170
+ }
171
+ }
172
+
173
+ export function writeConfig(config, configDir = defaultConfigDir()) {
174
+ fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
175
+ const file = configFilePath(configDir);
176
+ fs.writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
177
+ try {
178
+ fs.chmodSync(file, 0o600);
179
+ } catch {
180
+ // Best effort on filesystems that do not support chmod.
181
+ }
182
+ return config;
183
+ }
184
+
185
+ export function saveConfigPatch(patch, configDir = defaultConfigDir()) {
186
+ return writeConfig({ ...readConfig(configDir), ...patch }, configDir);
187
+ }
188
+
189
+ export function clearConfig(configDir = defaultConfigDir()) {
190
+ try {
191
+ fs.rmSync(configFilePath(configDir), { force: true });
192
+ } catch {
193
+ // Logout should be idempotent.
194
+ }
195
+ }
196
+
197
+ export function resolveApiBaseUrl({ flagValue, config = readConfig(), env = process.env } = {}) {
198
+ return normalizeApiBaseUrl(flagValue || env.DEXTER_API_URL || env.DEXTER_BRIDGE_API_URL || config.apiBaseUrl);
199
+ }
200
+
201
+ export function resolveAgentName({ flagValue, config = readConfig(), env = process.env } = {}) {
202
+ return normalizeAgentName(flagValue || env.DEXTER_BRIDGE_AGENT || config.agent || config.engine);
203
+ }
204
+
205
+ export function resolveCompanionModelName({ flagValue, config = readConfig(), env = process.env, agent = DEFAULT_BRIDGE_AGENT } = {}) {
206
+ return normalizeCompanionModelName(flagValue || env.DEXTER_BRIDGE_MODEL || config.model || config.companionModel, agent);
207
+ }