@blockrun/franklin 3.15.99 → 3.15.101

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.
@@ -30,6 +30,21 @@ export declare class PermissionManager {
30
30
  * Returns true if allowed, false if denied.
31
31
  */
32
32
  promptUser(toolName: string, input: Record<string, unknown>, pendingCount?: number): Promise<boolean>;
33
+ /**
34
+ * Persist a tool name to the user's allow rules so future sessions
35
+ * skip the prompt. Idempotent: appends to the existing
36
+ * `allow: []` array only if not already present.
37
+ *
38
+ * Why this exists: pre-2026-05-12, "always" in the UI prompt was a
39
+ * misnomer — it only added the tool to the in-memory `sessionAllowed`
40
+ * Set, which evaporated on every `franklin start`. Users reported
41
+ * being prompted repeatedly across sessions despite hitting [a] each
42
+ * time. Persistence here makes "always" actually mean always.
43
+ *
44
+ * Best-effort writes (try/catch around fs) — a logging failure should
45
+ * never block the paid call that just got approved.
46
+ */
47
+ private persistAllowRule;
33
48
  private loadRules;
34
49
  private matchesRule;
35
50
  private getPrimaryInputValue;
@@ -122,7 +122,15 @@ export class PermissionManager {
122
122
  if (this.promptFn) {
123
123
  const result = await this.promptFn(toolName, hint);
124
124
  if (result === 'always') {
125
+ // "Always" must mean ALWAYS — including after the user restarts
126
+ // franklin. Pre-fix it was only in-memory: every `franklin start`
127
+ // re-asked the same prompts even after the user had pressed [a]
128
+ // explicitly. Verified 2026-05-12: ~/.blockrun/franklin-permissions.json
129
+ // was non-existent despite the user reporting repeated prompts.
130
+ // Persist to disk and update in-memory rules so subsequent
131
+ // checks short-circuit at the allow-rule stage (line 124).
125
132
  this.sessionAllowed.add(toolName);
133
+ this.persistAllowRule(toolName);
126
134
  return true;
127
135
  }
128
136
  return result === 'yes';
@@ -140,7 +148,8 @@ export class PermissionManager {
140
148
  const normalized = answer.trim().toLowerCase();
141
149
  if (normalized === 'a' || normalized === 'always') {
142
150
  this.sessionAllowed.add(toolName);
143
- console.error(chalk.green(` ✓ ${toolName} allowed for this session`));
151
+ this.persistAllowRule(toolName);
152
+ console.error(chalk.green(` ✓ ${toolName} allowed (saved to ~/.blockrun/franklin-permissions.json)`));
144
153
  return true;
145
154
  }
146
155
  if (normalized === 'y' || normalized === 'yes' || normalized === '') {
@@ -150,6 +159,50 @@ export class PermissionManager {
150
159
  return false;
151
160
  }
152
161
  // ─── Internal ──────────────────────────────────────────────────────────
162
+ /**
163
+ * Persist a tool name to the user's allow rules so future sessions
164
+ * skip the prompt. Idempotent: appends to the existing
165
+ * `allow: []` array only if not already present.
166
+ *
167
+ * Why this exists: pre-2026-05-12, "always" in the UI prompt was a
168
+ * misnomer — it only added the tool to the in-memory `sessionAllowed`
169
+ * Set, which evaporated on every `franklin start`. Users reported
170
+ * being prompted repeatedly across sessions despite hitting [a] each
171
+ * time. Persistence here makes "always" actually mean always.
172
+ *
173
+ * Best-effort writes (try/catch around fs) — a logging failure should
174
+ * never block the paid call that just got approved.
175
+ */
176
+ persistAllowRule(toolName) {
177
+ const configPath = path.join(BLOCKRUN_DIR, 'franklin-permissions.json');
178
+ try {
179
+ // Read current state (may not exist). Treat missing/malformed as
180
+ // empty rules — never throw on the user's tool execution path.
181
+ let current = { allow: [], deny: [], ask: [] };
182
+ if (fs.existsSync(configPath)) {
183
+ try {
184
+ const raw = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
185
+ current = {
186
+ allow: Array.isArray(raw.allow) ? raw.allow : [],
187
+ deny: Array.isArray(raw.deny) ? raw.deny : [],
188
+ ask: Array.isArray(raw.ask) ? raw.ask : [],
189
+ };
190
+ }
191
+ catch { /* malformed — reset */ }
192
+ }
193
+ if (current.allow.includes(toolName))
194
+ return; // already saved
195
+ current.allow.push(toolName);
196
+ // Update in-memory rules too so subsequent checks short-circuit
197
+ // at line 124 (matchesRule against allow) without re-prompting.
198
+ if (!this.rules.allow.includes(toolName)) {
199
+ this.rules.allow.push(toolName);
200
+ }
201
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
202
+ fs.writeFileSync(configPath, JSON.stringify(current, null, 2));
203
+ }
204
+ catch { /* best-effort */ }
205
+ }
153
206
  loadRules() {
154
207
  const configPath = path.join(BLOCKRUN_DIR, 'franklin-permissions.json');
155
208
  const legacyPath = path.join(BLOCKRUN_DIR, 'runcode-permissions.json');
package/dist/banner.js CHANGED
@@ -102,7 +102,7 @@ export function printBanner(version) {
102
102
  const update = getAvailableUpdate();
103
103
  if (update) {
104
104
  console.log(chalk.yellow('⟳ ') +
105
- chalk.bold(`Franklin ${update.latest}`) +
105
+ chalk.bold(`Franklin Agent ${update.latest}`) +
106
106
  chalk.dim(` available — you have ${update.current}`));
107
107
  console.log(chalk.dim(' Run: ') +
108
108
  chalk.bold('npm install -g @blockrun/franklin@latest'));
@@ -43,7 +43,7 @@ async function runChecks() {
43
43
  kickoffVersionCheck();
44
44
  const update = await freshUpdatePromise;
45
45
  out.push({
46
- name: 'Franklin',
46
+ name: 'Franklin Agent',
47
47
  status: update ? 'warn' : 'ok',
48
48
  detail: update
49
49
  ? `v${VERSION} — update available: v${update.latest}`
package/dist/index.js CHANGED
@@ -29,8 +29,8 @@ import { VERSION as version } from './config.js';
29
29
  const program = new Command();
30
30
  program
31
31
  .name('franklin')
32
- .description('Franklin — The AI agent with a wallet.\n\n' +
33
- 'While others chat, Franklin spends — turning your USDC into real work.\n\n' +
32
+ .description('Franklin Agent — The AI agent with a wallet.\n\n' +
33
+ 'While others chat, Franklin Agent spends — turning your USDC into real work.\n\n' +
34
34
  'Pay per action in USDC on Base or Solana. No subscriptions. No accounts.')
35
35
  .version(version);
36
36
  program
package/dist/ui/app.js CHANGED
@@ -1060,7 +1060,7 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
1060
1060
  : `${formatTokens(r.tokens.input)} in / ${formatTokens(r.tokens.output)} out`, r.cost > 0 ? ` · $${r.cost.toFixed(4)}` : '', r.savings !== undefined && r.savings > 0 ? _jsxs(Text, { color: "green", children: [" saved ", Math.round(r.savings * 100), "%"] }) : '', r.ctxPct !== undefined && r.ctxPct >= 5
1061
1061
  ? _jsxs(Text, { color: r.ctxPct >= 80 ? 'red' : r.ctxPct >= 50 ? 'yellow' : undefined, dimColor: r.ctxPct < 50, children: [" \u00B7 ctx ", r.ctxPct, "%"] })
1062
1062
  : ''] }) }))] }, r.key));
1063
- } }), permissionRequest && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, children: [_jsx(Text, { color: "red", bold: true, children: "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u26A0 ACTION REQUIRED \u26A0 \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501" }), _jsx(Text, { color: "yellow", children: "\u256D\u2500 Permission required \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Text, { color: "yellow", children: ["\u2502 ", _jsx(Text, { bold: true, children: permissionRequest.toolName })] }), permissionRequest.description.split('\n').map((line, i) => (_jsxs(Text, { dimColor: true, children: ["\u2502 ", line] }, i))), _jsx(Text, { color: "yellow", children: "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsx(Box, { marginLeft: 2, children: _jsxs(Text, { children: [_jsx(Text, { bold: true, color: "green", children: "[y]" }), _jsx(Text, { dimColor: true, children: " yes " }), _jsx(Text, { bold: true, color: "cyan", children: "[a]" }), _jsx(Text, { dimColor: true, children: " always " }), _jsx(Text, { bold: true, color: "red", children: "[n]" }), _jsx(Text, { dimColor: true, children: " no" })] }) })] })), askUserRequest && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, children: [_jsx(Text, { color: "magenta", bold: true, children: "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u26A0 ANSWER REQUIRED \u26A0 \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501" }), _jsx(Text, { color: "cyan", children: "\u256D\u2500 Question \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Text, { color: "cyan", children: ["\u2502 ", _jsx(Text, { bold: true, children: askUserRequest.question })] }), askUserRequest.options && askUserRequest.options.length > 0 && (askUserRequest.options.map((opt, i) => (_jsxs(Text, { dimColor: true, children: ["\u2502 ", i + 1, ". ", opt] }, i)))), _jsx(Text, { color: "cyan", children: "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Box, { marginLeft: 2, children: [_jsx(Text, { bold: true, children: "answer> " }), _jsx(TextInput, { value: askUserInput, onChange: setAskUserInput, onSubmit: (val) => {
1063
+ } }), permissionRequest && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, children: [_jsx(Text, { color: "red", bold: true, children: "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u26A0 ACTION REQUIRED \u26A0 \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501" }), _jsx(Text, { color: "yellow", children: "\u256D\u2500 Permission required \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Text, { color: "yellow", children: ["\u2502 ", _jsx(Text, { bold: true, children: permissionRequest.toolName })] }), permissionRequest.description.split('\n').map((line, i) => (_jsxs(Text, { dimColor: true, children: ["\u2502 ", line] }, i))), _jsx(Text, { color: "yellow", children: "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsx(Box, { marginLeft: 2, children: _jsxs(Text, { children: [_jsx(Text, { bold: true, color: "green", children: "[y]" }), _jsx(Text, { dimColor: true, children: " yes " }), _jsx(Text, { bold: true, color: "cyan", children: "[a]" }), _jsx(Text, { dimColor: true, children: " always " }), _jsx(Text, { dimColor: true, italic: true, children: "(saved across sessions)" }), _jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { bold: true, color: "red", children: "[n]" }), _jsx(Text, { dimColor: true, children: " no" })] }) })] })), askUserRequest && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, children: [_jsx(Text, { color: "magenta", bold: true, children: "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u26A0 ANSWER REQUIRED \u26A0 \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501" }), _jsx(Text, { color: "cyan", children: "\u256D\u2500 Question \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Text, { color: "cyan", children: ["\u2502 ", _jsx(Text, { bold: true, children: askUserRequest.question })] }), askUserRequest.options && askUserRequest.options.length > 0 && (askUserRequest.options.map((opt, i) => (_jsxs(Text, { dimColor: true, children: ["\u2502 ", i + 1, ". ", opt] }, i)))), _jsx(Text, { color: "cyan", children: "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Box, { marginLeft: 2, children: [_jsx(Text, { bold: true, children: "answer> " }), _jsx(TextInput, { value: askUserInput, onChange: setAskUserInput, onSubmit: (val) => {
1064
1064
  // resolveAskUserAnswer translates "1" / "2" / ... into the
1065
1065
  // matching label string when the dialog showed a numbered
1066
1066
  // option list. Without it, every onAskUser caller's
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.15.99",
3
+ "version": "3.15.101",
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": {