Skip to content

Commit 09c2595

Browse files
author
1bcMax
committed
fix: end-to-end audit — 5 silent-charge + branding bugs
Audit hunted every remaining place a user could be surprised by cost or see stale branding. Five real bugs found & fixed: 1. plugin-sdk/workflow.ts: DEFAULT_MODEL_TIERS.cheap was zai/glm-5.1 ($0.001/call paid). Any plugin using the 'cheap' tier silently charged users. Now nemotron (free). 2. pricing.ts:94: estimateCost defaulted unknown models to {input: 2.0, output: 10.0} — fake cost accumulating in UI for any model not in the pricing map. Now {input: 0, output: 0} so unknowns conservatively show as free. Real charges still tracked on-chain in cost_log.jsonl. 3. tools/moa.ts: AGGREGATOR_MODEL constant was claude-sonnet-4.6. Defense in depth: aggregator now defaults free; users explicitly pass aggregator='sonnet' to upgrade. 4. commands/init.ts: LaunchAgent label was ai.blockrun.runcode.plist and messaging said 'runcode'. Now ai.blockrun.franklin.plist with franklin messaging; legacy plist auto-unloaded & removed on init. uninit.ts handles both old + new plists. 5. User-facing strings across src/ — 'runcode setup', 'runcode proxy', 'runcode stats', 'runcode start --model', etc. now say 'franklin'.
1 parent 6e7bce1 commit 09c2595

23 files changed

Lines changed: 145 additions & 100 deletions

File tree

