@blockrun/franklin 3.3.3 → 3.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/README.md +55 -4
- package/dist/agent/commands.d.ts +1 -1
- package/dist/agent/commands.js +128 -17
- package/dist/agent/compact.d.ts +2 -2
- package/dist/agent/compact.js +148 -22
- package/dist/agent/context.d.ts +8 -3
- package/dist/agent/context.js +301 -108
- package/dist/agent/error-classifier.d.ts +11 -2
- package/dist/agent/error-classifier.js +64 -10
- package/dist/agent/llm.d.ts +8 -1
- package/dist/agent/llm.js +114 -19
- package/dist/agent/loop.d.ts +1 -2
- package/dist/agent/loop.js +509 -61
- package/dist/agent/optimize.d.ts +2 -2
- package/dist/agent/optimize.js +9 -7
- package/dist/agent/permissions.d.ts +1 -1
- package/dist/agent/permissions.js +1 -1
- package/dist/agent/planner.d.ts +42 -0
- package/dist/agent/planner.js +110 -0
- package/dist/agent/reduce.d.ts +7 -1
- package/dist/agent/reduce.js +85 -3
- package/dist/agent/streaming-executor.d.ts +6 -1
- package/dist/agent/streaming-executor.js +83 -5
- package/dist/agent/tokens.d.ts +11 -2
- package/dist/agent/tokens.js +38 -5
- package/dist/agent/tool-guard.d.ts +27 -0
- package/dist/agent/tool-guard.js +324 -0
- package/dist/agent/types.d.ts +7 -1
- package/dist/agent/types.js +1 -1
- package/dist/brain/extract.d.ts +11 -0
- package/dist/brain/extract.js +154 -0
- package/dist/brain/index.d.ts +3 -0
- package/dist/brain/index.js +2 -0
- package/dist/brain/store.d.ts +42 -0
- package/dist/brain/store.js +225 -0
- package/dist/brain/types.d.ts +45 -0
- package/dist/brain/types.js +5 -0
- package/dist/commands/daemon.js +2 -1
- package/dist/commands/start.js +16 -3
- package/dist/config.js +1 -1
- package/dist/index.js +27 -2
- package/dist/learnings/extractor.d.ts +13 -0
- package/dist/learnings/extractor.js +69 -8
- package/dist/learnings/index.d.ts +1 -1
- package/dist/learnings/index.js +1 -1
- package/dist/learnings/store.js +42 -13
- package/dist/learnings/types.d.ts +1 -1
- package/dist/mcp/client.d.ts +1 -1
- package/dist/mcp/client.js +5 -5
- package/dist/mcp/config.d.ts +1 -1
- package/dist/mcp/config.js +1 -1
- package/dist/panel/html.d.ts +2 -0
- package/dist/panel/html.js +409 -146
- package/dist/panel/server.js +19 -0
- package/dist/pricing.js +3 -2
- package/dist/proxy/fallback.d.ts +3 -1
- package/dist/proxy/fallback.js +4 -4
- package/dist/proxy/server.js +29 -11
- package/dist/proxy/sse-translator.js +1 -1
- package/dist/router/categories.d.ts +21 -0
- package/dist/router/categories.js +96 -0
- package/dist/router/index.d.ts +9 -2
- package/dist/router/index.js +106 -27
- package/dist/router/local-elo.d.ts +32 -0
- package/dist/router/local-elo.js +107 -0
- package/dist/router/selector.d.ts +46 -0
- package/dist/router/selector.js +106 -0
- package/dist/session/storage.d.ts +5 -1
- package/dist/session/storage.js +24 -2
- package/dist/social/a11y.d.ts +1 -1
- package/dist/social/a11y.js +5 -1
- package/dist/social/browser.d.ts +5 -0
- package/dist/social/browser.js +22 -0
- package/dist/social/preflight.d.ts +4 -0
- package/dist/social/preflight.js +42 -3
- package/dist/stats/failures.d.ts +20 -0
- package/dist/stats/failures.js +63 -0
- package/dist/stats/format.d.ts +6 -0
- package/dist/stats/format.js +23 -0
- package/dist/stats/insights.js +1 -21
- package/dist/stats/session-tracker.d.ts +21 -0
- package/dist/stats/session-tracker.js +28 -0
- package/dist/stats/tracker.d.ts +1 -1
- package/dist/stats/tracker.js +1 -1
- package/dist/tools/bash.d.ts +14 -1
- package/dist/tools/bash.js +132 -7
- package/dist/tools/edit.js +77 -14
- package/dist/tools/glob.js +13 -3
- package/dist/tools/grep.js +30 -12
- package/dist/tools/imagegen.js +3 -3
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +5 -1
- package/dist/tools/read.d.ts +16 -2
- package/dist/tools/read.js +36 -8
- package/dist/tools/searchx.d.ts +6 -2
- package/dist/tools/searchx.js +221 -44
- package/dist/tools/subagent.js +37 -3
- package/dist/tools/task.js +43 -7
- package/dist/tools/validate.d.ts +11 -0
- package/dist/tools/validate.js +42 -0
- package/dist/tools/webfetch.js +18 -7
- package/dist/tools/websearch.js +41 -7
- package/dist/tools/write.js +26 -6
- package/dist/ui/app.js +31 -6
- package/dist/ui/model-picker.d.ts +1 -1
- package/dist/ui/model-picker.js +1 -1
- package/dist/ui/terminal.d.ts +1 -1
- package/dist/ui/terminal.js +1 -1
- package/package.json +2 -2
package/dist/tools/bash.js
CHANGED
|
@@ -161,15 +161,49 @@ function compressBuild(out) {
|
|
|
161
161
|
});
|
|
162
162
|
return collapseBlankLines(kept.join('\n')).trim() || out.trim();
|
|
163
163
|
}
|
|
164
|
+
const backgroundTasks = new Map();
|
|
165
|
+
let bgTaskCounter = 0;
|
|
166
|
+
/** Get a background task's result (called by the agent to check status). */
|
|
167
|
+
export function getBackgroundTask(id) {
|
|
168
|
+
return backgroundTasks.get(id);
|
|
169
|
+
}
|
|
170
|
+
/** List all background tasks. */
|
|
171
|
+
export function listBackgroundTasks() {
|
|
172
|
+
return [...backgroundTasks.values()];
|
|
173
|
+
}
|
|
164
174
|
const MAX_OUTPUT_BYTES = 512 * 1024; // 512KB capture buffer (prevents OOM)
|
|
165
175
|
const MAX_RETURN_CHARS = 32_000; // 32KB return cap (~8,000 tokens) — prevents context bloat
|
|
166
176
|
const DEFAULT_TIMEOUT_MS = 120_000; // 2 minutes
|
|
167
177
|
async function execute(input, ctx) {
|
|
168
|
-
const { command, timeout } = input;
|
|
178
|
+
const { command, timeout, run_in_background: runInBackground } = input;
|
|
169
179
|
if (!command || typeof command !== 'string') {
|
|
170
180
|
return { output: 'Error: command is required', isError: true };
|
|
171
181
|
}
|
|
172
182
|
const timeoutMs = Math.min(timeout ?? DEFAULT_TIMEOUT_MS, 600_000);
|
|
183
|
+
// Background execution: spawn and return immediately with a task ID
|
|
184
|
+
if (runInBackground) {
|
|
185
|
+
const taskId = `bg-${++bgTaskCounter}`;
|
|
186
|
+
const desc = input.description || command.slice(0, 60);
|
|
187
|
+
const task = {
|
|
188
|
+
id: taskId,
|
|
189
|
+
command,
|
|
190
|
+
description: desc,
|
|
191
|
+
startedAt: Date.now(),
|
|
192
|
+
status: 'running',
|
|
193
|
+
};
|
|
194
|
+
backgroundTasks.set(taskId, task);
|
|
195
|
+
// Run in background — don't await
|
|
196
|
+
executeCommand(command, timeoutMs, ctx).then(result => {
|
|
197
|
+
task.status = result.isError ? 'failed' : 'completed';
|
|
198
|
+
task.result = result;
|
|
199
|
+
});
|
|
200
|
+
return {
|
|
201
|
+
output: `Background task started: ${taskId}\nCommand: ${command.slice(0, 100)}\n\nYou will be notified when it completes. Do not poll or sleep — continue with other work.`,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return executeCommand(command, timeoutMs, ctx);
|
|
205
|
+
}
|
|
206
|
+
function executeCommand(command, timeoutMs, ctx) {
|
|
173
207
|
return new Promise((resolve) => {
|
|
174
208
|
const shell = process.env.SHELL || '/bin/bash';
|
|
175
209
|
let child;
|
|
@@ -178,7 +212,9 @@ async function execute(input, ctx) {
|
|
|
178
212
|
cwd: ctx.workingDir,
|
|
179
213
|
env: {
|
|
180
214
|
...process.env,
|
|
181
|
-
|
|
215
|
+
FRANKLIN: '1', // Let scripts detect they're running inside Franklin
|
|
216
|
+
FRANKLIN_WORKDIR: ctx.workingDir,
|
|
217
|
+
RUNCODE: '1', // Backwards compat
|
|
182
218
|
RUNCODE_WORKDIR: ctx.workingDir,
|
|
183
219
|
},
|
|
184
220
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -217,8 +253,8 @@ async function execute(input, ctx) {
|
|
|
217
253
|
if (!ctx.onProgress)
|
|
218
254
|
return;
|
|
219
255
|
const now = Date.now();
|
|
220
|
-
if (now - lastProgressEmit <
|
|
221
|
-
return; // max
|
|
256
|
+
if (now - lastProgressEmit < 200)
|
|
257
|
+
return; // max 5 updates/sec
|
|
222
258
|
lastProgressEmit = now;
|
|
223
259
|
const lastLine = text.split('\n').map(l => l.trim()).filter(Boolean).pop();
|
|
224
260
|
if (lastLine)
|
|
@@ -323,19 +359,108 @@ async function execute(input, ctx) {
|
|
|
323
359
|
});
|
|
324
360
|
});
|
|
325
361
|
}
|
|
362
|
+
/**
|
|
363
|
+
* Detect if a bash command is read-only (safe to run concurrently).
|
|
364
|
+
* Inspired by Claude Code's isSearchOrReadBashCommand — analyzes command segments
|
|
365
|
+
* to determine if ALL operations are read-only.
|
|
366
|
+
*/
|
|
367
|
+
const READ_ONLY_COMMANDS = new Set([
|
|
368
|
+
'ls', 'cat', 'head', 'tail', 'wc', 'du', 'df', 'file', 'stat', 'tree',
|
|
369
|
+
'find', 'grep', 'rg', 'ag', 'ack', 'which', 'whereis', 'type',
|
|
370
|
+
'echo', 'printf', 'date', 'whoami', 'hostname', 'uname', 'env', 'printenv',
|
|
371
|
+
'pwd', 'realpath', 'dirname', 'basename',
|
|
372
|
+
'jq', 'yq', 'sort', 'uniq', 'cut', 'tr', 'awk', 'sed', // sed is read-only when used in pipeline (no -i)
|
|
373
|
+
'diff', 'comm', 'less', 'more',
|
|
374
|
+
]);
|
|
375
|
+
const READ_ONLY_GIT_SUBCOMMANDS = new Set([
|
|
376
|
+
'status', 'log', 'diff', 'show', 'branch', 'tag', 'remote', 'stash',
|
|
377
|
+
'blame', 'shortlog', 'describe', 'rev-parse', 'rev-list', 'ls-files',
|
|
378
|
+
'ls-tree', 'ls-remote', 'config', 'reflog',
|
|
379
|
+
]);
|
|
380
|
+
function isReadOnlyCommand(command) {
|
|
381
|
+
// Split on operators (&&, ||, ;, |) and check each segment
|
|
382
|
+
const segments = command.split(/\s*(?:&&|\|\||[;|])\s*/);
|
|
383
|
+
for (const segment of segments) {
|
|
384
|
+
const trimmed = segment.trim();
|
|
385
|
+
if (!trimmed)
|
|
386
|
+
continue;
|
|
387
|
+
// Extract the base command (first word, ignore env vars and redirects)
|
|
388
|
+
const words = trimmed.split(/\s+/).filter(w => !w.includes('=') && !w.startsWith('>') && !w.startsWith('<'));
|
|
389
|
+
const baseCmd = words[0]?.replace(/^(sudo|time|nice)\s+/, '') || '';
|
|
390
|
+
if (baseCmd === 'git') {
|
|
391
|
+
const subCmd = words[1] || '';
|
|
392
|
+
if (!READ_ONLY_GIT_SUBCOMMANDS.has(subCmd))
|
|
393
|
+
return false;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (baseCmd === 'npm' || baseCmd === 'npx' || baseCmd === 'yarn' || baseCmd === 'pnpm') {
|
|
397
|
+
const subCmd = words[1] || '';
|
|
398
|
+
// npm run/test/list/info are read-only; npm install/build are not
|
|
399
|
+
if (['run', 'test', 'list', 'ls', 'info', 'view', 'show', 'outdated', 'audit'].includes(subCmd))
|
|
400
|
+
continue;
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
// Check if it's a known read-only command
|
|
404
|
+
const baseName = baseCmd.split('/').pop() || baseCmd;
|
|
405
|
+
if (!READ_ONLY_COMMANDS.has(baseName))
|
|
406
|
+
return false;
|
|
407
|
+
// sed with -i flag is NOT read-only
|
|
408
|
+
if (baseName === 'sed' && trimmed.includes(' -i'))
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
return segments.some(s => s.trim().length > 0); // At least one non-empty segment
|
|
412
|
+
}
|
|
326
413
|
export const bashCapability = {
|
|
327
414
|
spec: {
|
|
328
415
|
name: 'Bash',
|
|
329
|
-
description:
|
|
416
|
+
description: `Executes a given bash command and returns its output.
|
|
417
|
+
|
|
418
|
+
The working directory persists between commands, but shell state does not. The shell environment is initialized from the user's profile (bash or zsh).
|
|
419
|
+
|
|
420
|
+
IMPORTANT: Avoid using this tool to run \`find\`, \`grep\`, \`cat\`, \`head\`, \`tail\`, \`sed\`, \`awk\`, or \`echo\` commands, unless explicitly instructed or after you have verified that a dedicated tool cannot accomplish your task. Instead, use the appropriate dedicated tool as this will provide a much better experience for the user:
|
|
421
|
+
|
|
422
|
+
- File search: Use Glob (NOT find or ls)
|
|
423
|
+
- Content search: Use Grep (NOT grep or rg)
|
|
424
|
+
- Read files: Use Read (NOT cat/head/tail)
|
|
425
|
+
- Edit files: Use Edit (NOT sed/awk)
|
|
426
|
+
- Write files: Use Write (NOT echo >/cat <<EOF)
|
|
427
|
+
- Communication: Output text directly (NOT echo/printf)
|
|
428
|
+
|
|
429
|
+
# Instructions
|
|
430
|
+
- If your command will create new directories or files, first use this tool to run \`ls\` to verify the parent directory exists and is the correct location.
|
|
431
|
+
- Always quote file paths that contain spaces with double quotes in your command (e.g., cd "path with spaces/file.txt")
|
|
432
|
+
- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of \`cd\`. You may use \`cd\` if the user explicitly requests it.
|
|
433
|
+
- You may specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). By default, your command will timeout after 120000ms (2 minutes).
|
|
434
|
+
- When issuing multiple commands:
|
|
435
|
+
- If the commands are independent and can run in parallel, make multiple Bash tool calls in a single message.
|
|
436
|
+
- If the commands depend on each other and must run sequentially, use a single Bash call with '&&' to chain them together.
|
|
437
|
+
- Use ';' only when you need to run commands sequentially but don't care if earlier commands fail.
|
|
438
|
+
- DO NOT use newlines to separate commands (newlines are ok in quoted strings).
|
|
439
|
+
- For git commands:
|
|
440
|
+
- Prefer to create a new commit rather than amending an existing commit.
|
|
441
|
+
- Before running destructive operations (e.g., git reset --hard, git push --force, git checkout --), consider whether there is a safer alternative. Only use destructive operations when truly the best approach.
|
|
442
|
+
- Never skip hooks (--no-verify) unless the user has explicitly asked for it. If a hook fails, investigate and fix the underlying issue.
|
|
443
|
+
- NEVER use git commands with the -i flag (like git rebase -i or git add -i) since they require interactive input which is not supported.
|
|
444
|
+
- Avoid unnecessary \`sleep\` commands:
|
|
445
|
+
- Do not sleep between commands that can run immediately — just run them.
|
|
446
|
+
- Do not retry failing commands in a sleep loop — diagnose the root cause.
|
|
447
|
+
|
|
448
|
+
Output is capped at 512KB capture / 32KB return.`,
|
|
330
449
|
input_schema: {
|
|
331
450
|
type: 'object',
|
|
332
451
|
properties: {
|
|
333
|
-
command: { type: 'string', description: 'The
|
|
452
|
+
command: { type: 'string', description: 'The command to execute' },
|
|
453
|
+
description: { type: 'string', description: 'Clear, concise description of what this command does in active voice. For simple commands (git, npm), keep it brief (5-10 words): "Show working tree status", "Install dependencies". For complex commands (piped, obscure flags), add enough context: "Find and delete all .tmp files recursively"' },
|
|
334
454
|
timeout: { type: 'number', description: 'Timeout in milliseconds (default: 120000, max: 600000)' },
|
|
455
|
+
run_in_background: { type: 'boolean', description: 'Set to true to run this command in the background. Returns immediately with a task ID. Use this for long-running commands (builds, installs, deploys) when you don\'t need the result immediately. You will be notified when it completes — do NOT sleep or poll.' },
|
|
335
456
|
},
|
|
336
457
|
required: ['command'],
|
|
337
458
|
},
|
|
338
459
|
},
|
|
339
460
|
execute,
|
|
340
|
-
concurrent: false,
|
|
461
|
+
concurrent: false, // Default; overridden by isConcurrentSafe for read-only commands
|
|
462
|
+
isConcurrentSafe: (input) => {
|
|
463
|
+
const cmd = input.command || '';
|
|
464
|
+
return isReadOnlyCommand(cmd);
|
|
465
|
+
},
|
|
341
466
|
};
|
package/dist/tools/edit.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import { partiallyReadFiles } from './read.js';
|
|
6
|
+
import { partiallyReadFiles, fileReadTracker } from './read.js';
|
|
7
7
|
/**
|
|
8
8
|
* Normalize curly/smart quotes to straight quotes.
|
|
9
9
|
* Claude Code does this to handle API-sanitized strings and editor paste artifacts.
|
|
@@ -28,8 +28,27 @@ async function execute(input, ctx) {
|
|
|
28
28
|
return { output: 'Error: old_string and new_string are identical', isError: true };
|
|
29
29
|
}
|
|
30
30
|
const resolved = path.isAbsolute(filePath) ? filePath : path.resolve(ctx.workingDir, filePath);
|
|
31
|
-
//
|
|
32
|
-
const
|
|
31
|
+
// Enforce read-before-edit: the model must Read the file before editing it
|
|
32
|
+
const readRecord = fileReadTracker.get(resolved);
|
|
33
|
+
if (!readRecord) {
|
|
34
|
+
return {
|
|
35
|
+
output: `Error: you must Read this file before editing it. Use Read to understand the current content first.\nFile: ${resolved}`,
|
|
36
|
+
isError: true,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// Check if the file was modified since it was last read (stale write detection)
|
|
40
|
+
try {
|
|
41
|
+
const currentStat = fs.statSync(resolved);
|
|
42
|
+
if (currentStat.mtimeMs !== readRecord.mtimeMs) {
|
|
43
|
+
return {
|
|
44
|
+
output: `Warning: ${resolved} has been modified since you last read it. Read the file again to see the current content before editing.`,
|
|
45
|
+
isError: true,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch { /* file may have been deleted — will be caught below */ }
|
|
50
|
+
// Check if the file was only partially read — used for smarter warning below
|
|
51
|
+
const partialInfo = partiallyReadFiles.get(resolved);
|
|
33
52
|
try {
|
|
34
53
|
if (!fs.existsSync(resolved)) {
|
|
35
54
|
return { output: `Error: file not found: ${resolved}`, isError: true };
|
|
@@ -41,9 +60,31 @@ async function execute(input, ctx) {
|
|
|
41
60
|
const normalized = normalizeQuotes(oldStr);
|
|
42
61
|
const contentNormalized = normalizeQuotes(content);
|
|
43
62
|
if (normalized !== oldStr && contentNormalized.includes(normalized)) {
|
|
44
|
-
// Find the original text in content that corresponds to the normalized match
|
|
45
|
-
|
|
46
|
-
|
|
63
|
+
// Find the original text in content that corresponds to the normalized match.
|
|
64
|
+
// IMPORTANT: We can't use normalized.length to slice the original content because
|
|
65
|
+
// smart quotes are multi-byte in UTF-8 (3 bytes) while straight quotes are 1 byte.
|
|
66
|
+
// Instead, we map the character index from the normalized string back to the original.
|
|
67
|
+
const normIdx = contentNormalized.indexOf(normalized);
|
|
68
|
+
// Walk through content character-by-character, mapping normalized positions to original positions
|
|
69
|
+
let origStart = -1;
|
|
70
|
+
let origEnd = -1;
|
|
71
|
+
let normPos = 0;
|
|
72
|
+
for (let i = 0; i < content.length; i++) {
|
|
73
|
+
if (normPos === normIdx && origStart === -1) {
|
|
74
|
+
origStart = i;
|
|
75
|
+
}
|
|
76
|
+
if (normPos === normIdx + normalized.length) {
|
|
77
|
+
origEnd = i;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
// Both content and contentNormalized have same character count (quote replacement is 1:1 char)
|
|
81
|
+
normPos++;
|
|
82
|
+
}
|
|
83
|
+
if (origStart !== -1) {
|
|
84
|
+
if (origEnd === -1)
|
|
85
|
+
origEnd = content.length;
|
|
86
|
+
effectiveOldStr = content.slice(origStart, origEnd);
|
|
87
|
+
}
|
|
47
88
|
}
|
|
48
89
|
}
|
|
49
90
|
if (!content.includes(effectiveOldStr)) {
|
|
@@ -104,6 +145,9 @@ async function execute(input, ctx) {
|
|
|
104
145
|
fs.writeFileSync(resolved, updated, 'utf-8');
|
|
105
146
|
// File has been modified — remove from partial-read tracking so next read is fresh
|
|
106
147
|
partiallyReadFiles.delete(resolved);
|
|
148
|
+
// Update read tracker mtime so subsequent edits don't trigger stale-write detection
|
|
149
|
+
const newStat = fs.statSync(resolved);
|
|
150
|
+
fileReadTracker.set(resolved, { mtimeMs: newStat.mtimeMs, readAt: Date.now() });
|
|
107
151
|
// Build a concise diff preview
|
|
108
152
|
const oldLines = effectiveOldStr.split('\n');
|
|
109
153
|
const newLines = newStr.split('\n');
|
|
@@ -116,9 +160,16 @@ async function execute(input, ctx) {
|
|
|
116
160
|
else {
|
|
117
161
|
diffPreview = ` (${oldLines.length} lines → ${newLines.length} lines)`;
|
|
118
162
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
163
|
+
// Only warn about partial read if the edit target is near or beyond the read boundary.
|
|
164
|
+
// A normal Read(limit=2000) on a 10K line file shouldn't warn if editing line 50.
|
|
165
|
+
let partialWarning = '';
|
|
166
|
+
if (partialInfo) {
|
|
167
|
+
const editLine = content.slice(0, content.indexOf(effectiveOldStr)).split('\n').length;
|
|
168
|
+
const nearBoundary = editLine >= partialInfo.endLine - 10 || editLine < partialInfo.startLine;
|
|
169
|
+
if (nearBoundary) {
|
|
170
|
+
partialWarning = `\nWarning: file was only partially read (lines ${partialInfo.startLine}-${partialInfo.endLine} of ${partialInfo.totalLines}). This edit is near the boundary — consider reading more of the file.`;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
122
173
|
return {
|
|
123
174
|
output: `Updated ${resolved} — ${matchCount} replacement${matchCount > 1 ? 's' : ''} made.${diffPreview}${partialWarning}`,
|
|
124
175
|
};
|
|
@@ -131,14 +182,26 @@ async function execute(input, ctx) {
|
|
|
131
182
|
export const editCapability = {
|
|
132
183
|
spec: {
|
|
133
184
|
name: 'Edit',
|
|
134
|
-
description:
|
|
185
|
+
description: `Perform exact string replacements in files.
|
|
186
|
+
|
|
187
|
+
Usage:
|
|
188
|
+
- You MUST use Read at least once before editing. This tool will error if you attempt an edit without reading the file first.
|
|
189
|
+
- When editing text from Read output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: line number + tab. Everything after that is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string.
|
|
190
|
+
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
|
|
191
|
+
- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
|
|
192
|
+
- The edit will FAIL if old_string is not unique in the file. Either provide a larger string with more surrounding context to make it unique, or use replace_all to change every instance of old_string.
|
|
193
|
+
- Use replace_all for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.
|
|
194
|
+
- old_string and new_string must be different.
|
|
195
|
+
- If the file has been modified since your last Read (by linter, formatter, or another tool), the edit will fail with a stale-write warning. Read the file again to get the current content.
|
|
196
|
+
|
|
197
|
+
IMPORTANT: Always use Edit instead of sed or awk via Bash.`,
|
|
135
198
|
input_schema: {
|
|
136
199
|
type: 'object',
|
|
137
200
|
properties: {
|
|
138
|
-
file_path: { type: 'string', description: '
|
|
139
|
-
old_string: { type: 'string', description: '
|
|
140
|
-
new_string: { type: 'string', description: '
|
|
141
|
-
replace_all: { type: 'boolean', description: 'Replace all occurrences' },
|
|
201
|
+
file_path: { type: 'string', description: 'The absolute path to the file to modify' },
|
|
202
|
+
old_string: { type: 'string', description: 'The text to replace (must be different from new_string)' },
|
|
203
|
+
new_string: { type: 'string', description: 'The text to replace it with' },
|
|
204
|
+
replace_all: { type: 'boolean', description: 'Replace all occurrences of old_string (default false)' },
|
|
142
205
|
},
|
|
143
206
|
required: ['file_path', 'old_string', 'new_string'],
|
|
144
207
|
},
|
package/dist/tools/glob.js
CHANGED
|
@@ -143,12 +143,22 @@ async function execute(input, ctx) {
|
|
|
143
143
|
export const globCapability = {
|
|
144
144
|
spec: {
|
|
145
145
|
name: 'Glob',
|
|
146
|
-
description:
|
|
146
|
+
description: `Fast file pattern matching tool that works with any codebase size.
|
|
147
|
+
|
|
148
|
+
Usage:
|
|
149
|
+
- Supports glob patterns like "**/*.js" or "src/**/*.ts"
|
|
150
|
+
- Returns matching file paths sorted by modification time (most recent first)
|
|
151
|
+
- Use this when you need to find files by name patterns
|
|
152
|
+
- Skips node_modules, .git, __pycache__ automatically
|
|
153
|
+
- Returns up to 200 results
|
|
154
|
+
- When doing an open-ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead
|
|
155
|
+
|
|
156
|
+
IMPORTANT: Always use Glob instead of find or ls via Bash.`,
|
|
147
157
|
input_schema: {
|
|
148
158
|
type: 'object',
|
|
149
159
|
properties: {
|
|
150
|
-
pattern: { type: 'string', description: '
|
|
151
|
-
path: { type: 'string', description: '
|
|
160
|
+
pattern: { type: 'string', description: 'The glob pattern to match files against (e.g. "**/*.ts", "src/**/*.tsx")' },
|
|
161
|
+
path: { type: 'string', description: 'The directory to search in. Defaults to working directory.' },
|
|
152
162
|
},
|
|
153
163
|
required: ['pattern'],
|
|
154
164
|
},
|
package/dist/tools/grep.js
CHANGED
|
@@ -70,6 +70,8 @@ function runRipgrep(opts, searchPath, mode, limit, cwd) {
|
|
|
70
70
|
args.push('-U', '--multiline-dotall');
|
|
71
71
|
if (opts.glob)
|
|
72
72
|
args.push(`--glob=${opts.glob}`);
|
|
73
|
+
if (opts.type)
|
|
74
|
+
args.push(`--type=${opts.type}`);
|
|
73
75
|
// Always exclude common noise + lock files (huge, rarely useful)
|
|
74
76
|
args.push('--glob=!node_modules', '--glob=!.git', '--glob=!dist', '--glob=!*.lock', '--glob=!package-lock.json', '--glob=!pnpm-lock.yaml');
|
|
75
77
|
args.push('--', opts.pattern);
|
|
@@ -81,7 +83,9 @@ function runRipgrep(opts, searchPath, mode, limit, cwd) {
|
|
|
81
83
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
82
84
|
});
|
|
83
85
|
const lines = result.split('\n').filter(Boolean);
|
|
84
|
-
const
|
|
86
|
+
const offset = opts.offset ?? 0;
|
|
87
|
+
const sliced = offset > 0 ? lines.slice(offset) : lines;
|
|
88
|
+
const limited = limit > 0 ? sliced.slice(0, limit) : sliced;
|
|
85
89
|
// Convert absolute paths to relative paths to save tokens (same as Claude Code)
|
|
86
90
|
const relativized = limited.map(line => {
|
|
87
91
|
// Lines: /abs/path or /abs/path:rest (content mode)
|
|
@@ -171,20 +175,34 @@ function runNativeGrep(opts, searchPath, mode, limit, cwd) {
|
|
|
171
175
|
export const grepCapability = {
|
|
172
176
|
spec: {
|
|
173
177
|
name: 'Grep',
|
|
174
|
-
description:
|
|
178
|
+
description: `A powerful search tool built on ripgrep.
|
|
179
|
+
|
|
180
|
+
ALWAYS use Grep for search tasks. NEVER invoke grep or rg as a Bash command. The Grep tool has been optimized for correct permissions and access.
|
|
181
|
+
|
|
182
|
+
Usage:
|
|
183
|
+
- Supports full regex syntax (e.g., "log.*Error", "function\\s+\\w+")
|
|
184
|
+
- Filter files with glob parameter (e.g., "*.js", "**/*.tsx") or type parameter (e.g., "js", "py", "rust") — more efficient than searching all files
|
|
185
|
+
- Output modes: "content" shows matching lines with context, "files_with_matches" shows only file paths (default), "count" shows match counts
|
|
186
|
+
- Use context/before_context/after_context for surrounding lines (requires output_mode: "content")
|
|
187
|
+
- Pattern syntax: Uses ripgrep (not grep) — literal braces need escaping (use \`interface\\{\\}\` to find \`interface{}\` in Go code)
|
|
188
|
+
- Multiline matching: By default patterns match within single lines only. For cross-line patterns like \`struct \\{[\\s\\S]*?field\`, use multiline: true
|
|
189
|
+
- Use Agent tool for open-ended searches requiring multiple rounds of exploration
|
|
190
|
+
- Default head_limit is 250 results. Pass 0 for unlimited (use sparingly — large results waste context)`,
|
|
175
191
|
input_schema: {
|
|
176
192
|
type: 'object',
|
|
177
193
|
properties: {
|
|
178
|
-
pattern: { type: 'string', description: '
|
|
179
|
-
path: { type: 'string', description: 'File or
|
|
180
|
-
glob: { type: 'string', description: '
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
194
|
+
pattern: { type: 'string', description: 'The regular expression pattern to search for in file contents' },
|
|
195
|
+
path: { type: 'string', description: 'File or directory to search in. Defaults to working directory.' },
|
|
196
|
+
glob: { type: 'string', description: 'Glob pattern to filter files (e.g. "*.js", "*.{ts,tsx}") — maps to rg --glob' },
|
|
197
|
+
type: { type: 'string', description: 'File type to search (rg --type). Common types: js, py, rust, go, java, ts. More efficient than glob for standard file types.' },
|
|
198
|
+
output_mode: { type: 'string', description: 'Output mode: "content" shows matching lines, "files_with_matches" shows file paths (default), "count" shows match counts' },
|
|
199
|
+
context: { type: 'number', description: 'Number of lines to show before and after each match (rg -C). Requires output_mode: "content"' },
|
|
200
|
+
before_context: { type: 'number', description: 'Number of lines to show before each match (rg -B). Requires output_mode: "content"' },
|
|
201
|
+
after_context: { type: 'number', description: 'Number of lines to show after each match (rg -A). Requires output_mode: "content"' },
|
|
202
|
+
case_insensitive: { type: 'boolean', description: 'Case insensitive search (rg -i)' },
|
|
203
|
+
head_limit: { type: 'number', description: 'Limit output to first N entries. Defaults to 250. Pass 0 for unlimited (use sparingly — large results waste context).' },
|
|
204
|
+
offset: { type: 'number', description: 'Skip first N entries before applying head_limit. Defaults to 0.' },
|
|
205
|
+
multiline: { type: 'boolean', description: 'Enable multiline mode where . matches newlines and patterns can span lines (rg -U --multiline-dotall). Default: false.' },
|
|
188
206
|
},
|
|
189
207
|
required: ['pattern'],
|
|
190
208
|
},
|
package/dist/tools/imagegen.js
CHANGED
|
@@ -29,7 +29,7 @@ async function execute(input, ctx) {
|
|
|
29
29
|
});
|
|
30
30
|
const headers = {
|
|
31
31
|
'Content-Type': 'application/json',
|
|
32
|
-
'User-Agent': `
|
|
32
|
+
'User-Agent': `franklin/${VERSION}`,
|
|
33
33
|
};
|
|
34
34
|
const controller = new AbortController();
|
|
35
35
|
const timeout = setTimeout(() => controller.abort(), 60_000); // 60s timeout
|
|
@@ -134,7 +134,7 @@ async function signPayment(response, chain, endpoint) {
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
catch (err) {
|
|
137
|
-
console.error(`[
|
|
137
|
+
console.error(`[franklin] Image payment error: ${err.message}`);
|
|
138
138
|
return null;
|
|
139
139
|
}
|
|
140
140
|
}
|
|
@@ -155,7 +155,7 @@ async function extractPaymentReq(response) {
|
|
|
155
155
|
export const imageGenCapability = {
|
|
156
156
|
spec: {
|
|
157
157
|
name: 'ImageGen',
|
|
158
|
-
description: 'Generate an image from a text prompt using
|
|
158
|
+
description: 'Generate an image from a text prompt using DALL-E. Costs USDC from the user\'s wallet — confirm before generating. Saves to a local file. Default size: 1024x1024. Do NOT call repeatedly to iterate on style — ask the user first.',
|
|
159
159
|
input_schema: {
|
|
160
160
|
type: 'object',
|
|
161
161
|
properties: {
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { grepCapability } from './grep.js';
|
|
|
11
11
|
import { webFetchCapability } from './webfetch.js';
|
|
12
12
|
import { webSearchCapability } from './websearch.js';
|
|
13
13
|
import { taskCapability } from './task.js';
|
|
14
|
-
/** All capabilities available to the
|
|
14
|
+
/** All capabilities available to the Franklin agent (excluding sub-agent, which needs config). */
|
|
15
15
|
export declare const allCapabilities: CapabilityHandler[];
|
|
16
16
|
export { readCapability, writeCapability, editCapability, bashCapability, globCapability, grepCapability, webFetchCapability, webSearchCapability, taskCapability, };
|
|
17
17
|
export { createSubAgentCapability } from './subagent.js';
|
package/dist/tools/index.js
CHANGED
|
@@ -13,7 +13,9 @@ import { taskCapability } from './task.js';
|
|
|
13
13
|
import { imageGenCapability } from './imagegen.js';
|
|
14
14
|
import { askUserCapability } from './askuser.js';
|
|
15
15
|
import { tradingSignalCapability, tradingMarketCapability } from './trading.js';
|
|
16
|
-
|
|
16
|
+
import { searchXCapability } from './searchx.js';
|
|
17
|
+
import { postToXCapability } from './posttox.js';
|
|
18
|
+
/** All capabilities available to the Franklin agent (excluding sub-agent, which needs config). */
|
|
17
19
|
export const allCapabilities = [
|
|
18
20
|
readCapability,
|
|
19
21
|
writeCapability,
|
|
@@ -28,6 +30,8 @@ export const allCapabilities = [
|
|
|
28
30
|
askUserCapability,
|
|
29
31
|
tradingSignalCapability,
|
|
30
32
|
tradingMarketCapability,
|
|
33
|
+
searchXCapability,
|
|
34
|
+
postToXCapability,
|
|
31
35
|
];
|
|
32
36
|
export { readCapability, writeCapability, editCapability, bashCapability, globCapability, grepCapability, webFetchCapability, webSearchCapability, taskCapability, };
|
|
33
37
|
export { createSubAgentCapability } from './subagent.js';
|
package/dist/tools/read.d.ts
CHANGED
|
@@ -4,8 +4,22 @@
|
|
|
4
4
|
import type { CapabilityHandler } from '../agent/types.js';
|
|
5
5
|
/**
|
|
6
6
|
* Tracks files that were only partially read (offset or limit applied).
|
|
7
|
-
*
|
|
7
|
+
* Stores the read range so Edit tool can give smarter warnings —
|
|
8
|
+
* only warns if the edit target is near/beyond the boundary of what was read.
|
|
8
9
|
* Exported so edit.ts can check and clear entries.
|
|
9
10
|
*/
|
|
10
|
-
export declare const partiallyReadFiles:
|
|
11
|
+
export declare const partiallyReadFiles: Map<string, {
|
|
12
|
+
startLine: number;
|
|
13
|
+
endLine: number;
|
|
14
|
+
totalLines: number;
|
|
15
|
+
}>;
|
|
16
|
+
/**
|
|
17
|
+
* Tracks files that have been read in this session — enables read-before-edit enforcement.
|
|
18
|
+
* Stores the file's mtime at read time so we can detect stale writes.
|
|
19
|
+
* Exported so edit.ts and write.ts can check.
|
|
20
|
+
*/
|
|
21
|
+
export declare const fileReadTracker: Map<string, {
|
|
22
|
+
mtimeMs: number;
|
|
23
|
+
readAt: number;
|
|
24
|
+
}>;
|
|
11
25
|
export declare const readCapability: CapabilityHandler;
|
package/dist/tools/read.js
CHANGED
|
@@ -5,10 +5,17 @@ import fs from 'node:fs';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
/**
|
|
7
7
|
* Tracks files that were only partially read (offset or limit applied).
|
|
8
|
-
*
|
|
8
|
+
* Stores the read range so Edit tool can give smarter warnings —
|
|
9
|
+
* only warns if the edit target is near/beyond the boundary of what was read.
|
|
9
10
|
* Exported so edit.ts can check and clear entries.
|
|
10
11
|
*/
|
|
11
|
-
export const partiallyReadFiles = new
|
|
12
|
+
export const partiallyReadFiles = new Map();
|
|
13
|
+
/**
|
|
14
|
+
* Tracks files that have been read in this session — enables read-before-edit enforcement.
|
|
15
|
+
* Stores the file's mtime at read time so we can detect stale writes.
|
|
16
|
+
* Exported so edit.ts and write.ts can check.
|
|
17
|
+
*/
|
|
18
|
+
export const fileReadTracker = new Map();
|
|
12
19
|
async function execute(input, ctx) {
|
|
13
20
|
const { file_path: filePath, offset, limit } = input;
|
|
14
21
|
if (!filePath) {
|
|
@@ -43,15 +50,21 @@ async function execute(input, ctx) {
|
|
|
43
50
|
const maxLines = limit ?? 2000;
|
|
44
51
|
const endLine = Math.min(allLines.length, startLine + maxLines);
|
|
45
52
|
const slice = allLines.slice(startLine, endLine);
|
|
46
|
-
// Track partial reads —
|
|
53
|
+
// Track partial reads — store the range so Edit can give smarter warnings
|
|
47
54
|
const isPartial = startLine > 0 || endLine < allLines.length;
|
|
48
55
|
if (isPartial) {
|
|
49
|
-
partiallyReadFiles.
|
|
56
|
+
partiallyReadFiles.set(resolved, {
|
|
57
|
+
startLine: startLine + 1, // 1-based
|
|
58
|
+
endLine,
|
|
59
|
+
totalLines: allLines.length,
|
|
60
|
+
});
|
|
50
61
|
}
|
|
51
62
|
else {
|
|
52
63
|
// Full read — clear any stale partial flag
|
|
53
64
|
partiallyReadFiles.delete(resolved);
|
|
54
65
|
}
|
|
66
|
+
// Record this read for read-before-edit/write enforcement
|
|
67
|
+
fileReadTracker.set(resolved, { mtimeMs: stat.mtimeMs, readAt: Date.now() });
|
|
55
68
|
// Format with line numbers (cat -n style)
|
|
56
69
|
const numbered = slice.map((line, i) => `${startLine + i + 1}\t${line}`);
|
|
57
70
|
let result = numbered.join('\n');
|
|
@@ -74,13 +87,28 @@ async function execute(input, ctx) {
|
|
|
74
87
|
export const readCapability = {
|
|
75
88
|
spec: {
|
|
76
89
|
name: 'Read',
|
|
77
|
-
description:
|
|
90
|
+
description: `Read a file from the local filesystem. You can access any file directly by using this tool.
|
|
91
|
+
|
|
92
|
+
Assume this tool is able to read all files on the machine. If the user provides a path to a file, assume that path is valid. It is okay to read a file that does not exist; an error will be returned.
|
|
93
|
+
|
|
94
|
+
Usage:
|
|
95
|
+
- The file_path parameter must be an absolute path, not a relative path.
|
|
96
|
+
- By default, reads up to 2000 lines starting from the beginning of the file.
|
|
97
|
+
- When you already know which part of the file you need, only read that part using offset/limit. This can be important for larger files.
|
|
98
|
+
- Results are returned in cat -n format, with line numbers starting at 1.
|
|
99
|
+
- This tool can only read files, not directories. To list a directory, use Glob or ls via Bash.
|
|
100
|
+
- If you read a file that exists but has empty contents you will receive a warning.
|
|
101
|
+
- Reads over 2MB are rejected — use offset/limit to read portions.
|
|
102
|
+
- Cannot read binary files (images, PDFs, archives).
|
|
103
|
+
- You will regularly be asked to read screenshots or images. If the user provides a path, ALWAYS use this tool to view it.
|
|
104
|
+
|
|
105
|
+
IMPORTANT: Always use Read instead of cat, head, or tail via Bash. This tool provides line numbers and integrates with Edit's read-before-edit enforcement.`,
|
|
78
106
|
input_schema: {
|
|
79
107
|
type: 'object',
|
|
80
108
|
properties: {
|
|
81
|
-
file_path: { type: 'string', description: '
|
|
82
|
-
offset: { type: 'number', description: '
|
|
83
|
-
limit: { type: 'number', description: '
|
|
109
|
+
file_path: { type: 'string', description: 'The absolute path to the file to read' },
|
|
110
|
+
offset: { type: 'number', description: 'The line number to start reading from (1-based). Only provide if the file is too large to read at once.' },
|
|
111
|
+
limit: { type: 'number', description: 'The number of lines to read. Only provide if the file is too large to read at once. Default: 2000.' },
|
|
84
112
|
},
|
|
85
113
|
required: ['file_path'],
|
|
86
114
|
},
|
package/dist/tools/searchx.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SearchX capability — search X (Twitter) for posts matching a query.
|
|
3
|
-
* Returns candidate posts with snippets and product relevance scores.
|
|
4
|
-
*
|
|
3
|
+
* Returns candidate posts with snippets, tweet URLs, and product relevance scores.
|
|
4
|
+
*
|
|
5
|
+
* Works in two modes:
|
|
6
|
+
* - **Basic** (no config): browser-only search, returns snippets + URLs
|
|
7
|
+
* - **Enhanced** (with social config): adds product routing, dedup, login detection
|
|
5
8
|
*/
|
|
6
9
|
import type { CapabilityHandler } from '../agent/types.js';
|
|
10
|
+
export declare function detectNotificationsIntent(query: string | undefined, handle: string, knownHandles?: string[]): boolean;
|
|
7
11
|
export declare const searchXCapability: CapabilityHandler;
|