@gakim-digital/dexter-bridge 0.11.11 → 0.11.13
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/package.json +1 -1
- package/src/agent.js +27 -2
- package/src/agentOutput.js +39 -0
- package/src/framerAgentTools.js +67 -14
- package/src/nativeSkills.js +3 -3
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -405,7 +405,7 @@ function argsWithClaudeIsolation(args, definition, options = {}) {
|
|
|
405
405
|
(name) => `mcp__instawebai__${name}`,
|
|
406
406
|
);
|
|
407
407
|
const nativeTools = options.framerHarness
|
|
408
|
-
? harnessTools
|
|
408
|
+
? [...(options.nativeSkillsEnabled ? ['Skill'] : []), ...harnessTools]
|
|
409
409
|
: [
|
|
410
410
|
'Read',
|
|
411
411
|
'Write',
|
|
@@ -442,7 +442,12 @@ function argsWithClaudeIsolation(args, definition, options = {}) {
|
|
|
442
442
|
configured = argsWithoutFlagValue(configured, '--mcp-config');
|
|
443
443
|
configured.push('--mcp-config', JSON.stringify(options.mcpConfig));
|
|
444
444
|
}
|
|
445
|
-
|
|
445
|
+
// Restricted mode ignores the skills written into the run workspace, so a
|
|
446
|
+
// Framer run with skills leaves it off. The explicit tool list above is what
|
|
447
|
+
// keeps the shell and file tools out; it grants the Framer tools and Skill
|
|
448
|
+
// only, and settings stay limited to the workspace the bridge created.
|
|
449
|
+
const framerRunWithSkills = Boolean(options.framerHarness && options.nativeSkillsEnabled);
|
|
450
|
+
if (options.harnessMode && !framerRunWithSkills && !argsIncludeFlag(configured, '--restricted')) {
|
|
446
451
|
configured.push('--restricted');
|
|
447
452
|
}
|
|
448
453
|
return configured;
|
|
@@ -2345,6 +2350,26 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2345
2350
|
});
|
|
2346
2351
|
return;
|
|
2347
2352
|
}
|
|
2353
|
+
if (event.kind === 'skill_used') {
|
|
2354
|
+
// The harness loaded a skill on its own. Dexter shows which one; the
|
|
2355
|
+
// aggregate of picked skills is already known to it.
|
|
2356
|
+
const skill = (materializedNativeSkills?.skills || []).find(
|
|
2357
|
+
(candidate) => candidate.nativeName === event.skillName,
|
|
2358
|
+
);
|
|
2359
|
+
if (!skill) return;
|
|
2360
|
+
void postActivity({
|
|
2361
|
+
stage: 'outcome',
|
|
2362
|
+
outcomeId: run?.outcome?.outcomeId,
|
|
2363
|
+
source: 'harness',
|
|
2364
|
+
meaningful: true,
|
|
2365
|
+
kind: 'skill_used',
|
|
2366
|
+
provider: definition.id,
|
|
2367
|
+
skillId: skill.skillId,
|
|
2368
|
+
occurredAt: event.occurredAt || new Date().toISOString(),
|
|
2369
|
+
message: `Using ${skill.name}`,
|
|
2370
|
+
});
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2348
2373
|
const meaningful =
|
|
2349
2374
|
Boolean(event.message || event.name) ||
|
|
2350
2375
|
event.kind === 'turn_started';
|
package/src/agentOutput.js
CHANGED
|
@@ -397,6 +397,11 @@ export function createClaudeProgressParser(onProgress, options = {}) {
|
|
|
397
397
|
});
|
|
398
398
|
continue;
|
|
399
399
|
}
|
|
400
|
+
if (part.name === 'Skill' && progressText(input.skill)) {
|
|
401
|
+
tools.set(part.id, { suppressResult: true });
|
|
402
|
+
emit({ kind: 'skill_used', skillName: progressText(input.skill) });
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
400
405
|
const progress = claudeToolProgress(part.name, input, options.cwd);
|
|
401
406
|
tools.set(part.id, { name: part.name, ...progress });
|
|
402
407
|
emit({
|
|
@@ -429,11 +434,45 @@ export function createClaudeProgressParser(onProgress, options = {}) {
|
|
|
429
434
|
}
|
|
430
435
|
};
|
|
431
436
|
|
|
437
|
+
// A large edit can take the model a minute to write, and nothing else is
|
|
438
|
+
// reported until it has finished writing. Report the moment it starts, and
|
|
439
|
+
// close that report when the finished tool call arrives.
|
|
440
|
+
const drafting = new Map();
|
|
441
|
+
const DRAFTING_MESSAGE = 'Preparing changes for the Framer project';
|
|
442
|
+
const processStreamEvent = (row) => {
|
|
443
|
+
const event = row.event;
|
|
444
|
+
const block = event?.type === 'content_block_start' ? event.content_block : null;
|
|
445
|
+
if (block?.type !== 'tool_use' || !block.id || drafting.has(block.id)) return;
|
|
446
|
+
const shortName = String(block.name || '').replace(/^mcp__instawebai__/, '');
|
|
447
|
+
if (!['framer_apply_changes', 'framer_update_code_file', 'framer_write'].includes(shortName)) return;
|
|
448
|
+
drafting.set(block.id, true);
|
|
449
|
+
emit({
|
|
450
|
+
kind: 'item_started',
|
|
451
|
+
phase: 'started',
|
|
452
|
+
activity: `tool-draft:${block.id}`,
|
|
453
|
+
message: DRAFTING_MESSAGE,
|
|
454
|
+
});
|
|
455
|
+
};
|
|
456
|
+
const finishDrafting = (row) => {
|
|
457
|
+
for (const part of Array.isArray(row.message?.content) ? row.message.content : []) {
|
|
458
|
+
if (part?.type !== 'tool_use' || drafting.get(part.id) !== true) continue;
|
|
459
|
+
drafting.set(part.id, false);
|
|
460
|
+
emit({
|
|
461
|
+
kind: 'item_completed',
|
|
462
|
+
phase: 'completed',
|
|
463
|
+
activity: `tool-draft:${part.id}`,
|
|
464
|
+
message: DRAFTING_MESSAGE,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
|
|
432
469
|
const processLine = (line) => {
|
|
433
470
|
const trimmed = line.trim();
|
|
434
471
|
if (!trimmed) return;
|
|
435
472
|
try {
|
|
436
473
|
const row = JSON.parse(trimmed);
|
|
474
|
+
if (row?.type === 'stream_event') processStreamEvent(row);
|
|
475
|
+
if (row?.type === 'assistant') finishDrafting(row);
|
|
437
476
|
if (row?.type === 'assistant') processAssistant(row);
|
|
438
477
|
if (row?.type === 'user') processUser(row);
|
|
439
478
|
} catch {
|
package/src/framerAgentTools.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
const require = createRequire(import.meta.url);
|
|
21
21
|
const MAX_OUTPUT_BYTES = 2 * 1024 * 1024;
|
|
22
22
|
const MAX_MODEL_RESULT_BYTES = 120 * 1024;
|
|
23
|
+
const MAX_CODE_FILE_CONTENT_CHARS = 200_000;
|
|
23
24
|
const MAX_SCREENSHOT_BYTES = 4 * 1024 * 1024;
|
|
24
25
|
const MAX_SCREENSHOTS_PER_CALL = 4;
|
|
25
26
|
const SCREENSHOT_FETCH_TIMEOUT_MS = 20_000;
|
|
@@ -479,7 +480,7 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
|
|
|
479
480
|
{
|
|
480
481
|
name: 'framer_read_code_file',
|
|
481
482
|
description:
|
|
482
|
-
'Read an existing Framer code file by its exact project path. Use this instead of generic JavaScript when inspecting a code component or override.
|
|
483
|
+
'Read the complete source of an existing Framer code file by its exact project path. Use this instead of generic JavaScript when inspecting a code component or override. Files up to 200,000 characters are returned without the general Framer result cap, together with completeness metadata, a source hash, and the version ID required for a safe update.',
|
|
483
484
|
inputSchema: {
|
|
484
485
|
type: 'object',
|
|
485
486
|
properties: {
|
|
@@ -498,7 +499,10 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
|
|
|
498
499
|
properties: {
|
|
499
500
|
path: { type: 'string', minLength: 1, maxLength: 1_000 },
|
|
500
501
|
expectedVersionId: { type: 'string', minLength: 1, maxLength: 300 },
|
|
501
|
-
content: {
|
|
502
|
+
content: {
|
|
503
|
+
type: 'string',
|
|
504
|
+
maxLength: MAX_CODE_FILE_CONTENT_CHARS,
|
|
505
|
+
},
|
|
502
506
|
},
|
|
503
507
|
required: ['path', 'expectedVersionId', 'content'],
|
|
504
508
|
additionalProperties: false,
|
|
@@ -738,6 +742,8 @@ function codeFileResultError(result, fallbackCode, fallbackMessage) {
|
|
|
738
742
|
? 404
|
|
739
743
|
: code === 'FRAMER_CODE_FILE_VERSION_CONFLICT'
|
|
740
744
|
? 409
|
|
745
|
+
: code === 'FRAMER_CODE_FILE_TOO_LARGE'
|
|
746
|
+
? 413
|
|
741
747
|
: 400;
|
|
742
748
|
return toolError(code, message, statusCode);
|
|
743
749
|
}
|
|
@@ -1897,6 +1903,13 @@ export function createFramerAgentToolRuntime({
|
|
|
1897
1903
|
...(branchChange ? { branch: branchChange } : {}),
|
|
1898
1904
|
};
|
|
1899
1905
|
};
|
|
1906
|
+
const normalizedCodeFileReadResult = (result) => ({
|
|
1907
|
+
result: parseStructuredOutput(result.stdout),
|
|
1908
|
+
...(result.stderr.trim()
|
|
1909
|
+
? { diagnostics: boundedText(result.stderr, 20_000) }
|
|
1910
|
+
: {}),
|
|
1911
|
+
...(branchChange ? { branch: branchChange } : {}),
|
|
1912
|
+
});
|
|
1900
1913
|
|
|
1901
1914
|
const markInspection = ({
|
|
1902
1915
|
visual = false,
|
|
@@ -2843,17 +2856,29 @@ export function createFramerAgentToolRuntime({
|
|
|
2843
2856
|
' },',
|
|
2844
2857
|
' }));',
|
|
2845
2858
|
' } else {',
|
|
2846
|
-
'
|
|
2847
|
-
|
|
2848
|
-
'
|
|
2849
|
-
'
|
|
2850
|
-
'
|
|
2851
|
-
'
|
|
2852
|
-
'
|
|
2853
|
-
'
|
|
2854
|
-
'
|
|
2855
|
-
' }
|
|
2856
|
-
' }
|
|
2859
|
+
' const content = typeof file.content === "string" ? file.content : "";',
|
|
2860
|
+
` const maximumContentChars = ${MAX_CODE_FILE_CONTENT_CHARS};`,
|
|
2861
|
+
' if (content.length > maximumContentChars) {',
|
|
2862
|
+
' console.log(JSON.stringify({',
|
|
2863
|
+
' ok: false,',
|
|
2864
|
+
' error: {',
|
|
2865
|
+
' code: "FRAMER_CODE_FILE_TOO_LARGE",',
|
|
2866
|
+
' message: `The Framer code file contains ${content.length} characters; Dexter supports complete code-file reads up to ${maximumContentChars} characters.`,',
|
|
2867
|
+
' },',
|
|
2868
|
+
' }));',
|
|
2869
|
+
' } else {',
|
|
2870
|
+
' console.log(JSON.stringify({',
|
|
2871
|
+
' ok: true,',
|
|
2872
|
+
' file: {',
|
|
2873
|
+
' id: file.id,',
|
|
2874
|
+
' name: file.name,',
|
|
2875
|
+
' path: file.path,',
|
|
2876
|
+
' versionId: file.versionId,',
|
|
2877
|
+
' content,',
|
|
2878
|
+
' exports: file.exports,',
|
|
2879
|
+
' },',
|
|
2880
|
+
' }));',
|
|
2881
|
+
' }',
|
|
2857
2882
|
' }',
|
|
2858
2883
|
'} catch (error) {',
|
|
2859
2884
|
' console.log(JSON.stringify({',
|
|
@@ -2874,6 +2899,31 @@ export function createFramerAgentToolRuntime({
|
|
|
2874
2899
|
`Dexter could not read ${filePath}.`,
|
|
2875
2900
|
);
|
|
2876
2901
|
}
|
|
2902
|
+
const content =
|
|
2903
|
+
typeof parsed.file?.content === 'string'
|
|
2904
|
+
? parsed.file.content
|
|
2905
|
+
: '';
|
|
2906
|
+
result = {
|
|
2907
|
+
...result,
|
|
2908
|
+
stdout: JSON.stringify({
|
|
2909
|
+
...parsed,
|
|
2910
|
+
file: {
|
|
2911
|
+
...parsed.file,
|
|
2912
|
+
content,
|
|
2913
|
+
complete: true,
|
|
2914
|
+
contentChars: content.length,
|
|
2915
|
+
contentBytes: Buffer.byteLength(content, 'utf8'),
|
|
2916
|
+
lineCount:
|
|
2917
|
+
content.length === 0
|
|
2918
|
+
? 0
|
|
2919
|
+
: content.split(/\r\n|\r|\n/).length,
|
|
2920
|
+
sourceHash: crypto
|
|
2921
|
+
.createHash('sha256')
|
|
2922
|
+
.update(content)
|
|
2923
|
+
.digest('hex'),
|
|
2924
|
+
},
|
|
2925
|
+
}),
|
|
2926
|
+
};
|
|
2877
2927
|
markInspection();
|
|
2878
2928
|
} else if (name === 'framer_update_code_file') {
|
|
2879
2929
|
const filePath = normalizeCodeFilePath(rawArguments.path);
|
|
@@ -3129,7 +3179,10 @@ export function createFramerAgentToolRuntime({
|
|
|
3129
3179
|
: 'Finished inspecting the Framer project',
|
|
3130
3180
|
durationMs,
|
|
3131
3181
|
});
|
|
3132
|
-
const normalized =
|
|
3182
|
+
const normalized =
|
|
3183
|
+
name === 'framer_read_code_file'
|
|
3184
|
+
? normalizedCodeFileReadResult(result)
|
|
3185
|
+
: normalizedResult(result);
|
|
3133
3186
|
if (name !== 'framer_read_project') return normalized;
|
|
3134
3187
|
const screenshotCount = screenshotQueries(rawArguments).length;
|
|
3135
3188
|
return {
|
package/src/nativeSkills.js
CHANGED
|
@@ -174,7 +174,7 @@ function renderAggregateClaudeSkill(skills, nativeName) {
|
|
|
174
174
|
return [
|
|
175
175
|
'---',
|
|
176
176
|
`name: ${yamlString(nativeName)}`,
|
|
177
|
-
`description: ${yamlString('Apply every
|
|
177
|
+
`description: ${yamlString('Apply every skill the user selected for this request.')}`,
|
|
178
178
|
'user-invocable: true',
|
|
179
179
|
'disable-model-invocation: true',
|
|
180
180
|
'---',
|
|
@@ -231,7 +231,7 @@ export function materializeNativeSkills(
|
|
|
231
231
|
['always', 'explicit', 'command'].includes(skill.trigger),
|
|
232
232
|
);
|
|
233
233
|
if (provider === 'claude-code' && requiredSkills.length > 0) {
|
|
234
|
-
aggregateName = `
|
|
234
|
+
aggregateName = `dexter-active-${digest.slice(0, 8)}`;
|
|
235
235
|
const aggregateDirectory = path.join(skillsRoot, aggregateName);
|
|
236
236
|
fs.mkdirSync(aggregateDirectory, { recursive: true, mode: 0o700 });
|
|
237
237
|
aggregatePath = path.join(aggregateDirectory, 'SKILL.md');
|
|
@@ -260,7 +260,7 @@ export function claudeNativeSkillPrompt(prompt, materialized) {
|
|
|
260
260
|
return [
|
|
261
261
|
materialized?.aggregateName ? `/${materialized.aggregateName}` : '',
|
|
262
262
|
catalog
|
|
263
|
-
? `Available optional skills; invoke one only when it is relevant:\n${catalog}`
|
|
263
|
+
? `Available optional skills; invoke one only when it is relevant, and do not use any skill that is not listed here:\n${catalog}`
|
|
264
264
|
: '',
|
|
265
265
|
String(prompt || ''),
|
|
266
266
|
].filter(Boolean).join('\n\n');
|