dist/commands/init.js

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import chalk from 'chalk';
55
import { DEFAULT_PROXY_PORT } from '../config.js';
66
const CLAUDE_SETTINGS_FILE = path.join(os.homedir(), '.claude', 'settings.json');
77
const LAUNCH_AGENT_DIR = path.join(os.homedir(), 'Library', 'LaunchAgents');
8-
const LAUNCH_AGENT_PLIST = path.join(LAUNCH_AGENT_DIR, 'ai.blockrun.runcode.plist');
8+
const LAUNCH_AGENT_PLIST = path.join(LAUNCH_AGENT_DIR, 'ai.blockrun.franklin.plist');
9+
const LEGACY_LAUNCH_AGENT_PLIST = path.join(LAUNCH_AGENT_DIR, 'ai.blockrun.runcode.plist');
910
export async function initCommand(options) {
1011
const port = parseInt(options.port || String(DEFAULT_PROXY_PORT));
1112
if (isNaN(port) || port < 1 || port > 65535) {
@@ -36,24 +37,43 @@ export async function initCommand(options) {
3637
console.log(chalk.green(`✓ Configured ${CLAUDE_SETTINGS_FILE}`));
3738
// ── 2. Install macOS LaunchAgent (auto-start on login) ─────────────────
3839
if (process.platform === 'darwin') {
39-
let runcodeBin = '';
40+
// Clean up legacy runcode LaunchAgent if present
41+
if (fs.existsSync(LEGACY_LAUNCH_AGENT_PLIST)) {
42+
try {
43+
const { execSync } = await import('node:child_process');
44+
execSync(`launchctl unload -w "${LEGACY_LAUNCH_AGENT_PLIST}"`, { stdio: 'pipe' });
45+
}
46+
catch { /* may not be loaded */ }
47+
try {
48+
fs.unlinkSync(LEGACY_LAUNCH_AGENT_PLIST);
49+
}
50+
catch { /* best effort */ }
51+
}
52+
let franklinBin = '';
4053
try {
4154
const { execSync } = await import('node:child_process');
42-
runcodeBin = execSync('which runcode', { encoding: 'utf-8' }).trim();
55+
franklinBin = execSync('which franklin', { encoding: 'utf-8' }).trim();
4356
}
4457
catch {
45-
console.log(chalk.yellow(' Warning: runcode not found in PATH — LaunchAgent not installed.'));
58+
// Fall back to legacy binary name
59+
try {
60+
const { execSync } = await import('node:child_process');
61+
franklinBin = execSync('which runcode', { encoding: 'utf-8' }).trim();
62+
}
63+
catch {
64+
console.log(chalk.yellow(' Warning: franklin not found in PATH — LaunchAgent not installed.'));
65+
}
4666
}
47-
if (runcodeBin) {
67+
if (franklinBin) {
4868
const plist = `<?xml version="1.0" encoding="UTF-8"?>
4969
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
5070
<plist version="1.0">
5171
<dict>
5272
<key>Label</key>
53-
<string>ai.blockrun.runcode</string>
73+
<string>ai.blockrun.franklin</string>
5474
<key>ProgramArguments</key>
5575
<array>
56-
<string>${runcodeBin}</string>
76+
<string>${franklinBin}</string>
5777
<string>proxy</string>
5878
<string>--port</string>
5979
<string>${port}</string>
@@ -73,7 +93,7 @@ export async function initCommand(options) {
7393
try {
7494
const { execSync } = await import('node:child_process');
7595
execSync(`launchctl load -w "${LAUNCH_AGENT_PLIST}"`, { stdio: 'pipe' });
76-
console.log(chalk.green(`✓ LaunchAgent installed — runcode proxy starts automatically on login`));
96+
console.log(chalk.green(`✓ LaunchAgent installed — franklin proxy starts automatically on login`));
7797
}
7898
catch {
7999
console.log(chalk.dim(` LaunchAgent written to ${LAUNCH_AGENT_PLIST}`));
@@ -83,10 +103,10 @@ export async function initCommand(options) {
83103
}
84104
// ── 3. Start daemon now ──────────────────────────────────────────────────
85105
console.log('');
86-
console.log(chalk.bold('runcode initialized (proxy mode for Claude Code).'));
87-
console.log(`Run ${chalk.bold('runcode daemon start')} to start the background proxy now.`);
88-
console.log(`Then just run ${chalk.bold('claude')}runcode proxy handles payments automatically.`);
106+
console.log(chalk.bold('franklin initialized (proxy mode for Claude Code).'));
107+
console.log(`Run ${chalk.bold('franklin daemon start')} to start the background proxy now.`);
108+
console.log(`Then just run ${chalk.bold('claude')}franklin proxy handles payments automatically.`);
89109
console.log('');
90-
console.log(chalk.dim('Or use runcode directly: runcode start'));
110+
console.log(chalk.dim('Or use franklin directly: franklin start'));
91111
console.log(chalk.dim('Note: Claude Code will ask you to trust the proxy URL once.'));
92112
}

dist/commands/models.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export async function modelsCommand() {
4141
const ctx = '';
4242
console.log(` ${chalk.cyan(m.id.padEnd(35))} ${input.padEnd(12)} ${output.padEnd(12)} ${ctx}`);
4343
}
44-
console.log(`\n${chalk.dim(`${models.length} models available. Use:`)} ${chalk.bold('runcode start --model <model-id>')}`);
44+
console.log(`\n${chalk.dim(`${models.length} models available. Use:`)} ${chalk.bold('franklin start --model <model-id>')}`);
4545
}
4646
catch (err) {
4747
const msg = err instanceof Error ? err.message : 'unknown error';

dist/commands/proxy.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export async function proxyCommand(options) {
2525
if (wallet.isNew) {
2626
console.log(chalk.yellow('No Solana wallet found — created a new one.'));
2727
console.log(`Address: ${chalk.cyan(wallet.address)}`);
28-
console.log(`\nSend USDC on Solana to this address, then run ${chalk.bold('runcode proxy')} again.\n`);
28+
console.log(`\nSend USDC on Solana to this address, then run ${chalk.bold('franklin proxy')} again.\n`);
2929
return;
3030
}
3131
printBanner(version);
@@ -52,7 +52,7 @@ export async function proxyCommand(options) {
5252
if (wallet.isNew) {
5353
console.log(chalk.yellow('No wallet found — created a new one.'));
5454
console.log(`Address: ${chalk.cyan(wallet.address)}`);
55-
console.log(`\nSend USDC on Base to this address, then run ${chalk.bold('runcode proxy')} again.\n`);
55+
console.log(`\nSend USDC on Base to this address, then run ${chalk.bold('franklin proxy')} again.\n`);
5656
return;
5757
}
5858
printBanner(version);

dist/commands/stats.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ export function statsCommand(options) {
2626
return;
2727
}
2828
// Pretty output
29-
console.log(chalk.bold('\n📊 runcode Usage Statistics\n'));
29+
console.log(chalk.bold('\n📊 Franklin Usage Statistics\n'));
3030
console.log('─'.repeat(55));
3131
if (stats.totalRequests === 0) {
32-
console.log(chalk.gray('\n No requests recorded yet. Start using runcode!\n'));
32+
console.log(chalk.gray('\n No requests recorded yet. Start using franklin!\n'));
3333
console.log('─'.repeat(55) + '\n');
3434
return;
3535
}

dist/commands/uninit.js

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import path from 'node:path';
33
import os from 'node:os';
44
import chalk from 'chalk';
55
const CLAUDE_SETTINGS_FILE = path.join(os.homedir(), '.claude', 'settings.json');
6-
const LAUNCH_AGENT_PLIST = path.join(os.homedir(), 'Library', 'LaunchAgents', 'ai.blockrun.runcode.plist');
6+
const LAUNCH_AGENT_PLISTS = [
7+
path.join(os.homedir(), 'Library', 'LaunchAgents', 'ai.blockrun.franklin.plist'),
8+
path.join(os.homedir(), 'Library', 'LaunchAgents', 'ai.blockrun.runcode.plist'), // legacy
9+
];
710
export async function uninitCommand() {
811
let changed = false;
912
// ── 1. Remove env section from ~/.claude/settings.json ──────────────────
@@ -31,7 +34,7 @@ export async function uninitCommand() {
3134
delete settings.env;
3235
if (removed) {
3336
fs.writeFileSync(CLAUDE_SETTINGS_FILE, JSON.stringify(settings, null, 2));
34-
console.log(chalk.green(`✓ Removed runcode env from ${CLAUDE_SETTINGS_FILE}`));
37+
console.log(chalk.green(`✓ Removed franklin env from ${CLAUDE_SETTINGS_FILE}`));
3538
changed = true;
3639
}
3740
}
@@ -40,24 +43,28 @@ export async function uninitCommand() {
4043
catch (e) {
4144
console.log(chalk.yellow(`Could not update settings.json: ${e.message}`));
4245
}
43-
// ── 2. Unload and remove LaunchAgent ────────────────────────────────────
44-
if (process.platform === 'darwin' && fs.existsSync(LAUNCH_AGENT_PLIST)) {
45-
try {
46-
const { execSync } = await import('node:child_process');
47-
execSync(`launchctl unload -w "${LAUNCH_AGENT_PLIST}"`, { stdio: 'pipe' });
46+
// ── 2. Unload and remove LaunchAgent(s) — new + legacy ─────────────────
47+
if (process.platform === 'darwin') {
48+
for (const plist of LAUNCH_AGENT_PLISTS) {
49+
if (fs.existsSync(plist)) {
50+
try {
51+
const { execSync } = await import('node:child_process');
52+
execSync(`launchctl unload -w "${plist}"`, { stdio: 'pipe' });
53+
}
54+
catch { /* already unloaded */ }
55+
fs.unlinkSync(plist);
56+
console.log(chalk.green(`✓ Removed LaunchAgent: ${path.basename(plist)}`));
57+
changed = true;
58+
}
4859
}
49-
catch { /* already unloaded */ }
50-
fs.unlinkSync(LAUNCH_AGENT_PLIST);
51-
console.log(chalk.green(`✓ Removed LaunchAgent`));
52-
changed = true;
5360
}
5461
if (!changed) {
55-
console.log(chalk.dim('Nothing to uninit — runcode was not initialized.'));
62+
console.log(chalk.dim('Nothing to uninit — franklin was not initialized.'));
5663
}
5764
else {
5865
console.log('');
59-
console.log(chalk.bold('runcode uninitialized.'));
66+
console.log(chalk.bold('franklin uninitialized.'));
6067
console.log(`Claude Code will use its default Anthropic API settings again.`);
61-
console.log(`Run ${chalk.bold('runcode daemon stop')} to stop any running proxy.`);
68+
console.log(`Run ${chalk.bold('franklin daemon stop')} to stop any running proxy.`);
6269
}
6370
}

dist/index.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ program
3737
.action((chain) => setupCommand(chain));
3838
program
3939
.command('start')
40-
.description('Start the runcode agent')
40+
.description('Start the franklin agent')
4141
.option('-m, --model <model>', 'Model to use (e.g. openai/gpt-5.4, anthropic/claude-sonnet-4.6). Default from config or claude-sonnet-4.6')
4242
.option('--debug', 'Enable debug logging')
4343
.option('--trust', 'Trust mode — skip permission prompts for all tools')
@@ -52,16 +52,16 @@ program
5252
.action((options) => proxyCommand({ ...options, version }));
5353
program
5454
.command('init')
55-
.description('Configure runcode auto-start (writes ~/.claude/settings.json + installs LaunchAgent on macOS)')
55+
.description('Configure franklin auto-start (writes ~/.claude/settings.json + installs LaunchAgent on macOS)')
5656
.option('-p, --port <port>', 'Proxy port', '8402')
5757
.action((options) => initCommand(options));
5858
program
5959
.command('uninit')
60-
.description('Remove runcode configuration and uninstall LaunchAgent')
60+
.description('Remove franklin configuration and uninstall LaunchAgent')
6161
.action(() => uninitCommand());
6262
program
6363
.command('daemon <action>')
64-
.description('Manage runcode background proxy (start|stop|status)')
64+
.description('Manage franklin background proxy (start|stop|status)')
6565
.option('-p, --port <port>', 'Proxy port', '8402')
6666
.action((action, options) => daemonCommand(action, options));
6767
program
@@ -82,7 +82,7 @@ program
8282
.action(balanceCommand);
8383
program
8484
.command('config <action> [key] [value]')
85-
.description('Manage runcode config (set, get, unset, list)\n' +
85+
.description('Manage franklin config (set, get, unset, list)\n' +
8686
'Keys: default-model, sonnet-model, opus-model, haiku-model, smart-routing')
8787
.action(configCommand);
8888
program

dist/plugin-sdk/workflow.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@
66
*/
77
export const DEFAULT_MODEL_TIERS = {
88
free: 'nvidia/nemotron-ultra-253b',
9-
cheap: 'zai/glm-5.1',
9+
cheap: 'nvidia/nemotron-ultra-253b', // Was glm-5.1 ($0.001/call). Free by default; opt-in to paid.
1010
premium: 'anthropic/claude-sonnet-4.6',
1111
};

dist/pricing.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,10 @@ export const OPUS_PRICING = MODEL_PRICING['anthropic/claude-opus-4.6'];
8383
* For per-call models (perCall > 0), uses flat per-call pricing instead of per-token.
8484
*/
8585
export function estimateCost(model, inputTokens, outputTokens, calls = 1) {
86-
const pricing = MODEL_PRICING[model] || { input: 2.0, output: 10.0 };
86+
// Unknown models: assume free (0). Prevents false cost accumulation in the UI
87+
// for models not yet listed — better to under-estimate than scare users with
88+
// fake charges. Real on-chain charges are tracked separately in cost_log.jsonl.
89+
const pricing = MODEL_PRICING[model] || { input: 0, output: 0 };
8790
if (pricing.perCall) {
8891
return pricing.perCall * calls;
8992
}

dist/session/search.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,6 @@ export function formatSearchResults(matches, query) {
224224
lines.push(` [${m.matchedRole}] ${m.snippet}`);
225225
lines.push('');
226226
}
227-
lines.push(` Resume: runcode (then /resume <session-id>)\n`);
227+
lines.push(` Resume: franklin (then /resume <session-id>)\n`);
228228
return lines.join('\n');
229229
}

dist/tools/moa.js

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ const REFERENCE_MODELS = [
1919
'google/gemini-2.5-flash', // Fast, cheap
2020
'deepseek/deepseek-chat', // Cheap, good reasoning
2121
];
22-
/** Aggregator model — strong model that synthesizes the best answer. */
23-
const AGGREGATOR_MODEL = 'anthropic/claude-sonnet-4.6';
22+
/** Aggregator model — free by default. Users explicitly pass `aggregator` to upgrade. */
23+
const AGGREGATOR_MODEL = 'nvidia/nemotron-ultra-253b';
2424
/** Max tokens per reference response. */
2525
const REFERENCE_MAX_TOKENS = 4096;
2626
/** Max tokens for aggregator. */
@@ -38,11 +38,8 @@ async function execute(input, ctx) {
3838
return { output: 'Error: prompt is required', isError: true };
3939
}
4040
const referenceModels = models || REFERENCE_MODELS;
41-
// If parent agent is on a free model, default aggregator to a free model too
42-
// so MoA doesn't silently charge the user. Explicit `aggregator` arg wins.
43-
const parentIsFree = registeredParentModel.startsWith('nvidia/') ||
44-
registeredParentModel === 'blockrun/free';
45-
const aggregatorModel = aggregator || (parentIsFree ? 'nvidia/nemotron-ultra-253b' : AGGREGATOR_MODEL);
41+
// Aggregator defaults to free. Pass `aggregator: 'sonnet'` to explicitly upgrade.
42+
const aggregatorModel = aggregator || AGGREGATOR_MODEL;
4643
const client = new ModelClient({
4744
apiUrl: registeredApiUrl,
4845
chain: registeredChain,

0 commit comments

Comments
 (0)