@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/LICENSE +16 -0
- package/README.md +154 -0
- package/bin/dexter-bridge.js +10 -0
- package/package.json +33 -0
- package/src/agent.js +998 -0
- package/src/agentOutput.js +232 -0
- package/src/api.js +165 -0
- package/src/cli.js +337 -0
- package/src/config.js +207 -0
- package/src/logger.js +183 -0
- package/src/protocol.js +190 -0
- package/src/providers/claudeAgentSdk.js +508 -0
- package/src/providers/codexAppServer.js +457 -0
- package/src/providers/index.js +55 -0
- package/src/providers/jsonRpcClient.js +172 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
const TOKEN_FIELDS = [
|
|
2
|
+
'inputTokens',
|
|
3
|
+
'outputTokens',
|
|
4
|
+
'cachedInputTokens',
|
|
5
|
+
'cacheWriteInputTokens',
|
|
6
|
+
'reasoningOutputTokens',
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
function nonNegativeNumber(value) {
|
|
10
|
+
const parsed = Number(value);
|
|
11
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function optionalNonNegativeNumber(value) {
|
|
15
|
+
if (value === undefined || value === null || value === '') return undefined;
|
|
16
|
+
const parsed = Number(value);
|
|
17
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function firstDefined(record, keys) {
|
|
21
|
+
for (const key of keys) {
|
|
22
|
+
if (record?.[key] !== undefined && record?.[key] !== null) return record[key];
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function normalizeCompanionTokenUsage(value = {}) {
|
|
28
|
+
const inputTokens = nonNegativeNumber(firstDefined(value, ['inputTokens', 'input_tokens']));
|
|
29
|
+
const outputTokens = nonNegativeNumber(firstDefined(value, ['outputTokens', 'output_tokens']));
|
|
30
|
+
const cachedInputTokens = nonNegativeNumber(firstDefined(value, [
|
|
31
|
+
'cachedInputTokens',
|
|
32
|
+
'cached_input_tokens',
|
|
33
|
+
'cacheReadInputTokens',
|
|
34
|
+
'cache_read_input_tokens',
|
|
35
|
+
]));
|
|
36
|
+
const cacheWriteInputTokens = nonNegativeNumber(firstDefined(value, [
|
|
37
|
+
'cacheWriteInputTokens',
|
|
38
|
+
'cache_write_input_tokens',
|
|
39
|
+
'cacheCreationInputTokens',
|
|
40
|
+
'cache_creation_input_tokens',
|
|
41
|
+
]));
|
|
42
|
+
const reasoningOutputTokens = nonNegativeNumber(firstDefined(value, [
|
|
43
|
+
'reasoningOutputTokens',
|
|
44
|
+
'reasoning_output_tokens',
|
|
45
|
+
]));
|
|
46
|
+
const explicitTotal = nonNegativeNumber(firstDefined(value, ['totalTokens', 'total_tokens']));
|
|
47
|
+
const totalTokens = explicitTotal || inputTokens + outputTokens + reasoningOutputTokens;
|
|
48
|
+
return {
|
|
49
|
+
inputTokens,
|
|
50
|
+
outputTokens,
|
|
51
|
+
cachedInputTokens,
|
|
52
|
+
cacheWriteInputTokens,
|
|
53
|
+
reasoningOutputTokens,
|
|
54
|
+
totalTokens,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function usageHasReportedTokens(usage) {
|
|
59
|
+
return TOKEN_FIELDS.some((field) => usage[field] > 0) || usage.totalTokens > 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeModelUsage(modelUsage) {
|
|
63
|
+
if (!modelUsage || typeof modelUsage !== 'object' || Array.isArray(modelUsage)) return [];
|
|
64
|
+
return Object.entries(modelUsage).flatMap(([model, raw]) => {
|
|
65
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return [];
|
|
66
|
+
const usage = normalizeCompanionTokenUsage(raw);
|
|
67
|
+
const estimatedCostUSD = optionalNonNegativeNumber(firstDefined(raw, ['costUSD', 'cost_usd', 'estimatedCostUSD']));
|
|
68
|
+
return [{ model, ...usage, ...(estimatedCostUSD === undefined ? {} : { estimatedCostUSD }) }];
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseWholeJson(stdout) {
|
|
73
|
+
const text = String(stdout || '').trim();
|
|
74
|
+
if (!text) return null;
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(text);
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function parseClaudeOutput(stdout) {
|
|
83
|
+
const parsed = parseWholeJson(stdout);
|
|
84
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
85
|
+
return {
|
|
86
|
+
resultText: String(stdout || '').trim(),
|
|
87
|
+
tokenUsage: normalizeCompanionTokenUsage(),
|
|
88
|
+
modelUsage: [],
|
|
89
|
+
usageAvailable: false,
|
|
90
|
+
usageSource: 'claude-code',
|
|
91
|
+
usageAccuracy: 'unavailable',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const hasEnvelope = typeof parsed.result === 'string' || parsed.usage || parsed.total_cost_usd !== undefined || parsed.modelUsage;
|
|
96
|
+
if (!hasEnvelope) {
|
|
97
|
+
return {
|
|
98
|
+
resultText: String(stdout || '').trim(),
|
|
99
|
+
tokenUsage: normalizeCompanionTokenUsage(),
|
|
100
|
+
modelUsage: [],
|
|
101
|
+
usageAvailable: false,
|
|
102
|
+
usageSource: 'claude-code',
|
|
103
|
+
usageAccuracy: 'unavailable',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const tokenUsage = normalizeCompanionTokenUsage(parsed.usage || {});
|
|
108
|
+
const modelUsage = normalizeModelUsage(parsed.modelUsage || parsed.model_usage);
|
|
109
|
+
const estimatedCostUSD = optionalNonNegativeNumber(parsed.total_cost_usd ?? parsed.totalCostUsd);
|
|
110
|
+
return {
|
|
111
|
+
resultText: typeof parsed.result === 'string' ? parsed.result.trim() : '',
|
|
112
|
+
tokenUsage,
|
|
113
|
+
model: typeof parsed.model === 'string' ? parsed.model : undefined,
|
|
114
|
+
sessionId: typeof parsed.session_id === 'string'
|
|
115
|
+
? parsed.session_id
|
|
116
|
+
: typeof parsed.sessionId === 'string'
|
|
117
|
+
? parsed.sessionId
|
|
118
|
+
: undefined,
|
|
119
|
+
modelUsage,
|
|
120
|
+
...(estimatedCostUSD === undefined ? {} : { estimatedCostUSD }),
|
|
121
|
+
usageAvailable: usageHasReportedTokens(tokenUsage) || estimatedCostUSD !== undefined || modelUsage.length > 0,
|
|
122
|
+
usageSource: 'claude-code',
|
|
123
|
+
usageAccuracy: 'reported',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function parseJsonLines(stdout) {
|
|
128
|
+
return String(stdout || '')
|
|
129
|
+
.split(/\r?\n/)
|
|
130
|
+
.map((line) => line.trim())
|
|
131
|
+
.filter(Boolean)
|
|
132
|
+
.flatMap((line) => {
|
|
133
|
+
try {
|
|
134
|
+
const parsed = JSON.parse(line);
|
|
135
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? [parsed] : [];
|
|
136
|
+
} catch {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function parseCodexOutput(stdout) {
|
|
143
|
+
const events = parseJsonLines(stdout);
|
|
144
|
+
if (!events.length) {
|
|
145
|
+
return {
|
|
146
|
+
resultText: String(stdout || '').trim(),
|
|
147
|
+
tokenUsage: normalizeCompanionTokenUsage(),
|
|
148
|
+
modelUsage: [],
|
|
149
|
+
usageAvailable: false,
|
|
150
|
+
usageSource: 'codex',
|
|
151
|
+
usageAccuracy: 'unavailable',
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let resultText = '';
|
|
156
|
+
let sessionId;
|
|
157
|
+
const accumulator = createCompanionUsageAccumulator('codex');
|
|
158
|
+
for (const event of events) {
|
|
159
|
+
if (
|
|
160
|
+
(event.type === 'thread.started' || event.type === 'thread.resumed') &&
|
|
161
|
+
typeof (event.thread_id || event.threadId) === 'string'
|
|
162
|
+
) {
|
|
163
|
+
sessionId = event.thread_id || event.threadId;
|
|
164
|
+
}
|
|
165
|
+
if (event.type === 'item.completed' && event.item?.type === 'agent_message' && typeof event.item.text === 'string') {
|
|
166
|
+
resultText = event.item.text.trim();
|
|
167
|
+
}
|
|
168
|
+
if (event.type === 'turn.completed' && event.usage && typeof event.usage === 'object') {
|
|
169
|
+
accumulator.add({ tokenUsage: event.usage, usageAvailable: true, usageAccuracy: 'reported', usageSource: 'codex' });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const usage = accumulator.snapshot();
|
|
173
|
+
return {
|
|
174
|
+
resultText: resultText || String(stdout || '').trim(),
|
|
175
|
+
tokenUsage: usage.tokenUsage,
|
|
176
|
+
modelUsage: [],
|
|
177
|
+
sessionId,
|
|
178
|
+
usageAvailable: usage.usageAvailable,
|
|
179
|
+
usageSource: 'codex',
|
|
180
|
+
usageAccuracy: usage.usageAvailable ? 'reported' : 'unavailable',
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function parseAgentOutput(agent, stdout) {
|
|
185
|
+
return agent === 'codex' ? parseCodexOutput(stdout) : parseClaudeOutput(stdout);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function createCompanionUsageAccumulator(source) {
|
|
189
|
+
const totals = normalizeCompanionTokenUsage();
|
|
190
|
+
const modelTotals = new Map();
|
|
191
|
+
let estimatedCostUSD = 0;
|
|
192
|
+
let hasEstimatedCost = false;
|
|
193
|
+
let usageAvailable = false;
|
|
194
|
+
let accuracy = 'unavailable';
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
add(report = {}) {
|
|
198
|
+
const usage = normalizeCompanionTokenUsage(report.tokenUsage || report.usage || report);
|
|
199
|
+
for (const field of TOKEN_FIELDS) totals[field] += usage[field];
|
|
200
|
+
totals.totalTokens += usage.totalTokens;
|
|
201
|
+
usageAvailable = usageAvailable || Boolean(report.usageAvailable) || usageHasReportedTokens(usage);
|
|
202
|
+
if (report.usageAccuracy === 'reported') accuracy = 'reported';
|
|
203
|
+
|
|
204
|
+
const cost = optionalNonNegativeNumber(report.estimatedCostUSD);
|
|
205
|
+
if (cost !== undefined) {
|
|
206
|
+
estimatedCostUSD += cost;
|
|
207
|
+
hasEstimatedCost = true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (const item of Array.isArray(report.modelUsage) ? report.modelUsage : []) {
|
|
211
|
+
if (!item || typeof item !== 'object' || typeof item.model !== 'string' || !item.model.trim()) continue;
|
|
212
|
+
const current = modelTotals.get(item.model) || { model: item.model, ...normalizeCompanionTokenUsage(), estimatedCostUSD: 0 };
|
|
213
|
+
const itemUsage = normalizeCompanionTokenUsage(item);
|
|
214
|
+
for (const field of TOKEN_FIELDS) current[field] += itemUsage[field];
|
|
215
|
+
current.totalTokens += itemUsage.totalTokens;
|
|
216
|
+
const itemCost = optionalNonNegativeNumber(item.estimatedCostUSD);
|
|
217
|
+
if (itemCost !== undefined) current.estimatedCostUSD += itemCost;
|
|
218
|
+
modelTotals.set(item.model, current);
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
snapshot() {
|
|
222
|
+
return {
|
|
223
|
+
tokenUsage: { ...totals },
|
|
224
|
+
...(hasEstimatedCost ? { estimatedCostUSD } : {}),
|
|
225
|
+
modelUsage: Array.from(modelTotals.values()),
|
|
226
|
+
usageAvailable,
|
|
227
|
+
usageSource: source,
|
|
228
|
+
usageAccuracy: usageAvailable ? accuracy : 'unavailable',
|
|
229
|
+
};
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BRIDGE_VERSION,
|
|
3
|
+
companionModelMetadata,
|
|
4
|
+
normalizeApiBaseUrl,
|
|
5
|
+
normalizeAgentName,
|
|
6
|
+
normalizeCompanionModelName,
|
|
7
|
+
} from './config.js';
|
|
8
|
+
|
|
9
|
+
export class DexterBridgeApiError extends Error {
|
|
10
|
+
constructor(message, status, body) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'DexterBridgeApiError';
|
|
13
|
+
this.status = status;
|
|
14
|
+
this.body = body;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function bearerHeaders(deviceToken) {
|
|
19
|
+
return deviceToken ? { Authorization: `Bearer ${deviceToken}` } : {};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function bridgeMetadata(agent, model, metadata) {
|
|
23
|
+
const normalizedAgent = normalizeAgentName(agent);
|
|
24
|
+
const normalizedModel = normalizeCompanionModelName(model, normalizedAgent);
|
|
25
|
+
const modelMetadata = companionModelMetadata(normalizedAgent, normalizedModel);
|
|
26
|
+
return {
|
|
27
|
+
shell: 'npx',
|
|
28
|
+
agent: normalizedAgent,
|
|
29
|
+
model: normalizedModel,
|
|
30
|
+
modelLabel: modelMetadata.modelLabel,
|
|
31
|
+
models: modelMetadata.models,
|
|
32
|
+
...(metadata && typeof metadata === 'object' ? metadata : {}),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function requestJson(apiBaseUrl, path, {
|
|
37
|
+
method = 'GET',
|
|
38
|
+
body,
|
|
39
|
+
deviceToken,
|
|
40
|
+
fetchImpl = fetch,
|
|
41
|
+
} = {}) {
|
|
42
|
+
const response = await fetchImpl(`${normalizeApiBaseUrl(apiBaseUrl)}${path}`, {
|
|
43
|
+
method,
|
|
44
|
+
headers: {
|
|
45
|
+
Accept: 'application/json',
|
|
46
|
+
'Content-Type': 'application/json',
|
|
47
|
+
...bearerHeaders(deviceToken),
|
|
48
|
+
},
|
|
49
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
50
|
+
});
|
|
51
|
+
const parsed = await response.json().catch(() => null);
|
|
52
|
+
if (!response.ok || parsed?.success === false) {
|
|
53
|
+
throw new DexterBridgeApiError(
|
|
54
|
+
parsed?.error || parsed?.message || `Dexter API request failed with status ${response.status}`,
|
|
55
|
+
response.status,
|
|
56
|
+
parsed,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return parsed;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function claimPairing(apiBaseUrl, {
|
|
63
|
+
pairingCode,
|
|
64
|
+
pairingToken,
|
|
65
|
+
deviceName,
|
|
66
|
+
agent,
|
|
67
|
+
model,
|
|
68
|
+
metadata,
|
|
69
|
+
fetchImpl,
|
|
70
|
+
}) {
|
|
71
|
+
const normalizedAgent = normalizeAgentName(agent);
|
|
72
|
+
const normalizedModel = normalizeCompanionModelName(model, normalizedAgent);
|
|
73
|
+
return requestJson(apiBaseUrl, '/ai/framer/companion/claim', {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
fetchImpl,
|
|
76
|
+
body: {
|
|
77
|
+
pairingCode,
|
|
78
|
+
pairingToken,
|
|
79
|
+
deviceName,
|
|
80
|
+
platform: process.platform,
|
|
81
|
+
companionVersion: BRIDGE_VERSION,
|
|
82
|
+
metadata: bridgeMetadata(normalizedAgent, normalizedModel, metadata),
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function heartbeat(apiBaseUrl, {
|
|
88
|
+
deviceToken,
|
|
89
|
+
status = 'ready',
|
|
90
|
+
agent,
|
|
91
|
+
model,
|
|
92
|
+
metadata,
|
|
93
|
+
fetchImpl,
|
|
94
|
+
}) {
|
|
95
|
+
const normalizedAgent = normalizeAgentName(agent);
|
|
96
|
+
const normalizedModel = normalizeCompanionModelName(model, normalizedAgent);
|
|
97
|
+
return requestJson(apiBaseUrl, '/ai/framer/companion/heartbeat', {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
deviceToken,
|
|
100
|
+
fetchImpl,
|
|
101
|
+
body: {
|
|
102
|
+
status,
|
|
103
|
+
platform: process.platform,
|
|
104
|
+
companionVersion: BRIDGE_VERSION,
|
|
105
|
+
metadata: bridgeMetadata(normalizedAgent, normalizedModel, metadata),
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function disconnectDevice(apiBaseUrl, {
|
|
111
|
+
deviceToken,
|
|
112
|
+
fetchImpl,
|
|
113
|
+
}) {
|
|
114
|
+
return requestJson(apiBaseUrl, '/ai/framer/companion/device/self', {
|
|
115
|
+
method: 'DELETE',
|
|
116
|
+
deviceToken,
|
|
117
|
+
fetchImpl,
|
|
118
|
+
body: {
|
|
119
|
+
platform: process.platform,
|
|
120
|
+
companionVersion: BRIDGE_VERSION,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function pollRun(apiBaseUrl, {
|
|
126
|
+
deviceToken,
|
|
127
|
+
waitMs = 25000,
|
|
128
|
+
agent,
|
|
129
|
+
model,
|
|
130
|
+
metadata,
|
|
131
|
+
fetchImpl,
|
|
132
|
+
}) {
|
|
133
|
+
const normalizedAgent = normalizeAgentName(agent);
|
|
134
|
+
const normalizedModel = normalizeCompanionModelName(model, normalizedAgent);
|
|
135
|
+
return requestJson(apiBaseUrl, '/ai/framer/companion/runs/poll', {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
deviceToken,
|
|
138
|
+
fetchImpl,
|
|
139
|
+
body: {
|
|
140
|
+
waitMs,
|
|
141
|
+
platform: process.platform,
|
|
142
|
+
companionVersion: BRIDGE_VERSION,
|
|
143
|
+
status: 'polling',
|
|
144
|
+
metadata: bridgeMetadata(normalizedAgent, normalizedModel, metadata),
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function postRunEvent(apiBaseUrl, {
|
|
150
|
+
deviceToken,
|
|
151
|
+
runId,
|
|
152
|
+
event,
|
|
153
|
+
fetchImpl,
|
|
154
|
+
}) {
|
|
155
|
+
return requestJson(apiBaseUrl, `/ai/framer/companion/runs/${encodeURIComponent(runId)}/events`, {
|
|
156
|
+
method: 'POST',
|
|
157
|
+
deviceToken,
|
|
158
|
+
fetchImpl,
|
|
159
|
+
body: {
|
|
160
|
+
event,
|
|
161
|
+
platform: process.platform,
|
|
162
|
+
companionVersion: BRIDGE_VERSION,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|