@blockrun/franklin 3.23.1 → 3.24.1
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/dist/agent/llm.js +17 -4
- package/dist/commands/start.js +13 -1
- package/dist/mcp/client.d.ts +11 -0
- package/dist/mcp/client.js +32 -1
- package/dist/mcp/codegraph.d.ts +45 -0
- package/dist/mcp/codegraph.js +105 -0
- package/dist/mcp/config.js +8 -0
- package/package.json +2 -1
package/dist/agent/llm.js
CHANGED
|
@@ -79,6 +79,11 @@ function getModelRequestTimeoutMs() {
|
|
|
79
79
|
180_000);
|
|
80
80
|
}
|
|
81
81
|
function getModelStreamIdleTimeoutMs() {
|
|
82
|
+
// Inter-chunk idle budget: the max gap allowed *between* SSE chunks once the
|
|
83
|
+
// stream is flowing. It does NOT cover time-to-first-token — that first read
|
|
84
|
+
// uses the larger request budget (see getModelRequestTimeoutMs + the
|
|
85
|
+
// firstRead branch in parseSSEStream). Conflating the two regressed #74:
|
|
86
|
+
// reasoning models taking 60–120s to first token aborted at this 90s wall.
|
|
82
87
|
return (parseTimeoutEnv('FRANKLIN_MODEL_STREAM_IDLE_TIMEOUT_MS') ??
|
|
83
88
|
parseTimeoutEnv('FRANKLIN_MODEL_IDLE_TIMEOUT_MS') ??
|
|
84
89
|
90_000);
|
|
@@ -597,8 +602,11 @@ export class ModelClient {
|
|
|
597
602
|
yield* this.parseNonStreamingMessage(response, request.model);
|
|
598
603
|
return;
|
|
599
604
|
}
|
|
600
|
-
// Parse SSE stream
|
|
601
|
-
|
|
605
|
+
// Parse SSE stream. The first read waits for time-to-first-token (which
|
|
606
|
+
// the gateway does *not* cover with the request timeout — it flushes SSE
|
|
607
|
+
// headers before the first content chunk), so it gets the larger request
|
|
608
|
+
// budget; subsequent reads use the tighter stream-idle budget.
|
|
609
|
+
yield* this.parseSSEStream(response, requestController, streamTimeoutMs, request.model, requestTimeoutMs);
|
|
602
610
|
}
|
|
603
611
|
finally {
|
|
604
612
|
unlinkAbort();
|
|
@@ -1087,7 +1095,7 @@ export class ModelClient {
|
|
|
1087
1095
|
return header;
|
|
1088
1096
|
}
|
|
1089
1097
|
// ─── SSE Parsing ───────────────────────────────────────────────────────
|
|
1090
|
-
async *parseSSEStream(response, controller, timeoutMs, model) {
|
|
1098
|
+
async *parseSSEStream(response, controller, timeoutMs, model, firstReadTimeoutMs = timeoutMs) {
|
|
1091
1099
|
const reader = response.body?.getReader();
|
|
1092
1100
|
if (!reader) {
|
|
1093
1101
|
yield { kind: 'error', payload: { message: 'No response body' } };
|
|
@@ -1097,12 +1105,17 @@ export class ModelClient {
|
|
|
1097
1105
|
let buffer = '';
|
|
1098
1106
|
// Persist across read() calls — event: and data: may arrive in separate chunks
|
|
1099
1107
|
let currentEvent = '';
|
|
1108
|
+
// The first read waits for time-to-first-token (60–120s for reasoning
|
|
1109
|
+
// models on cache-cold prompts); only later reads measure inter-chunk idle.
|
|
1110
|
+
let firstRead = true;
|
|
1100
1111
|
const MAX_BUFFER = 1_000_000; // 1MB buffer cap
|
|
1101
1112
|
try {
|
|
1102
1113
|
while (true) {
|
|
1103
1114
|
if (controller.signal.aborted)
|
|
1104
1115
|
break;
|
|
1105
|
-
const
|
|
1116
|
+
const budgetMs = firstRead ? firstReadTimeoutMs : timeoutMs;
|
|
1117
|
+
firstRead = false;
|
|
1118
|
+
const { done, value } = await withAbortableTimeout(() => reader.read(), controller, createModelTimeoutError('stream', model, budgetMs), budgetMs);
|
|
1106
1119
|
if (done)
|
|
1107
1120
|
break;
|
|
1108
1121
|
buffer += decoder.decode(value, { stream: true });
|
package/dist/commands/start.js
CHANGED
|
@@ -15,7 +15,8 @@ import { validateToolDescriptions } from '../tools/validate.js';
|
|
|
15
15
|
import { launchInkUI } from '../ui/app.js';
|
|
16
16
|
import { pickModel, resolveModel } from '../ui/model-picker.js';
|
|
17
17
|
import { loadMcpConfig } from '../mcp/config.js';
|
|
18
|
-
import { connectMcpServers, disconnectMcpServers } from '../mcp/client.js';
|
|
18
|
+
import { connectMcpServers, disconnectMcpServers, getMcpServerInstructions } from '../mcp/client.js';
|
|
19
|
+
import { ensureCodegraphIndex } from '../mcp/codegraph.js';
|
|
19
20
|
export async function startCommand(options) {
|
|
20
21
|
const version = options.version ?? '1.0.0';
|
|
21
22
|
// Early-validate explicit resume ID so a typo fails fast — before wallet
|
|
@@ -282,6 +283,10 @@ export async function startCommand(options) {
|
|
|
282
283
|
const systemInstructions = assembleInstructions(workDir, model);
|
|
283
284
|
// Connect MCP servers (non-blocking — add tools if servers are available)
|
|
284
285
|
const mcpConfig = loadMcpConfig(workDir);
|
|
286
|
+
// Kick off the CodeGraph index build (no-op if disabled/absent/already built).
|
|
287
|
+
// Runs in the background so it never blocks startup; the agent falls back to
|
|
288
|
+
// grep/read until the index is ready.
|
|
289
|
+
ensureCodegraphIndex(workDir);
|
|
285
290
|
let mcpTools = [];
|
|
286
291
|
const mcpServerCount = Object.keys(mcpConfig.mcpServers).filter(k => !mcpConfig.mcpServers[k].disabled).length;
|
|
287
292
|
if (mcpServerCount > 0) {
|
|
@@ -290,6 +295,13 @@ export async function startCommand(options) {
|
|
|
290
295
|
if (mcpTools.length > 0) {
|
|
291
296
|
console.log(chalk.dim(` MCP: ${mcpTools.length} tools from ${mcpServerCount} server(s)`));
|
|
292
297
|
}
|
|
298
|
+
// Fold each connected server's playbook (from its initialize response)
|
|
299
|
+
// into the system prompt. For CodeGraph this is what drives the agent to
|
|
300
|
+
// query the index instead of looping grep — the bulk of the savings.
|
|
301
|
+
const mcpInstructions = getMcpServerInstructions();
|
|
302
|
+
if (mcpInstructions) {
|
|
303
|
+
systemInstructions.push(mcpInstructions);
|
|
304
|
+
}
|
|
293
305
|
}
|
|
294
306
|
catch (err) {
|
|
295
307
|
if (options.debug) {
|
package/dist/mcp/client.d.ts
CHANGED
|
@@ -34,6 +34,17 @@ export declare function connectMcpServers(config: McpConfig, debug?: boolean): P
|
|
|
34
34
|
* Disconnect all MCP servers.
|
|
35
35
|
*/
|
|
36
36
|
export declare function disconnectMcpServers(): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Aggregate server-level instructions from all connected MCP servers into a
|
|
39
|
+
* single system-prompt section, or '' if no server supplied any.
|
|
40
|
+
*
|
|
41
|
+
* These come from the `initialize` response of servers Franklin chose to
|
|
42
|
+
* connect (built-in, or user-configured + trusted), so they're treated as
|
|
43
|
+
* trusted guidance rather than untrusted data. The agent reads this once per
|
|
44
|
+
* session to learn each toolset's playbook (which tool for which question,
|
|
45
|
+
* common chains, anti-patterns) instead of rediscovering it by trial.
|
|
46
|
+
*/
|
|
47
|
+
export declare function getMcpServerInstructions(): string;
|
|
37
48
|
/**
|
|
38
49
|
* List connected MCP servers and their tools.
|
|
39
50
|
*/
|
package/dist/mcp/client.js
CHANGED
|
@@ -151,7 +151,12 @@ async function connectStdio(name, config) {
|
|
|
151
151
|
catch {
|
|
152
152
|
// Server doesn't support resources — that's fine, tools-only mode
|
|
153
153
|
}
|
|
154
|
-
|
|
154
|
+
// Server-level instructions from the initialize response. MCP servers use
|
|
155
|
+
// this to tell the agent HOW to use their tools (selection-by-intent, common
|
|
156
|
+
// chains, anti-patterns) — e.g. CodeGraph's "answer directly, don't grep to
|
|
157
|
+
// re-verify" playbook, which is where most of its tool-call savings come from.
|
|
158
|
+
const instructions = (client.getInstructions() || '').trim() || undefined;
|
|
159
|
+
const connected = { name, client, transport, tools: capabilities, instructions };
|
|
155
160
|
connections.set(name, connected);
|
|
156
161
|
return connected;
|
|
157
162
|
}
|
|
@@ -210,6 +215,32 @@ export async function disconnectMcpServers() {
|
|
|
210
215
|
connections.delete(name);
|
|
211
216
|
}
|
|
212
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Aggregate server-level instructions from all connected MCP servers into a
|
|
220
|
+
* single system-prompt section, or '' if no server supplied any.
|
|
221
|
+
*
|
|
222
|
+
* These come from the `initialize` response of servers Franklin chose to
|
|
223
|
+
* connect (built-in, or user-configured + trusted), so they're treated as
|
|
224
|
+
* trusted guidance rather than untrusted data. The agent reads this once per
|
|
225
|
+
* session to learn each toolset's playbook (which tool for which question,
|
|
226
|
+
* common chains, anti-patterns) instead of rediscovering it by trial.
|
|
227
|
+
*/
|
|
228
|
+
export function getMcpServerInstructions() {
|
|
229
|
+
const blocks = [];
|
|
230
|
+
for (const [name, conn] of connections) {
|
|
231
|
+
if (conn.instructions) {
|
|
232
|
+
blocks.push(`### MCP server: ${name}\n${conn.instructions}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (blocks.length === 0)
|
|
236
|
+
return '';
|
|
237
|
+
return [
|
|
238
|
+
'## Connected MCP tool playbooks',
|
|
239
|
+
'Each connected MCP server below provides guidance on how to use its tools effectively. Follow these playbooks when those tools are relevant.',
|
|
240
|
+
'',
|
|
241
|
+
blocks.join('\n\n'),
|
|
242
|
+
].join('\n');
|
|
243
|
+
}
|
|
213
244
|
/**
|
|
214
245
|
* List connected MCP servers and their tools.
|
|
215
246
|
*/
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeGraph built-in MCP server integration.
|
|
3
|
+
*
|
|
4
|
+
* CodeGraph (https://github.com/colbymchenry/codegraph, MIT) builds a local
|
|
5
|
+
* SQLite knowledge graph of a repo's symbols, call edges, and files via
|
|
6
|
+
* tree-sitter, then serves it over MCP. For Franklin this is a direct USDC win:
|
|
7
|
+
* agents answer "how does X work / what calls Y / trace this flow" from the
|
|
8
|
+
* pre-built index instead of looping grep + read, which cuts tool calls (and
|
|
9
|
+
* therefore paid LLM round-trips) sharply on real codebases.
|
|
10
|
+
*
|
|
11
|
+
* Shipped as a dependency, so `franklin` users get it with no extra install.
|
|
12
|
+
* The npm package is a thin shim (`npm-shim.js`) that locates a per-platform
|
|
13
|
+
* bundle (vendored Node 24 + app) and execs it — so we always launch it via
|
|
14
|
+
* the user's own node against the shim, never a global `codegraph` on PATH.
|
|
15
|
+
*
|
|
16
|
+
* Opt out with FRANKLIN_CODEGRAPH=0 (or "false").
|
|
17
|
+
*/
|
|
18
|
+
import type { McpServerConfig } from './client.js';
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the CodeGraph npm shim entry point, or null if the dependency
|
|
21
|
+
* isn't installed / resolvable. The shim is plain JS runnable by any node.
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveCodegraphShim(): string | null;
|
|
24
|
+
/** Whether CodeGraph is available and not disabled. */
|
|
25
|
+
export declare function isCodegraphEnabled(): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Build the built-in MCP server config for CodeGraph, pinned to `workDir`.
|
|
28
|
+
*
|
|
29
|
+
* We launch via the user's node + the shim (the shim re-execs the bundled
|
|
30
|
+
* Node 24 runtime internally). `--path` is required because Franklin's MCP
|
|
31
|
+
* client doesn't advertise a `roots` capability, so the server can't infer
|
|
32
|
+
* the project from a rootUri — without it CodeGraph wouldn't know which repo
|
|
33
|
+
* to index. Returns null when CodeGraph is unavailable or disabled.
|
|
34
|
+
*/
|
|
35
|
+
export declare function getCodegraphServerConfig(workDir: string): McpServerConfig | null;
|
|
36
|
+
/**
|
|
37
|
+
* Build the initial index for `workDir` if it has no `.codegraph/` yet.
|
|
38
|
+
*
|
|
39
|
+
* Non-blocking: spawns `codegraph init <workDir> -i` detached and returns
|
|
40
|
+
* immediately. The serving MCP process watches the project, so it picks up
|
|
41
|
+
* the freshly built index; until it's ready, codegraph tools report
|
|
42
|
+
* "not initialized" and the agent falls back to grep/read (no regression).
|
|
43
|
+
* No-op when CodeGraph is disabled, unavailable, or already initialized.
|
|
44
|
+
*/
|
|
45
|
+
export declare function ensureCodegraphIndex(workDir: string): void;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeGraph built-in MCP server integration.
|
|
3
|
+
*
|
|
4
|
+
* CodeGraph (https://github.com/colbymchenry/codegraph, MIT) builds a local
|
|
5
|
+
* SQLite knowledge graph of a repo's symbols, call edges, and files via
|
|
6
|
+
* tree-sitter, then serves it over MCP. For Franklin this is a direct USDC win:
|
|
7
|
+
* agents answer "how does X work / what calls Y / trace this flow" from the
|
|
8
|
+
* pre-built index instead of looping grep + read, which cuts tool calls (and
|
|
9
|
+
* therefore paid LLM round-trips) sharply on real codebases.
|
|
10
|
+
*
|
|
11
|
+
* Shipped as a dependency, so `franklin` users get it with no extra install.
|
|
12
|
+
* The npm package is a thin shim (`npm-shim.js`) that locates a per-platform
|
|
13
|
+
* bundle (vendored Node 24 + app) and execs it — so we always launch it via
|
|
14
|
+
* the user's own node against the shim, never a global `codegraph` on PATH.
|
|
15
|
+
*
|
|
16
|
+
* Opt out with FRANKLIN_CODEGRAPH=0 (or "false").
|
|
17
|
+
*/
|
|
18
|
+
import { createRequire } from 'node:module';
|
|
19
|
+
import { spawn } from 'node:child_process';
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { logger } from '../logger.js';
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
/** True unless the user explicitly disabled CodeGraph via env. */
|
|
25
|
+
function userEnabled() {
|
|
26
|
+
const v = (process.env.FRANKLIN_CODEGRAPH || '').toLowerCase();
|
|
27
|
+
return v !== '0' && v !== 'false' && v !== 'off';
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolve the CodeGraph npm shim entry point, or null if the dependency
|
|
31
|
+
* isn't installed / resolvable. The shim is plain JS runnable by any node.
|
|
32
|
+
*/
|
|
33
|
+
export function resolveCodegraphShim() {
|
|
34
|
+
try {
|
|
35
|
+
const shim = require.resolve('@colbymchenry/codegraph/npm-shim.js');
|
|
36
|
+
return fs.existsSync(shim) ? shim : null;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Whether CodeGraph is available and not disabled. */
|
|
43
|
+
export function isCodegraphEnabled() {
|
|
44
|
+
return userEnabled() && resolveCodegraphShim() !== null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Build the built-in MCP server config for CodeGraph, pinned to `workDir`.
|
|
48
|
+
*
|
|
49
|
+
* We launch via the user's node + the shim (the shim re-execs the bundled
|
|
50
|
+
* Node 24 runtime internally). `--path` is required because Franklin's MCP
|
|
51
|
+
* client doesn't advertise a `roots` capability, so the server can't infer
|
|
52
|
+
* the project from a rootUri — without it CodeGraph wouldn't know which repo
|
|
53
|
+
* to index. Returns null when CodeGraph is unavailable or disabled.
|
|
54
|
+
*/
|
|
55
|
+
export function getCodegraphServerConfig(workDir) {
|
|
56
|
+
if (!userEnabled())
|
|
57
|
+
return null;
|
|
58
|
+
const shim = resolveCodegraphShim();
|
|
59
|
+
if (!shim)
|
|
60
|
+
return null;
|
|
61
|
+
return {
|
|
62
|
+
transport: 'stdio',
|
|
63
|
+
command: process.execPath,
|
|
64
|
+
args: [shim, 'serve', '--mcp', '--path', workDir],
|
|
65
|
+
label: 'CodeGraph (built-in)',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Build the initial index for `workDir` if it has no `.codegraph/` yet.
|
|
70
|
+
*
|
|
71
|
+
* Non-blocking: spawns `codegraph init <workDir> -i` detached and returns
|
|
72
|
+
* immediately. The serving MCP process watches the project, so it picks up
|
|
73
|
+
* the freshly built index; until it's ready, codegraph tools report
|
|
74
|
+
* "not initialized" and the agent falls back to grep/read (no regression).
|
|
75
|
+
* No-op when CodeGraph is disabled, unavailable, or already initialized.
|
|
76
|
+
*/
|
|
77
|
+
export function ensureCodegraphIndex(workDir) {
|
|
78
|
+
if (!isCodegraphEnabled())
|
|
79
|
+
return;
|
|
80
|
+
const indexDir = path.join(workDir, '.codegraph');
|
|
81
|
+
if (fs.existsSync(indexDir))
|
|
82
|
+
return; // already initialized — watcher keeps it fresh
|
|
83
|
+
const shim = resolveCodegraphShim();
|
|
84
|
+
if (!shim)
|
|
85
|
+
return;
|
|
86
|
+
try {
|
|
87
|
+
const child = spawn(process.execPath, [shim, 'init', workDir, '-i'], {
|
|
88
|
+
cwd: workDir,
|
|
89
|
+
// Discard output: this is best-effort background indexing. Failures are
|
|
90
|
+
// non-fatal — the agent simply keeps using grep/read until (and if) the
|
|
91
|
+
// index appears. Surfacing a stack trace here would just be noise.
|
|
92
|
+
stdio: 'ignore',
|
|
93
|
+
detached: true,
|
|
94
|
+
});
|
|
95
|
+
child.on('error', (err) => {
|
|
96
|
+
logger.debug(`[franklin] codegraph index build failed: ${err.message}`);
|
|
97
|
+
});
|
|
98
|
+
// Don't keep the event loop alive waiting on the indexer.
|
|
99
|
+
child.unref();
|
|
100
|
+
logger.info(`[franklin] CodeGraph: building initial index for ${workDir}`);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
logger.debug(`[franklin] codegraph index spawn error: ${err.message}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
package/dist/mcp/config.js
CHANGED
|
@@ -8,6 +8,7 @@ import fs from 'node:fs';
|
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { execSync } from 'node:child_process';
|
|
10
10
|
import { BLOCKRUN_DIR } from '../config.js';
|
|
11
|
+
import { getCodegraphServerConfig } from './codegraph.js';
|
|
11
12
|
const GLOBAL_MCP_FILE = path.join(BLOCKRUN_DIR, 'mcp.json');
|
|
12
13
|
/**
|
|
13
14
|
* Load MCP server configurations from global + project files.
|
|
@@ -46,6 +47,13 @@ export function loadMcpConfig(workDir) {
|
|
|
46
47
|
servers[name] = config;
|
|
47
48
|
}
|
|
48
49
|
}
|
|
50
|
+
// Built-in CodeGraph: shipped as a dependency (not on PATH), so it's
|
|
51
|
+
// resolved + pinned to this workDir rather than probed via `which`.
|
|
52
|
+
// User config below can still override or disable it.
|
|
53
|
+
const codegraph = getCodegraphServerConfig(workDir);
|
|
54
|
+
if (codegraph) {
|
|
55
|
+
servers.codegraph = codegraph;
|
|
56
|
+
}
|
|
49
57
|
// 1. Global config
|
|
50
58
|
try {
|
|
51
59
|
if (fs.existsSync(GLOBAL_MCP_FILE)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blockrun/franklin",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.24.1",
|
|
4
4
|
"description": "Franklin Agent — 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": {
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
69
|
"@blockrun/llm": "^2.0.0",
|
|
70
|
+
"@colbymchenry/codegraph": "^0.9.7",
|
|
70
71
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
71
72
|
"@solana/spl-token": "^0.4.14",
|
|
72
73
|
"@solana/web3.js": "^1.98.4",
|