@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.
Files changed (109) hide show
  1. package/README.md +55 -4
  2. package/dist/agent/commands.d.ts +1 -1
  3. package/dist/agent/commands.js +128 -17
  4. package/dist/agent/compact.d.ts +2 -2
  5. package/dist/agent/compact.js +148 -22
  6. package/dist/agent/context.d.ts +8 -3
  7. package/dist/agent/context.js +301 -108
  8. package/dist/agent/error-classifier.d.ts +11 -2
  9. package/dist/agent/error-classifier.js +64 -10
  10. package/dist/agent/llm.d.ts +8 -1
  11. package/dist/agent/llm.js +114 -19
  12. package/dist/agent/loop.d.ts +1 -2
  13. package/dist/agent/loop.js +509 -61
  14. package/dist/agent/optimize.d.ts +2 -2
  15. package/dist/agent/optimize.js +9 -7
  16. package/dist/agent/permissions.d.ts +1 -1
  17. package/dist/agent/permissions.js +1 -1
  18. package/dist/agent/planner.d.ts +42 -0
  19. package/dist/agent/planner.js +110 -0
  20. package/dist/agent/reduce.d.ts +7 -1
  21. package/dist/agent/reduce.js +85 -3
  22. package/dist/agent/streaming-executor.d.ts +6 -1
  23. package/dist/agent/streaming-executor.js +83 -5
  24. package/dist/agent/tokens.d.ts +11 -2
  25. package/dist/agent/tokens.js +38 -5
  26. package/dist/agent/tool-guard.d.ts +27 -0
  27. package/dist/agent/tool-guard.js +324 -0
  28. package/dist/agent/types.d.ts +7 -1
  29. package/dist/agent/types.js +1 -1
  30. package/dist/brain/extract.d.ts +11 -0
  31. package/dist/brain/extract.js +154 -0
  32. package/dist/brain/index.d.ts +3 -0
  33. package/dist/brain/index.js +2 -0
  34. package/dist/brain/store.d.ts +42 -0
  35. package/dist/brain/store.js +225 -0
  36. package/dist/brain/types.d.ts +45 -0
  37. package/dist/brain/types.js +5 -0
  38. package/dist/commands/daemon.js +2 -1
  39. package/dist/commands/start.js +16 -3
  40. package/dist/config.js +1 -1
  41. package/dist/index.js +27 -2
  42. package/dist/learnings/extractor.d.ts +13 -0
  43. package/dist/learnings/extractor.js +69 -8
  44. package/dist/learnings/index.d.ts +1 -1
  45. package/dist/learnings/index.js +1 -1
  46. package/dist/learnings/store.js +42 -13
  47. package/dist/learnings/types.d.ts +1 -1
  48. package/dist/mcp/client.d.ts +1 -1
  49. package/dist/mcp/client.js +5 -5
  50. package/dist/mcp/config.d.ts +1 -1
  51. package/dist/mcp/config.js +1 -1
  52. package/dist/panel/html.d.ts +2 -0
  53. package/dist/panel/html.js +409 -146
  54. package/dist/panel/server.js +19 -0
  55. package/dist/pricing.js +3 -2
  56. package/dist/proxy/fallback.d.ts +3 -1
  57. package/dist/proxy/fallback.js +4 -4
  58. package/dist/proxy/server.js +29 -11
  59. package/dist/proxy/sse-translator.js +1 -1
  60. package/dist/router/categories.d.ts +21 -0
  61. package/dist/router/categories.js +96 -0
  62. package/dist/router/index.d.ts +9 -2
  63. package/dist/router/index.js +106 -27
  64. package/dist/router/local-elo.d.ts +32 -0
  65. package/dist/router/local-elo.js +107 -0
  66. package/dist/router/selector.d.ts +46 -0
  67. package/dist/router/selector.js +106 -0
  68. package/dist/session/storage.d.ts +5 -1
  69. package/dist/session/storage.js +24 -2
  70. package/dist/social/a11y.d.ts +1 -1
  71. package/dist/social/a11y.js +5 -1
  72. package/dist/social/browser.d.ts +5 -0
  73. package/dist/social/browser.js +22 -0
  74. package/dist/social/preflight.d.ts +4 -0
  75. package/dist/social/preflight.js +42 -3
  76. package/dist/stats/failures.d.ts +20 -0
  77. package/dist/stats/failures.js +63 -0
  78. package/dist/stats/format.d.ts +6 -0
  79. package/dist/stats/format.js +23 -0
  80. package/dist/stats/insights.js +1 -21
  81. package/dist/stats/session-tracker.d.ts +21 -0
  82. package/dist/stats/session-tracker.js +28 -0
  83. package/dist/stats/tracker.d.ts +1 -1
  84. package/dist/stats/tracker.js +1 -1
  85. package/dist/tools/bash.d.ts +14 -1
  86. package/dist/tools/bash.js +132 -7
  87. package/dist/tools/edit.js +77 -14
  88. package/dist/tools/glob.js +13 -3
  89. package/dist/tools/grep.js +30 -12
  90. package/dist/tools/imagegen.js +3 -3
  91. package/dist/tools/index.d.ts +1 -1
  92. package/dist/tools/index.js +5 -1
  93. package/dist/tools/read.d.ts +16 -2
  94. package/dist/tools/read.js +36 -8
  95. package/dist/tools/searchx.d.ts +6 -2
  96. package/dist/tools/searchx.js +221 -44
  97. package/dist/tools/subagent.js +37 -3
  98. package/dist/tools/task.js +43 -7
  99. package/dist/tools/validate.d.ts +11 -0
  100. package/dist/tools/validate.js +42 -0
  101. package/dist/tools/webfetch.js +18 -7
  102. package/dist/tools/websearch.js +41 -7
  103. package/dist/tools/write.js +26 -6
  104. package/dist/ui/app.js +31 -6
  105. package/dist/ui/model-picker.d.ts +1 -1
  106. package/dist/ui/model-picker.js +1 -1
  107. package/dist/ui/terminal.d.ts +1 -1
  108. package/dist/ui/terminal.js +1 -1
  109. package/package.json +2 -2
