@blockrun/franklin 3.6.3 → 3.6.5

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.
@@ -642,18 +642,29 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
642
642
  console.error(`[franklin] Max tokens hit — escalating to ${maxTokensOverride}`);
643
643
  }
644
644
  }
645
- // Append what we got + a continuation prompt (text already streamed)
645
+ // Append what we got + a continuation prompt with last-line anchor
646
646
  const partialAssistant = { role: 'assistant', content: responseParts };
647
+ // Extract last line of output to give the model a concrete resume point
648
+ const textParts = responseParts.filter(p => p.type === 'text');
649
+ const lastTextBlock = textParts[textParts.length - 1];
650
+ let lastLineAnchor = '';
651
+ if (lastTextBlock && lastTextBlock.type === 'text') {
652
+ const lastLine = lastTextBlock.text.split('\n').filter(l => l.trim()).pop() ?? '';
653
+ if (lastLine.length > 10) {
654
+ lastLineAnchor = `\nYour output ended with: "${lastLine.slice(0, 120)}"\nResume immediately after that point.`;
655
+ }
656
+ }
647
657
  const continuationPrompt = {
648
658
  role: 'user',
649
659
  content: [
650
- 'Output token limit hit. Continue with these rules:',
651
- '1. Resume directly no apology, no recap of what you already said. Pick up mid-sentence if that is where the cut happened.',
652
- '2. Do NOT repeat any text or code that was already output above.',
653
- '3. Break remaining work into smaller pieces use multiple tool calls if needed instead of one large output.',
654
- '4. Skip extended reasoning for the continuationfocus on executing.',
655
- '5. If you were in the middle of outputting code, finish the code block first.',
656
- ].join('\n'),
660
+ 'Output token limit hit. Continue:',
661
+ '1. Resume exactly where you stopped your prior output is visible above.',
662
+ '2. Do NOT repeat, summarize, or recap anything already output.',
663
+ '3. If mid-code-block, continue the same block without restarting.',
664
+ '4. Prefer tool calls (Write, Edit) over large text output they are more token-efficient.',
665
+ '5. Be concise skip explanations, focus on completing the work.',
666
+ lastLineAnchor,
667
+ ].filter(l => l).join('\n'),
657
668
  };
658
669
  history.push(partialAssistant);
659
670
  persistSessionMessage(partialAssistant);
@@ -236,6 +236,18 @@ async function runWithInkUI(agentConfig, model, workDir, version, walletInfo, on
236
236
  catch { /* extraction is best-effort */ }
237
237
  }
238
238
  await disconnectMcpServers();
239
+ // Session summary — show cost and usage before goodbye
240
+ try {
241
+ const { getStatsSummary } = await import('../stats/tracker.js');
242
+ const { stats, saved } = getStatsSummary();
243
+ if (stats.totalRequests > 0) {
244
+ const cost = stats.totalCostUsd.toFixed(4);
245
+ const savedStr = saved > 0.001 ? ` · saved $${saved.toFixed(2)} vs Opus` : '';
246
+ const tokens = `${(stats.totalInputTokens / 1000).toFixed(0)}k in / ${(stats.totalOutputTokens / 1000).toFixed(0)}k out`;
247
+ console.log(chalk.dim(`\n Session: ${stats.totalRequests} requests · $${cost} USDC${savedStr} · ${tokens}`));
248
+ }
249
+ }
250
+ catch { /* stats unavailable */ }
239
251
  console.log(chalk.dim('\nGoodbye.\n'));
240
252
  }
241
253
  // ─── Basic readline UI (piped input) ───────────────────────────────────────
@@ -288,6 +300,18 @@ async function runWithBasicUI(agentConfig, model, workDir) {
288
300
  console.error(chalk.red(`\nError: ${err.message}`));
289
301
  }
290
302
  }
303
+ // Session summary for piped mode
304
+ try {
305
+ const { getStatsSummary } = await import('../stats/tracker.js');
306
+ const { stats, saved } = getStatsSummary();
307
+ if (stats.totalRequests > 0) {
308
+ const cost = stats.totalCostUsd.toFixed(4);
309
+ const savedStr = saved > 0.001 ? ` · saved $${saved.toFixed(2)} vs Opus` : '';
310
+ const tokens = `${(stats.totalInputTokens / 1000).toFixed(0)}k in / ${(stats.totalOutputTokens / 1000).toFixed(0)}k out`;
311
+ console.error(`Session: ${stats.totalRequests} requests · $${cost} USDC${savedStr} · ${tokens}`);
312
+ }
313
+ }
314
+ catch { /* stats unavailable */ }
291
315
  ui.printGoodbye();
292
316
  flushStats();
293
317
  }
