@blockrun/franklin 3.8.6 → 3.8.8

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.
@@ -16,8 +16,24 @@ const DANGEROUS_PATTERNS = [
16
16
  [/\brm\s+-[a-zA-Z]*[rR][a-zA-Z]*\s+[/~]/, 'recursive delete on root/home'],
17
17
  [/\brm\s+-[a-zA-Z]*[rR][a-zA-Z]*f/, 'forced recursive delete'],
18
18
  [/\brm\s+-[a-zA-Z]*f[a-zA-Z]*[rR]/, 'forced recursive delete'],
19
+ [/\brm\s+-[a-zA-Z]*f\s+\//, 'forced delete at filesystem root'],
19
20
  [/\bmkfs\b/, 'format filesystem'],
20
21
  [/\bdd\s+.*of=/, 'raw disk write'],
22
+ [/\btruncate\s+-s\s+0\b/, 'truncate file to zero'],
23
+ [/>\s*\/dev\/(sd|nvme|disk|hd)/, 'write to raw block device'],
24
+ // Silently overwriting with mv/cp
25
+ [/\bmv\s+-f\b/, 'mv -f overwrites target silently'],
26
+ [/\bcp\s+-[a-zA-Z]*f[a-zA-Z]*r/, 'cp -rf can overwrite directory trees silently'],
27
+ // Writes to system-level paths — most agents should NEVER touch these.
28
+ // Redirections (`>`, `>>`) or tee'ing to /etc/, /usr/, /boot/, /var/lib/ etc.
29
+ [/>\s*\/(etc|usr|bin|sbin|boot|lib|lib64|var\/lib|sys|proc)\//, 'write to system path'],
30
+ [/\btee\s+.*\s+\/(etc|usr|bin|sbin|boot|lib|lib64|var\/lib|sys|proc)\//, 'tee to system path'],
31
+ // Extract tar/zip at filesystem root — classic traversal foot-gun.
32
+ [/\btar\s+.*-C\s+\/(?!tmp|var\/tmp|home)/, 'extract archive to system path'],
33
+ [/\bunzip\s+.*-d\s+\/(?!tmp|var\/tmp|home)/, 'unzip to system path'],
34
+ // Shell-out of untrusted text
35
+ [/\beval\s/, 'eval executes arbitrary shell'],
36
+ [/\bexec\s+(bash|sh|zsh)/, 'exec replaces the shell process'],
21
37
  // Git irreversible operations
22
38
  [/\bgit\s+push\s+.*--force\b/, 'force push'],
23
39
  [/\bgit\s+push\s+-f\b/, 'force push'],
@@ -25,11 +41,14 @@ const DANGEROUS_PATTERNS = [
25
41
  [/\bgit\s+clean\s+-[a-zA-Z]*f/, 'git clean — deletes untracked files'],
26
42
  [/\bgit\s+checkout\s+--\s+\./, 'discard all working changes'],
27
43
  [/\bgit\s+branch\s+-D\b/, 'force delete branch'],
44
+ [/\bgit\s+filter-(repo|branch)\b/, 'history rewrite'],
28
45
  // Database destructive
29
46
  [/\bDROP\s+(TABLE|DATABASE|SCHEMA)\b/i, 'drop database objects'],
30
47
  [/\bTRUNCATE\s+TABLE\b/i, 'truncate table'],
48
+ [/\bDELETE\s+FROM\s+\S+\s*;?\s*$/i, 'DELETE without WHERE'],
31
49
  // System-level danger
32
50
  [/\bchmod\s+(-R\s+)?777\b/, 'world-writable permissions'],
51
+ [/\bchown\s+-R\s+\S+\s+\//, 'recursive chown at root'],
33
52
  // Pipe-to-shell: catch sudo/env prefixes and common shell variants (bash/sh/zsh/ksh/dash/fish).
34
53
  // The optional `-e`/`-x` flags after the shell binary are intentionally allowed by \b;
35
54
  // what we block is the routing of downloaded content into an interpreter.
@@ -38,11 +57,21 @@ const DANGEROUS_PATTERNS = [
38
57
  // Command substitution of a downloader into argv — `$(curl …)` or `` `curl …` ``.
39
58
  [/\$\(\s*(curl|wget|fetch)\b/, 'command substitution of network downloader'],
40
59
  [/`\s*(curl|wget|fetch)\b[^`]*`/, 'backtick substitution of network downloader'],
60
+ // Privilege escalation wrappers to destructive ops — order matters: the
61
+ // specific `sudo rm` pattern is listed first so its tailored message wins.
41
62
  [/\bsudo\s+rm\b/, 'sudo delete'],
63
+ [/\b(sudo|doas|su\s+-c)\s+.*\b(mv|dd|chmod|chown|mkfs|shutdown|reboot)\b/, 'privileged destructive op'],
64
+ // sed -i (in-place) on any system path
65
+ [/\bsed\s+-i(\s+'')?\s+.*\/(etc|usr|bin|sbin|boot|lib)\//, 'in-place edit of system path'],
42
66
  // Kill/shutdown
43
67
  [/\bkill\s+-9\s+-1\b/, 'kill all processes'],
68
+ [/\bkillall\s/, 'killall targets matching processes globally'],
44
69
  [/\bshutdown\b/, 'system shutdown'],
45
70
  [/\breboot\b/, 'system reboot'],
71
+ [/\bpoweroff\b/, 'system poweroff'],
72
+ // Cryptocurrency key exfiltration / secret exposure
73
+ [/\bcat\s+.*\.env(\.\w+)?\s*\|/, 'env file piped — potential secret exfiltration'],
74
+ [/\bcat\s+.*(\.ssh|\.gnupg)\/.*\s*\|/, 'ssh/gpg key piped — potential secret exfiltration'],
46
75
  ];
47
76
  // ─── Safe Commands ────────────────────────────────────────────────────────
48
77
  // If ALL segments use these commands, auto-approve.
@@ -68,7 +68,7 @@ Rules:
68
68
  export function getExecutorModel(profile) {
69
69
  switch (profile) {
70
70
  case 'premium':
71
- return 'moonshot/kimi-k2.5'; // Medium-tier, reliable execution
71
+ return 'moonshot/kimi-k2.6'; // Medium-tier, reliable execution (256K ctx, vision + reasoning)
72
72
  case 'auto':
73
73
  default:
74
74
  return 'google/gemini-2.5-flash'; // Cheap, fast, good at instructions
@@ -190,6 +190,7 @@ const MODEL_CONTEXT_WINDOWS = {
190
190
  'xai/grok-4-1-fast-reasoning': 131_072,
191
191
  // Others
192
192
  'zai/glm-5.1': 200_000,
193
+ 'moonshot/kimi-k2.6': 256_000,
193
194
  'moonshot/kimi-k2.5': 128_000,
194
195
  'minimax/minimax-m2.7': 128_000,
195
196
  };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `franklin doctor` — one-command health check.
3
+ *
4
+ * The single highest-leverage onboarding improvement: most early failures
5
+ * are environmental (Node too old, no wallet, wrong chain, unreachable
6
+ * gateway, malformed MCP config). `franklin doctor` pokes each of those in
7
+ * sequence, prints a verdict per check, and exits non-zero if anything is
8
+ * broken so CI scripts can gate on it.
9
+ *
10
+ * Human-readable by default. Pass `--json` for machine-parseable output
11
+ * (useful for the ink REPL `/doctor` or external monitoring).
12
+ */
13
+ export declare function doctorCommand(opts?: {
14
+ json?: boolean;
15
+ }): Promise<void>;
@@ -0,0 +1,251 @@
1
+ /**
2
+ * `franklin doctor` — one-command health check.
3
+ *
4
+ * The single highest-leverage onboarding improvement: most early failures
5
+ * are environmental (Node too old, no wallet, wrong chain, unreachable
6
+ * gateway, malformed MCP config). `franklin doctor` pokes each of those in
7
+ * sequence, prints a verdict per check, and exits non-zero if anything is
8
+ * broken so CI scripts can gate on it.
9
+ *
10
+ * Human-readable by default. Pass `--json` for machine-parseable output
11
+ * (useful for the ink REPL `/doctor` or external monitoring).
12
+ */
13
+ import chalk from 'chalk';
14
+ import fs from 'node:fs';
15
+ import path from 'node:path';
16
+ import os from 'node:os';
17
+ import { setupAgentWallet, setupAgentSolanaWallet, } from '@blockrun/llm';
18
+ import { loadChain, API_URLS, VERSION, BLOCKRUN_DIR } from '../config.js';
19
+ import { isTelemetryEnabled, readAllRecords } from '../telemetry/store.js';
20
+ async function runChecks() {
21
+ const out = [];
22
+ // ── 1. Runtime ────────────────────────────────────────────────────
23
+ const nodeVer = process.versions.node;
24
+ const nodeMajor = parseInt(nodeVer.split('.')[0], 10);
25
+ out.push({
26
+ name: 'Node.js',
27
+ status: nodeMajor >= 20 ? 'ok' : 'fail',
28
+ detail: `${nodeVer}${nodeMajor >= 20 ? '' : ' — require >= 20'}`,
29
+ remedy: nodeMajor >= 20 ? undefined : 'Upgrade Node.js: https://nodejs.org',
30
+ });
31
+ // ── 2. Franklin version ───────────────────────────────────────────
32
+ out.push({
33
+ name: 'Franklin',
34
+ status: 'ok',
35
+ detail: `v${VERSION}`,
36
+ });
37
+ // ── 3. BLOCKRUN_DIR writable ──────────────────────────────────────
38
+ try {
39
+ fs.mkdirSync(BLOCKRUN_DIR, { recursive: true });
40
+ const probe = path.join(BLOCKRUN_DIR, '.doctor-probe');
41
+ fs.writeFileSync(probe, 'ok');
42
+ fs.unlinkSync(probe);
43
+ out.push({
44
+ name: 'Config directory',
45
+ status: 'ok',
46
+ detail: BLOCKRUN_DIR,
47
+ });
48
+ }
49
+ catch (err) {
50
+ out.push({
51
+ name: 'Config directory',
52
+ status: 'fail',
53
+ detail: `${BLOCKRUN_DIR} — ${err.message}`,
54
+ remedy: `Check permissions on ${BLOCKRUN_DIR} or unset HOME override`,
55
+ });
56
+ }
57
+ // ── 4. Chain configuration ────────────────────────────────────────
58
+ let chain = null;
59
+ try {
60
+ chain = loadChain();
61
+ out.push({
62
+ name: 'Chain',
63
+ status: 'ok',
64
+ detail: chain,
65
+ });
66
+ }
67
+ catch (err) {
68
+ out.push({
69
+ name: 'Chain',
70
+ status: 'fail',
71
+ detail: `failed to load — ${err.message}`,
72
+ remedy: 'Run: franklin setup base (or: franklin setup solana)',
73
+ });
74
+ }
75
+ // ── 5. Wallet ─────────────────────────────────────────────────────
76
+ let walletBalance = null;
77
+ let walletAddress = '';
78
+ if (chain) {
79
+ try {
80
+ if (chain === 'solana') {
81
+ const client = await setupAgentSolanaWallet({ silent: true });
82
+ walletAddress = await client.getWalletAddress();
83
+ walletBalance = await client.getBalance();
84
+ }
85
+ else {
86
+ const client = setupAgentWallet({ silent: true });
87
+ walletAddress = client.getWalletAddress();
88
+ walletBalance = await client.getBalance();
89
+ }
90
+ out.push({
91
+ name: 'Wallet',
92
+ status: 'ok',
93
+ detail: `${walletAddress.slice(0, 10)}…${walletAddress.slice(-6)}`,
94
+ });
95
+ out.push({
96
+ name: 'USDC balance',
97
+ status: walletBalance > 0 ? 'ok' : 'warn',
98
+ detail: `$${walletBalance.toFixed(2)}${walletBalance === 0 ? ' — free-tier models only' : ''}`,
99
+ remedy: walletBalance === 0
100
+ ? `Send USDC on ${chain} to ${walletAddress} to unlock paid models`
101
+ : undefined,
102
+ });
103
+ }
104
+ catch (err) {
105
+ const msg = err.message || '';
106
+ out.push({
107
+ name: 'Wallet',
108
+ status: 'fail',
109
+ detail: `error — ${msg.slice(0, 120)}`,
110
+ remedy: msg.includes('ENOENT') || msg.includes('wallet') || msg.includes('key')
111
+ ? 'Run: franklin setup'
112
+ : 'Check network / wallet file permissions',
113
+ });
114
+ }
115
+ }
116
+ // ── 6. Gateway reachability ───────────────────────────────────────
117
+ if (chain) {
118
+ const apiUrl = API_URLS[chain];
119
+ try {
120
+ const ctl = new AbortController();
121
+ const t = setTimeout(() => ctl.abort(), 5000);
122
+ const res = await fetch(`${apiUrl}/health`, { signal: ctl.signal }).catch(() => null);
123
+ clearTimeout(t);
124
+ if (res && res.ok) {
125
+ out.push({
126
+ name: 'Gateway',
127
+ status: 'ok',
128
+ detail: apiUrl,
129
+ });
130
+ }
131
+ else {
132
+ // Fall back to a HEAD on the messages endpoint — some deployments
133
+ // don't expose /health but the API is up.
134
+ const ctl2 = new AbortController();
135
+ const t2 = setTimeout(() => ctl2.abort(), 5000);
136
+ const res2 = await fetch(`${apiUrl}/v1/messages`, {
137
+ method: 'HEAD',
138
+ signal: ctl2.signal,
139
+ }).catch(() => null);
140
+ clearTimeout(t2);
141
+ out.push({
142
+ name: 'Gateway',
143
+ status: res2 ? 'ok' : 'fail',
144
+ detail: res2 ? apiUrl : `unreachable: ${apiUrl}`,
145
+ remedy: res2 ? undefined : 'Check network or try the other chain',
146
+ });
147
+ }
148
+ }
149
+ catch (err) {
150
+ out.push({
151
+ name: 'Gateway',
152
+ status: 'fail',
153
+ detail: `${apiUrl} — ${err.message}`,
154
+ });
155
+ }
156
+ }
157
+ // ── 7. MCP config ─────────────────────────────────────────────────
158
+ const mcpPath = path.join(BLOCKRUN_DIR, 'mcp.json');
159
+ if (fs.existsSync(mcpPath)) {
160
+ try {
161
+ const raw = fs.readFileSync(mcpPath, 'utf-8');
162
+ const parsed = JSON.parse(raw);
163
+ const count = Object.keys(parsed.mcpServers || {}).length;
164
+ out.push({
165
+ name: 'MCP servers',
166
+ status: 'ok',
167
+ detail: `${count} configured in ${mcpPath}`,
168
+ });
169
+ }
170
+ catch (err) {
171
+ out.push({
172
+ name: 'MCP servers',
173
+ status: 'warn',
174
+ detail: `${mcpPath} has invalid JSON — ${err.message}`,
175
+ remedy: `Fix or delete ${mcpPath}`,
176
+ });
177
+ }
178
+ }
179
+ else {
180
+ out.push({
181
+ name: 'MCP servers',
182
+ status: 'ok',
183
+ detail: 'none configured',
184
+ });
185
+ }
186
+ // ── 8. Telemetry ──────────────────────────────────────────────────
187
+ const telEnabled = isTelemetryEnabled();
188
+ if (telEnabled) {
189
+ const records = readAllRecords();
190
+ out.push({
191
+ name: 'Telemetry',
192
+ status: 'ok',
193
+ detail: `enabled — ${records.length} session${records.length === 1 ? '' : 's'} recorded`,
194
+ });
195
+ }
196
+ else {
197
+ out.push({
198
+ name: 'Telemetry',
199
+ status: 'ok',
200
+ detail: 'disabled (default)',
201
+ });
202
+ }
203
+ // ── 9. Shell / PATH hint ──────────────────────────────────────────
204
+ const which = process.env.PATH || '';
205
+ const hasHomebrew = which.includes('/opt/homebrew/bin') || which.includes('/usr/local/bin');
206
+ if (os.platform() === 'darwin' && !hasHomebrew) {
207
+ out.push({
208
+ name: 'PATH',
209
+ status: 'warn',
210
+ detail: 'Homebrew paths not in PATH',
211
+ remedy: 'Add /opt/homebrew/bin to PATH in ~/.zshrc',
212
+ });
213
+ }
214
+ return out;
215
+ }
216
+ function printHuman(checks) {
217
+ console.log(chalk.bold('\n franklin doctor\n'));
218
+ for (const c of checks) {
219
+ const icon = c.status === 'ok' ? chalk.green('✓') :
220
+ c.status === 'warn' ? chalk.yellow('⚠') :
221
+ chalk.red('✗');
222
+ console.log(` ${icon} ${c.name.padEnd(18)} ${chalk.dim(c.detail)}`);
223
+ if (c.remedy) {
224
+ console.log(` ${chalk.dim('↳')} ${chalk.yellow(c.remedy)}`);
225
+ }
226
+ }
227
+ const fails = checks.filter(c => c.status === 'fail').length;
228
+ const warns = checks.filter(c => c.status === 'warn').length;
229
+ console.log();
230
+ if (fails > 0) {
231
+ console.log(chalk.red(` ${fails} check${fails === 1 ? '' : 's'} failed. See remedies above.`));
232
+ }
233
+ else if (warns > 0) {
234
+ console.log(chalk.yellow(` All criticals ok. ${warns} warning${warns === 1 ? '' : 's'} above — safe to ignore for now.`));
235
+ }
236
+ else {
237
+ console.log(chalk.green(' All clear. Ready to run: franklin'));
238
+ }
239
+ console.log();
240
+ }
241
+ export async function doctorCommand(opts = {}) {
242
+ const checks = await runChecks();
243
+ if (opts.json) {
244
+ const fails = checks.filter(c => c.status === 'fail').length;
245
+ process.stdout.write(JSON.stringify({ checks, healthy: fails === 0 }, null, 2) + '\n');
246
+ process.exit(fails > 0 ? 1 : 0);
247
+ }
248
+ printHuman(checks);
249
+ const fails = checks.filter(c => c.status === 'fail').length;
250
+ process.exit(fails > 0 ? 1 : 0);
251
+ }
package/dist/index.js CHANGED
@@ -166,6 +166,14 @@ program
166
166
  });
167
167
  }
168
168
  }
169
+ program
170
+ .command('doctor')
171
+ .description('One-command health check (node, wallet, chain, gateway, MCP, telemetry)')
172
+ .option('--json', 'Machine-readable output')
173
+ .action(async (opts) => {
174
+ const { doctorCommand } = await import('./commands/doctor.js');
175
+ await doctorCommand(opts);
176
+ });
169
177
  program
170
178
  .command('telemetry [action]')
171
179
  .description('Manage opt-in local telemetry (status|enable|disable|view|summary)')
package/dist/pricing.js CHANGED
@@ -67,7 +67,8 @@ export const MODEL_PRICING = {
67
67
  // Minimax
68
68
  'minimax/minimax-m2.7': { input: 0.3, output: 1.2 },
69
69
  'minimax/minimax-m2.5': { input: 0.3, output: 1.2 },
70
- // Others
70
+ // Moonshot
71
+ 'moonshot/kimi-k2.6': { input: 0.95, output: 4.0 },
71
72
  'moonshot/kimi-k2.5': { input: 0.6, output: 3.0 },
72
73
  'nvidia/kimi-k2.5': { input: 0.55, output: 2.5 },
73
74
  // PROMOTION (active ~2026-04): flat $0.001/call for all GLM models
@@ -105,7 +105,7 @@ const MODEL_SHORTCUTS = {
105
105
  minimax: 'minimax/minimax-m2.7',
106
106
  // Others
107
107
  glm: 'zai/glm-5.1',
108
- kimi: 'moonshot/kimi-k2.5',
108
+ kimi: 'moonshot/kimi-k2.6',
109
109
  };
110
110
  // Model pricing now uses shared source from src/pricing.ts
111
111
  function detectModelSwitch(parsed) {
@@ -40,15 +40,15 @@ function loadLearnedWeights() {
40
40
  const AUTO_TIERS = {
41
41
  SIMPLE: {
42
42
  primary: 'google/gemini-2.5-flash',
43
- fallback: ['moonshot/kimi-k2.5', 'deepseek/deepseek-chat'],
43
+ fallback: ['moonshot/kimi-k2.6', 'deepseek/deepseek-chat'],
44
44
  },
45
45
  MEDIUM: {
46
46
  primary: 'anthropic/claude-sonnet-4.6',
47
- fallback: ['openai/gpt-5.4', 'google/gemini-3.1-pro', 'moonshot/kimi-k2.5'],
47
+ fallback: ['openai/gpt-5.4', 'google/gemini-3.1-pro', 'moonshot/kimi-k2.6'],
48
48
  },
49
49
  COMPLEX: {
50
50
  primary: 'anthropic/claude-sonnet-4.6',
51
- fallback: ['openai/gpt-5.4', 'anthropic/claude-opus-4.7', 'moonshot/kimi-k2.5'],
51
+ fallback: ['openai/gpt-5.4', 'anthropic/claude-opus-4.7', 'moonshot/kimi-k2.6'],
52
52
  },
53
53
  REASONING: {
54
54
  // Opus 4.7: step-change improvement in agentic coding over 4.6 per
@@ -84,7 +84,7 @@ const ECO_TIERS = {
84
84
  };
85
85
  const PREMIUM_TIERS = {
86
86
  SIMPLE: {
87
- primary: 'moonshot/kimi-k2.5',
87
+ primary: 'moonshot/kimi-k2.6',
88
88
  fallback: ['anthropic/claude-haiku-4.5'],
89
89
  },
90
90
  MEDIUM: {
@@ -80,13 +80,32 @@ async function execute(input, ctx) {
80
80
  if (stat.size > maxBytes) {
81
81
  return { output: `Error: file is too large (${(stat.size / 1024 / 1024).toFixed(1)}MB). Use offset/limit to read a portion.`, isError: true };
82
82
  }
83
- // Detect binary files
83
+ // Detect binary files — first by extension, then by content
84
+ // (some binaries have no extension: `.env.enc`, `.data`, compiled tools
85
+ // without suffixes, etc. Content sniff catches those.)
84
86
  const ext = path.extname(resolved).toLowerCase();
85
87
  const binaryExts = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.bmp', '.pdf', '.zip', '.tar', '.gz', '.woff', '.woff2', '.ttf', '.eot', '.mp3', '.mp4', '.wav', '.avi', '.mov', '.exe', '.dll', '.so', '.dylib']);
86
88
  if (binaryExts.has(ext)) {
87
89
  const sizeStr = stat.size >= 1024 ? `${(stat.size / 1024).toFixed(1)}KB` : `${stat.size}B`;
88
90
  return { output: `Binary file: ${resolved} (${ext}, ${sizeStr}). Cannot display contents.` };
89
91
  }
92
+ // NUL-byte content sniff — read up to 8KB as a Buffer, scan for 0x00.
93
+ // Text files effectively never contain NUL; binary files almost always
94
+ // do within the first few KB.
95
+ try {
96
+ const SNIFF_BYTES = Math.min(stat.size, 8192);
97
+ if (SNIFF_BYTES > 0) {
98
+ const fd = fs.openSync(resolved, 'r');
99
+ const buf = Buffer.alloc(SNIFF_BYTES);
100
+ fs.readSync(fd, buf, 0, SNIFF_BYTES, 0);
101
+ fs.closeSync(fd);
102
+ if (buf.includes(0)) {
103
+ const sizeStr = stat.size >= 1024 ? `${(stat.size / 1024).toFixed(1)}KB` : `${stat.size}B`;
104
+ return { output: `Binary file: ${resolved} (no text extension but NUL bytes detected, ${sizeStr}). Cannot display contents.` };
105
+ }
106
+ }
107
+ }
108
+ catch { /* best-effort sniff — fall through to text read */ }
90
109
  const raw = fs.readFileSync(resolved, 'utf-8');
91
110
  const allLines = raw.split('\n');
92
111
  const startLine = Math.max(0, (Math.max(1, offset ?? 1)) - 1);
@@ -87,6 +87,26 @@ async function execute(input, ctx) {
87
87
  isError: true,
88
88
  };
89
89
  }
90
+ // Write-size cap. A user-intended file write should never exceed a few
91
+ // MB; larger payloads are almost always accidental (log dumps, serialized
92
+ // objects) and refusing them explicitly beats a silent disk-full.
93
+ const MAX_WRITE_BYTES = 10 * 1024 * 1024;
94
+ const contentBytes = Buffer.byteLength(content, 'utf-8');
95
+ if (contentBytes > MAX_WRITE_BYTES) {
96
+ return {
97
+ output: `Error: refusing to write ${(contentBytes / 1024 / 1024).toFixed(1)}MB to ${resolved} — max allowed is ${MAX_WRITE_BYTES / 1024 / 1024}MB. Split into smaller writes, or use Bash if this is intentional bulk output.`,
98
+ isError: true,
99
+ };
100
+ }
101
+ // Content sniff — warn (not block) if NUL bytes detected. Text tools
102
+ // writing binary is almost always a mistake; explicit Buffer writes
103
+ // should go through Bash.
104
+ if (content.indexOf('\0') !== -1) {
105
+ return {
106
+ output: `Error: refusing to write NUL-byte content to ${resolved}. This tool writes text files only. For binary output use Bash with a base64 decode or an external script.`,
107
+ isError: true,
108
+ };
109
+ }
90
110
  try {
91
111
  // Ensure parent directory exists
92
112
  const parentDir = path.dirname(resolved);
@@ -58,7 +58,8 @@ export const MODEL_SHORTCUTS = {
58
58
  glm: 'zai/glm-5.1',
59
59
  'glm-turbo': 'zai/glm-5-turbo',
60
60
  'glm5': 'zai/glm-5.1',
61
- kimi: 'moonshot/kimi-k2.5',
61
+ kimi: 'moonshot/kimi-k2.6',
62
+ 'kimi-k2.5': 'moonshot/kimi-k2.5',
62
63
  };
63
64
  /**
64
65
  * Resolve a model name — supports shortcuts.
@@ -125,7 +126,8 @@ export const PICKER_CATEGORIES = [
125
126
  { id: 'openai/gpt-5-nano', shortcut: 'nano', label: 'GPT-5 Nano', price: '$0.05/$0.4' },
126
127
  { id: 'google/gemini-2.5-flash', shortcut: 'flash', label: 'Gemini 2.5 Flash', price: '$0.3/$2.5' },
127
128
  { id: 'deepseek/deepseek-chat', shortcut: 'deepseek', label: 'DeepSeek V3', price: '$0.28/$0.42' },
128
- { id: 'moonshot/kimi-k2.5', shortcut: 'kimi', label: 'Kimi K2.5', price: '$0.6/$3' },
129
+ { id: 'moonshot/kimi-k2.6', shortcut: 'kimi', label: 'Kimi K2.6', price: '$0.95/$4' },
130
+ { id: 'moonshot/kimi-k2.5', shortcut: 'kimi-k2.5', label: 'Kimi K2.5 (legacy)', price: '$0.6/$3' },
129
131
  { id: 'minimax/minimax-m2.7', shortcut: 'minimax', label: 'Minimax M2.7', price: '$0.3/$1.2' },
130
132
  ],
131
133
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.8.6",
3
+ "version": "3.8.8",
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": {