@@ -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
- RUNCODE: '1', // Let scripts detect they're running inside runcode
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 < 500)
221
- return; // max 2 updates/sec
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: 'Execute a shell command and return stdout+stderr. Runs in working directory with user env. Output capped at 512KB. Default timeout: 2min, max: 10min (set via timeout param in ms).',
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 shell command to execute' },
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
  };
@@ -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
- // Warn if the file was only partially read editing without full context risks mistakes
32
- const isPartial = partiallyReadFiles.has(resolved);
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
- const idx = contentNormalized.indexOf(normalized);
46
- effectiveOldStr = content.slice(idx, idx + normalized.length);
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
- const partialWarning = isPartial
120
- ? '\nNote: file was only partially read before this edit.'
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: 'Replace exact string in a file. old_string must be unique (or use replace_all).',
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: 'Absolute path' },
139
- old_string: { type: 'string', description: 'Text to find' },
140
- new_string: { type: 'string', description: 'Replacement text' },
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
  },
@@ -143,12 +143,22 @@ async function execute(input, ctx) {
143
143
  export const globCapability = {
144
144
  spec: {
145
145
  name: 'Glob',
146
- description: 'Find files by glob pattern (e.g. "**/*.ts", "src/**/*.tsx"). Returns up to 500 paths sorted by modification time. Skips node_modules, .git, hidden dirs.',
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: 'Glob pattern to match files (e.g. "**/*.ts")' },
151
- path: { type: 'string', description: 'Directory to search in. Defaults to working directory.' },
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
  },
@@ -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 limited = limit > 0 ? lines.slice(0, limit) : lines;
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: 'Search file contents by regex. Default output: file paths. output_mode "content" returns matching lines. Skips node_modules/.git/dist.',
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: 'Regex pattern' },
179
- path: { type: 'string', description: 'File or dir to search (default: cwd)' },
180
- glob: { type: 'string', description: 'File filter e.g. "*.ts"' },
181
- output_mode: { type: 'string', description: '"content" | "files_with_matches" | "count". Default: files_with_matches' },
182
- context: { type: 'number', description: 'Context lines around match' },
183
- before_context: { type: 'number', description: 'Lines before match' },
184
- after_context: { type: 'number', description: 'Lines after match' },
185
- case_insensitive: { type: 'boolean' },
186
- head_limit: { type: 'number', description: 'Max results (default 250)' },
187
- multiline: { type: 'boolean', description: 'Match across lines' },
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
  },
@@ -29,7 +29,7 @@ async function execute(input, ctx) {
29
29
  });
30
30
  const headers = {
31
31
  'Content-Type': 'application/json',
32
- 'User-Agent': `runcode/${VERSION}`,
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(`[runcode] Image payment error: ${err.message}`);
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 AI (DALL-E, etc). Saves the image to a file.',
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: {
@@ -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 runcode agent (excluding sub-agent, which needs config). */
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';
@@ -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
- /** All capabilities available to the runcode agent (excluding sub-agent, which needs config). */
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';
@@ -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
- * Edit tool uses this to warn when editing without full context.
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: Set<string>;
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;
@@ -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
- * Edit tool uses this to warn when editing without full context.
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 Set();
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 — file was not read from the beginning or was truncated
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.add(resolved);
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: 'Read file with line numbers. Use offset/limit for large files.',
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: 'Absolute path' },
82
- offset: { type: 'number', description: 'Start line (1-based)' },
83
- limit: { type: 'number', description: 'Max lines (default 2000)' },
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
  },
@@ -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
- * Requires social config and X login.
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;