@@ -93,7 +93,7 @@ async function execute(input, ctx) {
93
93
  const searchTerms = oldStr.split('\n').map(l => l.trim()).filter(l => l.length > 3);
94
94
  const matchedLines = [];
95
95
  if (searchTerms.length > 0) {
96
- for (let i = 0; i < lines.length && matchedLines.length < 5; i++) {
96
+ for (let i = 0; i < lines.length && matchedLines.length < 8; i++) {
97
97
  if (searchTerms.some(term => lines[i].includes(term))) {
98
98
  matchedLines.push({ num: i + 1, text: lines[i] });
99
99
  }
@@ -101,12 +101,18 @@ async function execute(input, ctx) {
101
101
  }
102
102
  let hint;
103
103
  if (matchedLines.length > 0) {
104
- const preview = matchedLines.map(m => `${m.num}\t${m.text}`).join('\n');
105
- hint = `\n\nSimilar lines found:\n${preview}\n\nCheck for whitespace or formatting differences.`;
104
+ // Show matched lines with 1 line of context above for better orientation
105
+ const preview = matchedLines.map(m => {
106
+ const above = m.num > 1 ? ` ${m.num - 1}\t${lines[m.num - 2].slice(0, 80)}\n` : '';
107
+ return `${above}→ ${m.num}\t${m.text}`;
108
+ }).join('\n');
109
+ hint = `\n\nLines containing fragments of your old_string (${matchedLines.length} found):\n${preview}\n\nThe old_string must match EXACTLY — check indentation, quotes, and whitespace. Use Read to see the full region.`;
106
110
  }
107
111
  else {
108
- const preview = lines.slice(0, 10).map((l, i) => `${i + 1}\t${l}`).join('\n');
109
- hint = `\n\nFirst 10 lines of file:\n${preview}`;
112
+ // No matches show the middle of the file (more useful than first 10 lines)
113
+ const mid = Math.max(0, Math.floor(lines.length / 2) - 5);
114
+ const preview = lines.slice(mid, mid + 12).map((l, i) => `${mid + i + 1}\t${l}`).join('\n');
115
+ hint = `\n\nNo matching fragments found in ${lines.length}-line file. Lines ${mid + 1}-${mid + 12}:\n${preview}\n\nUse Read to find the correct text.`;
110
116
  }
111
117
  return {
112
118
  output: `Error: old_string not found in ${resolved}.${hint}`,
@@ -118,9 +118,34 @@ async function execute(input, ctx) {
118
118
  : '';
119
119
  return { output: `No files matched pattern "${pattern}" in ${baseDir}.${hint}` };
120
120
  }
121
- let output = sorted.join('\n');
121
+ // Group by directory for compact output (saves 30-40% tokens on large results)
122
+ let output;
123
+ if (sorted.length > 10) {
124
+ const grouped = new Map();
125
+ for (const p of sorted) {
126
+ const dir = path.dirname(p);
127
+ if (!grouped.has(dir))
128
+ grouped.set(dir, []);
129
+ grouped.get(dir).push(path.basename(p));
130
+ }
131
+ const parts = [];
132
+ for (const [dir, files] of grouped) {
133
+ if (files.length === 1) {
134
+ parts.push(`${dir}/${files[0]}`);
135
+ }
136
+ else {
137
+ parts.push(`${dir}/ (${files.length} files)`);
138
+ for (const f of files)
139
+ parts.push(` ${f}`);
140
+ }
141
+ }
142
+ output = parts.join('\n');
143
+ }
144
+ else {
145
+ output = sorted.join('\n');
146
+ }
122
147
  if (sorted.length >= MAX_RESULTS) {
123
- output += `\n\n... (limited to ${MAX_RESULTS} results. Use a more specific pattern to narrow results.)`;
148
+ output += `\n\n... (limited to ${MAX_RESULTS} results. Use a more specific pattern.)`;
124
149
  }
125
150
  // Cap total output length to prevent context bloat
126
151
  if (output.length > MAX_OUTPUT_CHARS) {
@@ -135,7 +160,7 @@ async function execute(input, ctx) {
135
160
  }
136
161
  const remaining = lines.length - count;
137
162
  if (remaining > 0) {
138
- output = `${trimmed}\n... (${remaining} more paths not shown — use a more specific pattern)`;
163
+ output = `${trimmed}\n... (${remaining} more not shown — use a more specific pattern)`;
139
164
  }
140
165
  }
141
166
  return { output };
@@ -58,7 +58,25 @@ async function execute(input, _ctx) {
58
58
  blocker.blocks.push(task.id);
59
59
  }
60
60
  }
61
- return { output: `Updated task #${task.id} status` };
61
+ // Rich feedback: show status transition and dependency impact
62
+ let feedback = `Updated task #${task.id}`;
63
+ if (status) {
64
+ feedback += ` → ${status}`;
65
+ // If completed, show which tasks are now unblocked
66
+ if (status === 'completed' && task.blocks.length > 0) {
67
+ const nowUnblocked = task.blocks
68
+ .map(id => tasks.find(t => t.id === id))
69
+ .filter(t => t && t.blockedBy.every(bid => {
70
+ const blocker = tasks.find(bt => bt.id === bid);
71
+ return blocker?.status === 'completed';
72
+ }))
73
+ .map(t => `#${t.id} ${t.subject}`);
74
+ if (nowUnblocked.length > 0) {
75
+ feedback += ` — unblocked: ${nowUnblocked.join(', ')}`;
76
+ }
77
+ }
78
+ }
79
+ return { output: feedback };
62
80
  }
63
81
  case 'list': {
64
82
  if (tasks.length === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.6.3",
3
+ "version": "3.6.5",
4
4
  "description": "Franklin — The AI agent with a wallet. Spends USDC autonomously to get real work done. Pay per action, no subscriptions.",
5
5
  "type": "module",
6
6
  "exports": {