@blockrun/franklin 3.23.1 → 3.24.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.
@@ -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) {
@@ -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
  */
@@ -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
- const connected = { name, client, transport, tools: capabilities };
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
+ }
@@ -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.23.1",
3
+ "version": "3.24.0",
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",