@gakim-digital/dexter-bridge 0.5.21 → 0.11.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.
@@ -241,6 +241,215 @@ function parseClaudeStreamUsage(events) {
241
241
  };
242
242
  }
243
243
 
244
+ function progressText(value, maximum = 500) {
245
+ if (typeof value !== 'string') return '';
246
+ return value.replace(/\s+/g, ' ').trim().slice(0, maximum);
247
+ }
248
+
249
+ function progressPath(value, cwd) {
250
+ if (typeof value !== 'string') return undefined;
251
+ let normalized = value.trim().replaceAll('\\', '/').replace(/\/+/g, '/');
252
+ const normalizedCwd = typeof cwd === 'string'
253
+ ? cwd.trim().replaceAll('\\', '/').replace(/\/+$/, '')
254
+ : '';
255
+ if (normalizedCwd && normalized.startsWith(`${normalizedCwd}/`)) {
256
+ normalized = normalized.slice(normalizedCwd.length + 1);
257
+ }
258
+ normalized = normalized.replace(/^\.\/+/, '');
259
+ if (!normalized || normalized.split('/').some((part) => part === '..')) return undefined;
260
+ if (normalized.startsWith('/') || /^[A-Za-z]:\//.test(normalized)) {
261
+ const parts = normalized.split('/').filter(Boolean);
262
+ normalized = parts.slice(-2).join('/');
263
+ }
264
+ return normalized.slice(0, 500) || undefined;
265
+ }
266
+
267
+ function claudeToolProgress(name, input = {}, cwd) {
268
+ const toolName = String(name || '').trim();
269
+ const shortName = toolName.replace(/^mcp__instawebai__/, '');
270
+ const filePath = progressPath(
271
+ input.file_path || input.path || input.notebook_path,
272
+ cwd,
273
+ );
274
+ const paths = filePath ? [filePath] : undefined;
275
+ const description = progressText(input.description, 180);
276
+ const messages = {
277
+ Read: [
278
+ filePath ? `Reading ${filePath}` : 'Reading a project file',
279
+ filePath ? `Read ${filePath}` : 'Finished reading the project file',
280
+ ],
281
+ Write: [
282
+ filePath ? `Creating ${filePath}` : 'Creating a project file',
283
+ filePath ? `Created ${filePath}` : 'Created the project file',
284
+ ],
285
+ Edit: [
286
+ filePath ? `Updating ${filePath}` : 'Updating a project file',
287
+ filePath ? `Updated ${filePath}` : 'Updated the project file',
288
+ ],
289
+ Glob: ['Finding project files', 'Found the project files'],
290
+ Grep: ['Searching the code', 'Finished searching the code'],
291
+ Bash: [
292
+ description || 'Running a project command',
293
+ description ? `Finished: ${description}` : 'Finished the project command',
294
+ ],
295
+ workspace_inspect: ['Inspecting the project', 'Finished inspecting the project'],
296
+ workspace_sync: ['Saving project changes', 'Saved the project changes'],
297
+ shell_run: [
298
+ description || 'Running project checks',
299
+ description ? `Finished: ${description}` : 'Finished the project checks',
300
+ ],
301
+ preview_control: ['Starting the live preview', 'Started the live preview'],
302
+ browser_control: ['Checking the app in the browser', 'Finished checking the app in the browser'],
303
+ data_inspect: ['Checking app data', 'Finished checking app data'],
304
+ verification_run: ['Running final verification', 'Finished final verification'],
305
+ };
306
+ const [startedMessage, completedMessage] = messages[shortName] || [
307
+ `Using ${shortName || 'a project tool'}`,
308
+ `Finished using ${shortName || 'the project tool'}`,
309
+ ];
310
+ return {
311
+ startedMessage,
312
+ completedMessage,
313
+ failedMessage: `Couldn’t finish: ${startedMessage}`,
314
+ paths,
315
+ };
316
+ }
317
+
318
+ function summedClaudeProgressUsage(turnUsage) {
319
+ const total = normalizeCompanionTokenUsage();
320
+ for (const usage of turnUsage.values()) {
321
+ for (const field of TOKEN_FIELDS) total[field] += usage[field];
322
+ total.totalTokens += usage.totalTokens;
323
+ }
324
+ return total;
325
+ }
326
+
327
+ export function createClaudeProgressParser(onProgress, options = {}) {
328
+ let buffer = '';
329
+ let lastUsage = '';
330
+ const tools = new Map();
331
+ const emittedTools = new Set();
332
+ const emittedText = new Set();
333
+ const turnUsage = new Map();
334
+
335
+ const emit = (event) => {
336
+ onProgress?.({
337
+ provider: 'claude-code',
338
+ occurredAt: new Date().toISOString(),
339
+ ...event,
340
+ });
341
+ };
342
+
343
+ const updateUsage = (message) => {
344
+ if (!message?.id || !message.usage || typeof message.usage !== 'object') return;
345
+ const current = turnUsage.get(message.id) || normalizeCompanionTokenUsage();
346
+ mergeMaximumUsage(current, message.usage);
347
+ turnUsage.set(message.id, current);
348
+ const tokenUsage = summedClaudeProgressUsage(turnUsage);
349
+ const serialized = JSON.stringify(tokenUsage);
350
+ if (!usageHasReportedTokens(tokenUsage) || serialized === lastUsage) return;
351
+ lastUsage = serialized;
352
+ emit({ kind: 'token_usage', tokenUsage });
353
+ };
354
+
355
+ const processAssistant = (row) => {
356
+ const message = row.message;
357
+ if (!message || typeof message !== 'object') return;
358
+ updateUsage(message);
359
+ for (const [index, part] of (Array.isArray(message.content) ? message.content : []).entries()) {
360
+ if (part?.type === 'text') {
361
+ const text = progressText(part.text);
362
+ const key = `${message.id || 'assistant'}:${index}:${text}`;
363
+ if (
364
+ !text
365
+ || emittedText.has(key)
366
+ || (text.startsWith('{') && text.endsWith('}'))
367
+ ) {
368
+ continue;
369
+ }
370
+ emittedText.add(key);
371
+ emit({
372
+ kind: 'output_delta',
373
+ message: text,
374
+ activity: `assistant:${message.id || key}:${index}`,
375
+ });
376
+ continue;
377
+ }
378
+ if (part?.type !== 'tool_use' || !part.id || emittedTools.has(part.id)) continue;
379
+ emittedTools.add(part.id);
380
+ const input = part.input && typeof part.input === 'object' && !Array.isArray(part.input)
381
+ ? part.input
382
+ : {};
383
+ if (
384
+ String(part.name || '').replace(/^mcp__instawebai__/, '') === 'progress_update'
385
+ && progressText(input.message)
386
+ ) {
387
+ tools.set(part.id, { suppressResult: true });
388
+ emit({
389
+ kind: 'output_delta',
390
+ message: progressText(input.message),
391
+ activity: `assistant-progress:${part.id}`,
392
+ });
393
+ continue;
394
+ }
395
+ const progress = claudeToolProgress(part.name, input, options.cwd);
396
+ tools.set(part.id, { name: part.name, ...progress });
397
+ emit({
398
+ kind: 'item_started',
399
+ name: part.name,
400
+ phase: 'started',
401
+ activity: `tool:${part.id}`,
402
+ paths: progress.paths,
403
+ message: progress.startedMessage,
404
+ });
405
+ }
406
+ };
407
+
408
+ const processUser = (row) => {
409
+ const content = row.message?.content;
410
+ if (!Array.isArray(content)) return;
411
+ for (const part of content) {
412
+ if (part?.type !== 'tool_result' || !part.tool_use_id) continue;
413
+ const tool = tools.get(part.tool_use_id);
414
+ if (!tool || tool.suppressResult) continue;
415
+ const failed = part.is_error === true || Boolean(row.toolDenialKind);
416
+ emit({
417
+ kind: failed ? 'item_failed' : 'item_completed',
418
+ name: tool.name,
419
+ phase: failed ? 'failed' : 'completed',
420
+ activity: `tool:${part.tool_use_id}`,
421
+ paths: tool.paths,
422
+ message: failed ? tool.failedMessage : tool.completedMessage,
423
+ });
424
+ }
425
+ };
426
+
427
+ const processLine = (line) => {
428
+ const trimmed = line.trim();
429
+ if (!trimmed) return;
430
+ try {
431
+ const row = JSON.parse(trimmed);
432
+ if (row?.type === 'assistant') processAssistant(row);
433
+ if (row?.type === 'user') processUser(row);
434
+ } catch {
435
+ // Claude may split JSON records across chunks. Incomplete records remain buffered.
436
+ }
437
+ };
438
+
439
+ return {
440
+ push(chunk) {
441
+ buffer += String(chunk || '');
442
+ const lines = buffer.split(/\r?\n/);
443
+ buffer = lines.pop() || '';
444
+ for (const line of lines) processLine(line);
445
+ },
446
+ flush() {
447
+ if (buffer.trim()) processLine(buffer);
448
+ buffer = '';
449
+ },
450
+ };
451
+ }
452
+
244
453
  export function parseClaudeOutput(stdout) {
245
454
  const whole = parseWholeJson(stdout);
246
455
  if (whole && typeof whole === 'object' && !Array.isArray(whole)) {
package/src/api.js CHANGED
@@ -39,9 +39,14 @@ export async function requestJson(apiBaseUrl, path, {
39
39
  body,
40
40
  deviceToken,
41
41
  fetchImpl = fetch,
42
+ signal,
43
+ allowInsecureHttp = false,
42
44
  } = {}) {
43
- const response = await fetchImpl(`${normalizeApiBaseUrl(apiBaseUrl)}${path}`, {
45
+ const response = await fetchImpl(`${normalizeApiBaseUrl(apiBaseUrl, {
46
+ allowInsecureHttp,
47
+ })}${path}`, {
44
48
  method,
49
+ signal,
45
50
  headers: {
46
51
  Accept: 'application/json',
47
52
  'Content-Type': 'application/json',
@@ -75,6 +80,8 @@ export function claimPairing(apiBaseUrl, {
75
80
  agent,
76
81
  model,
77
82
  metadata,
83
+ runtimeProfiles,
84
+ runtimeSelection,
78
85
  fetchImpl,
79
86
  }) {
80
87
  const normalizedAgent = normalizeAgentName(agent);
@@ -89,6 +96,8 @@ export function claimPairing(apiBaseUrl, {
89
96
  platform: process.platform,
90
97
  companionVersion: BRIDGE_VERSION,
91
98
  metadata: bridgeMetadata(normalizedAgent, normalizedModel, metadata),
99
+ runtimeProfiles,
100
+ runtimeSelection,
92
101
  },
93
102
  });
94
103
  }
@@ -99,7 +108,9 @@ export function heartbeat(apiBaseUrl, {
99
108
  agent,
100
109
  model,
101
110
  metadata,
111
+ runtimeProfiles,
102
112
  fetchImpl,
113
+ allowInsecureHttp,
103
114
  }) {
104
115
  const normalizedAgent = normalizeAgentName(agent);
105
116
  const normalizedModel = normalizeCompanionModelName(model, normalizedAgent);
@@ -107,11 +118,13 @@ export function heartbeat(apiBaseUrl, {
107
118
  method: 'POST',
108
119
  deviceToken,
109
120
  fetchImpl,
121
+ allowInsecureHttp,
110
122
  body: {
111
123
  status,
112
124
  platform: process.platform,
113
125
  companionVersion: BRIDGE_VERSION,
114
126
  metadata: bridgeMetadata(normalizedAgent, normalizedModel, metadata),
127
+ runtimeProfiles,
115
128
  },
116
129
  });
117
130
  }
@@ -137,7 +150,9 @@ export function pollRun(apiBaseUrl, {
137
150
  agent,
138
151
  model,
139
152
  metadata,
153
+ runtimeProfiles,
140
154
  fetchImpl,
155
+ allowInsecureHttp,
141
156
  }) {
142
157
  const normalizedAgent = normalizeAgentName(agent);
143
158
  const normalizedModel = normalizeCompanionModelName(model, normalizedAgent);
@@ -145,26 +160,58 @@ export function pollRun(apiBaseUrl, {
145
160
  method: 'POST',
146
161
  deviceToken,
147
162
  fetchImpl,
163
+ allowInsecureHttp,
148
164
  body: {
149
165
  waitMs,
150
166
  platform: process.platform,
151
167
  companionVersion: BRIDGE_VERSION,
152
168
  status: 'polling',
153
169
  metadata: bridgeMetadata(normalizedAgent, normalizedModel, metadata),
170
+ runtimeProfiles,
154
171
  },
155
172
  });
156
173
  }
157
174
 
175
+ export function getFramerProjectAuthorization(apiBaseUrl, {
176
+ deviceToken,
177
+ runId,
178
+ initiate = false,
179
+ forceRefresh = false,
180
+ fetchImpl,
181
+ signal,
182
+ allowInsecureHttp,
183
+ }) {
184
+ return requestJson(
185
+ apiBaseUrl,
186
+ `/ai/framer/companion/runs/${encodeURIComponent(runId)}/framer-authorization`,
187
+ {
188
+ method: 'POST',
189
+ deviceToken,
190
+ fetchImpl,
191
+ signal,
192
+ allowInsecureHttp,
193
+ body: {
194
+ initiate,
195
+ forceRefresh,
196
+ platform: process.platform,
197
+ companionVersion: BRIDGE_VERSION,
198
+ },
199
+ },
200
+ );
201
+ }
202
+
158
203
  export function postRunEvent(apiBaseUrl, {
159
204
  deviceToken,
160
205
  runId,
161
206
  event,
162
207
  fetchImpl,
208
+ allowInsecureHttp,
163
209
  }) {
164
210
  return requestJson(apiBaseUrl, `/ai/framer/companion/runs/${encodeURIComponent(runId)}/events`, {
165
211
  method: 'POST',
166
212
  deviceToken,
167
213
  fetchImpl,
214
+ allowInsecureHttp,
168
215
  body: {
169
216
  event,
170
217
  platform: process.platform,