@blockrun/franklin 3.8.5 → 3.8.7

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/README.md CHANGED
@@ -16,7 +16,7 @@
16
16
  <p>
17
17
  <a href="https://npmjs.com/package/@blockrun/franklin"><img src="https://img.shields.io/npm/v/@blockrun/franklin.svg?style=flat-square&color=FFD700&label=npm" alt="npm"></a>
18
18
  <a href="https://npmjs.com/package/@blockrun/franklin"><img src="https://img.shields.io/npm/dm/@blockrun/franklin.svg?style=flat-square&color=10B981&label=downloads" alt="downloads"></a>
19
- <a href="https://github.com/RunFranklin/franklin/stargazers"><img src="https://img.shields.io/github/stars/RunFranklin/franklin?style=flat-square&color=FFD700&label=stars" alt="stars"></a>
19
+ <a href="https://github.com/BlockRunAI/Franklin/stargazers"><img src="https://img.shields.io/github/stars/BlockRunAI/Franklin?style=flat-square&color=FFD700&label=stars" alt="stars"></a>
20
20
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache_2.0-blue?style=flat-square" alt="license"></a>
21
21
  <a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-strict-3178C6?style=flat-square&logo=typescript&logoColor=white" alt="TypeScript"></a>
22
22
  <a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Node-%E2%89%A520-339933?style=flat-square&logo=node.js&logoColor=white" alt="Node"></a>
@@ -468,14 +468,14 @@ Same wallet. Same tools. From your phone.
468
468
 
469
469
  - [Telegram](https://t.me/blockrunAI) — realtime help, bug reports, feature requests
470
470
  - [@BlockRunAI](https://x.com/BlockRunAI) — release notes, demos
471
- - [Issues](https://github.com/RunFranklin/franklin/issues) — bugs and feature requests
471
+ - [Issues](https://github.com/BlockRunAI/Franklin/issues) — bugs and feature requests
472
472
 
473
473
  ---
474
474
 
475
475
  ## Development
476
476
 
477
477
  ```bash
478
- git clone https://github.com/RunFranklin/franklin.git
478
+ git clone https://github.com/BlockRunAI/Franklin.git
479
479
  cd franklin
480
480
  npm install
481
481
  npm run build
@@ -358,6 +358,10 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
358
358
  let sessionOutputTokens = 0;
359
359
  let sessionCostUsd = 0;
360
360
  let sessionSavedVsOpus = 0;
361
+ // Per-tool call counts aggregated across every turn. Session-scope, not
362
+ // per-turn. Counts the *name* of each tool invocation only — no inputs,
363
+ // outputs, or paths. Fed into opt-in telemetry at session end.
364
+ const sessionToolCounts = new Map();
361
365
  const toolGuard = new SessionToolGuard();
362
366
  const persistSessionMeta = () => {
363
367
  updateSessionMeta(sessionId, {
@@ -370,6 +374,9 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
370
374
  costUsd: sessionCostUsd,
371
375
  savedVsOpusUsd: sessionSavedVsOpus,
372
376
  ...(config.sessionChannel !== undefined ? { channel: config.sessionChannel } : {}),
377
+ ...(sessionToolCounts.size > 0
378
+ ? { toolCallCounts: Object.fromEntries(sessionToolCounts) }
379
+ : {}),
373
380
  });
374
381
  };
375
382
  const persistSessionMessage = (message) => {
@@ -1039,6 +1046,8 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
1039
1046
  for (const [inv] of results) {
1040
1047
  const name = inv.name;
1041
1048
  turnToolCounts.set(name, (turnToolCounts.get(name) || 0) + 1);
1049
+ // Session-scope aggregate (drives telemetry opt-in export).
1050
+ sessionToolCounts.set(name, (sessionToolCounts.get(name) || 0) + 1);
1042
1051
  // Read file dedup: track paths already read
1043
1052
  if (name === 'Read' && inv.input.file_path) {
1044
1053
  readFileCache.add(inv.input.file_path);
@@ -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
  };
@@ -313,6 +313,13 @@ async function runWithInkUI(agentConfig, model, workDir, version, walletInfo, on
313
313
  }
314
314
  ui.cleanup();
315
315
  flushStats();
316
+ // Opt-in telemetry — no-op unless user has run `franklin telemetry enable`.
317
+ // Appends a sanitized session summary to ~/.blockrun/telemetry.jsonl.
318
+ try {
319
+ const { recordLatestSessionIfEnabled } = await import('../telemetry/store.js');
320
+ recordLatestSessionIfEnabled(process.cwd(), agentConfig.chain);
321
+ }
322
+ catch { /* telemetry is best-effort */ }
316
323
  // Extract learnings from the session (async, 10s timeout, never blocks exit)
317
324
  if (sessionHistory && sessionHistory.length >= 4) {
318
325
  try {
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `franklin telemetry` — manage the opt-in local telemetry subsystem.
3
+ *
4
+ * Subcommands:
5
+ * status — print whether telemetry is enabled, where the log lives,
6
+ * and a one-line summary of what's been recorded
7
+ * enable — turn on local recording (default is OFF)
8
+ * disable — stop future recording; existing data stays on disk
9
+ * view — print every record in the log as pretty JSONL so the
10
+ * user can see exactly what was captured
11
+ * summary — aggregate all records into tool-usage histograms so
12
+ * positioning decisions can be made from real data
13
+ */
14
+ export declare function telemetryCommand(action?: string): Promise<void>;
@@ -0,0 +1,150 @@
1
+ /**
2
+ * `franklin telemetry` — manage the opt-in local telemetry subsystem.
3
+ *
4
+ * Subcommands:
5
+ * status — print whether telemetry is enabled, where the log lives,
6
+ * and a one-line summary of what's been recorded
7
+ * enable — turn on local recording (default is OFF)
8
+ * disable — stop future recording; existing data stays on disk
9
+ * view — print every record in the log as pretty JSONL so the
10
+ * user can see exactly what was captured
11
+ * summary — aggregate all records into tool-usage histograms so
12
+ * positioning decisions can be made from real data
13
+ */
14
+ import chalk from 'chalk';
15
+ import { isTelemetryEnabled, setTelemetryEnabled, readConsent, readAllRecords, telemetryPaths, } from '../telemetry/store.js';
16
+ export async function telemetryCommand(action) {
17
+ const cmd = (action || 'status').toLowerCase();
18
+ switch (cmd) {
19
+ case 'status':
20
+ return statusCmd();
21
+ case 'enable':
22
+ case 'on':
23
+ return enableCmd();
24
+ case 'disable':
25
+ case 'off':
26
+ return disableCmd();
27
+ case 'view':
28
+ case 'log':
29
+ return viewCmd();
30
+ case 'summary':
31
+ return summaryCmd();
32
+ default:
33
+ console.log(chalk.yellow(`Unknown subcommand: ${action}`));
34
+ console.log(chalk.dim('Try: franklin telemetry [status|enable|disable|view|summary]'));
35
+ process.exit(1);
36
+ }
37
+ }
38
+ function statusCmd() {
39
+ const enabled = isTelemetryEnabled();
40
+ const consent = readConsent();
41
+ const records = readAllRecords();
42
+ console.log(chalk.bold('Franklin telemetry'));
43
+ console.log(` state: ${enabled ? chalk.green('enabled') : chalk.dim('disabled (default)')}`);
44
+ if (consent.enabledAt) {
45
+ console.log(` enabled at: ${chalk.dim(new Date(consent.enabledAt).toISOString())}`);
46
+ }
47
+ if (consent.disabledAt && !enabled) {
48
+ console.log(` disabled at: ${chalk.dim(new Date(consent.disabledAt).toISOString())}`);
49
+ }
50
+ console.log(` records: ${chalk.cyan(records.length.toString())} session${records.length === 1 ? '' : 's'} on disk`);
51
+ console.log(` log file: ${chalk.dim(telemetryPaths.log)}`);
52
+ console.log();
53
+ console.log(chalk.dim('Telemetry is local-only: no network transmission.'));
54
+ console.log(chalk.dim('Records contain tool-usage counts and cost totals — NOT prompts, tool inputs, tool outputs, paths, or wallet addresses.'));
55
+ console.log();
56
+ console.log(chalk.dim('Commands:'));
57
+ console.log(chalk.dim(' franklin telemetry enable turn on local recording'));
58
+ console.log(chalk.dim(' franklin telemetry disable stop future recording (keeps existing data)'));
59
+ console.log(chalk.dim(' franklin telemetry view print every stored record verbatim'));
60
+ console.log(chalk.dim(' franklin telemetry summary aggregate tool-usage histograms'));
61
+ }
62
+ function enableCmd() {
63
+ if (isTelemetryEnabled()) {
64
+ console.log(chalk.dim('Telemetry is already enabled.'));
65
+ return;
66
+ }
67
+ setTelemetryEnabled(true);
68
+ console.log(chalk.green('Telemetry enabled.'));
69
+ console.log(chalk.dim('Each session end appends one JSON line to ' + telemetryPaths.log));
70
+ console.log(chalk.dim('Inspect with: franklin telemetry view'));
71
+ console.log(chalk.dim('Disable with: franklin telemetry disable'));
72
+ }
73
+ function disableCmd() {
74
+ if (!isTelemetryEnabled()) {
75
+ console.log(chalk.dim('Telemetry is already disabled.'));
76
+ return;
77
+ }
78
+ setTelemetryEnabled(false);
79
+ console.log(chalk.green('Telemetry disabled. Future sessions will not be recorded.'));
80
+ console.log(chalk.dim('Existing data at ' + telemetryPaths.log + ' is untouched.'));
81
+ console.log(chalk.dim('Delete it manually if you want to clear history.'));
82
+ }
83
+ function viewCmd() {
84
+ const records = readAllRecords();
85
+ if (records.length === 0) {
86
+ console.log(chalk.dim('No telemetry records yet.'));
87
+ if (!isTelemetryEnabled()) {
88
+ console.log(chalk.dim('Telemetry is disabled — enable with: franklin telemetry enable'));
89
+ }
90
+ return;
91
+ }
92
+ for (const r of records) {
93
+ console.log(JSON.stringify(r, null, 2));
94
+ console.log(chalk.dim('─'.repeat(60)));
95
+ }
96
+ }
97
+ function summaryCmd() {
98
+ const records = readAllRecords();
99
+ if (records.length === 0) {
100
+ console.log(chalk.dim('No telemetry records yet.'));
101
+ return;
102
+ }
103
+ const toolCounts = new Map();
104
+ const modelCounts = new Map();
105
+ const driverCounts = new Map();
106
+ let totalCost = 0;
107
+ let totalSaved = 0;
108
+ let totalTurns = 0;
109
+ for (const r of records) {
110
+ if (r.toolCallCounts) {
111
+ for (const [tool, n] of Object.entries(r.toolCallCounts)) {
112
+ toolCounts.set(tool, (toolCounts.get(tool) || 0) + n);
113
+ }
114
+ }
115
+ modelCounts.set(r.model, (modelCounts.get(r.model) || 0) + 1);
116
+ driverCounts.set(r.driver, (driverCounts.get(r.driver) || 0) + 1);
117
+ totalCost += r.costUsd;
118
+ totalSaved += r.savedVsOpusUsd;
119
+ totalTurns += r.turns;
120
+ }
121
+ console.log(chalk.bold(`\nFranklin telemetry summary — ${records.length} sessions\n`));
122
+ console.log(` total turns: ${totalTurns}`);
123
+ console.log(` total USDC cost: $${totalCost.toFixed(4)}`);
124
+ console.log(` saved vs Opus: $${totalSaved.toFixed(4)}`);
125
+ console.log();
126
+ console.log(chalk.bold(' Tool usage (session aggregate):'));
127
+ const sortedTools = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]);
128
+ if (sortedTools.length === 0) {
129
+ console.log(chalk.dim(' (no tool calls recorded)'));
130
+ }
131
+ else {
132
+ const maxCount = sortedTools[0][1];
133
+ for (const [tool, n] of sortedTools) {
134
+ const bar = '█'.repeat(Math.max(1, Math.round((n / maxCount) * 20)));
135
+ console.log(` ${tool.padEnd(22)} ${chalk.cyan(bar)} ${n}`);
136
+ }
137
+ }
138
+ console.log();
139
+ console.log(chalk.bold(' Models:'));
140
+ const sortedModels = [...modelCounts.entries()].sort((a, b) => b[1] - a[1]);
141
+ for (const [model, n] of sortedModels) {
142
+ console.log(` ${model.padEnd(36)} ${n}`);
143
+ }
144
+ console.log();
145
+ console.log(chalk.bold(' Drivers:'));
146
+ for (const [driver, n] of driverCounts.entries()) {
147
+ console.log(` ${driver.padEnd(22)} ${n}`);
148
+ }
149
+ console.log();
150
+ }
package/dist/index.js CHANGED
@@ -166,6 +166,13 @@ program
166
166
  });
167
167
  }
168
168
  }
169
+ program
170
+ .command('telemetry [action]')
171
+ .description('Manage opt-in local telemetry (status|enable|disable|view|summary)')
172
+ .action(async (action) => {
173
+ const { telemetryCommand } = await import('./commands/telemetry.js');
174
+ await telemetryCommand(action);
175
+ });
169
176
  program
170
177
  .command('telegram')
171
178
  .description('Drive Franklin from Telegram (requires TELEGRAM_BOT_TOKEN + TELEGRAM_OWNER_ID env vars)')
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: {
@@ -21,6 +21,13 @@ export interface SessionMeta {
21
21
  * Lets findLatestSessionByChannel pick up the right session on bot restart.
22
22
  */
23
23
  channel?: string;
24
+ /**
25
+ * Per-tool invocation counts for this session, aggregated across every
26
+ * turn. Populated by the agent loop at each tool-call batch. Used by the
27
+ * opt-in telemetry subsystem to aggregate vertical-usage signals — do NOT
28
+ * add any tool inputs or outputs here, just the count per tool name.
29
+ */
30
+ toolCallCounts?: Record<string, number>;
24
31
  }
25
32
  /** Get the absolute path to a session's JSONL file (for external readers like search). */
26
33
  export declare function getSessionFilePath(id: string): string;
@@ -95,6 +95,9 @@ export function updateSessionMeta(sessionId, meta) {
95
95
  ...(meta.channel !== undefined || existing?.channel !== undefined
96
96
  ? { channel: meta.channel ?? existing?.channel }
97
97
  : {}),
98
+ ...(meta.toolCallCounts !== undefined || existing?.toolCallCounts !== undefined
99
+ ? { toolCallCounts: meta.toolCallCounts ?? existing?.toolCallCounts }
100
+ : {}),
98
101
  };
99
102
  // Atomic write: tmp file + rename. Prevents corruption when parent
100
103
  // and sub-agent update the same session meta concurrently.
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Opt-in local telemetry.
3
+ *
4
+ * Principles (non-negotiable):
5
+ * 1. Opt-in only — default is off. A fresh install collects nothing.
6
+ * 2. Local only — this module writes to ~/.blockrun/telemetry.jsonl.
7
+ * No network transmission, ever. A future opt-in "upload" feature
8
+ * would be a separate module with its own consent gate.
9
+ * 3. No content — never log prompts, tool inputs, tool outputs, file
10
+ * paths, or wallet addresses. Count-level aggregates only.
11
+ * 4. Inspectable — the log is plain JSONL, one record per session.
12
+ * `franklin telemetry view` prints it. Users see exactly what was
13
+ * recorded before ever considering sharing it.
14
+ * 5. Revocable — `franklin telemetry disable` stops future writes
15
+ * and leaves historical data intact. `franklin telemetry reset`
16
+ * (future) would wipe the log.
17
+ *
18
+ * Data model is a sanitized projection of SessionMeta. Nothing original
19
+ * is stored here that isn't already derivable from the session meta
20
+ * files — telemetry is just a stable, aggregation-friendly view of
21
+ * information the user already has.
22
+ */
23
+ import type { SessionMeta } from '../session/storage.js';
24
+ interface ConsentRecord {
25
+ enabled: boolean;
26
+ enabledAt?: number;
27
+ disabledAt?: number;
28
+ }
29
+ /** Sanitized projection of a session used for telemetry. No content. */
30
+ export interface TelemetryRecord {
31
+ /** Stable per-install random UUID. Not tied to wallet or email. */
32
+ installId: string;
33
+ /** Franklin version at the time this session ran. */
34
+ version: string;
35
+ /** Session timestamp (ISO string). */
36
+ ts: string;
37
+ /** Number of user turns. */
38
+ turns: number;
39
+ /** Number of message entries (user + assistant + tool_result). */
40
+ messages: number;
41
+ /** Input / output tokens for the whole session. */
42
+ inputTokens: number;
43
+ outputTokens: number;
44
+ /** Cost in USDC. */
45
+ costUsd: number;
46
+ /** Savings vs Opus-tier baseline in USDC. */
47
+ savedVsOpusUsd: number;
48
+ /** Last-active model id for the session. */
49
+ model: string;
50
+ /** Chain the session settled on (base / solana). */
51
+ chain?: string;
52
+ /** Session driver — "cli" for normal use, or the channel tag for Telegram/etc. */
53
+ driver: string;
54
+ /** Per-tool invocation counts (names only, no content). */
55
+ toolCallCounts?: Record<string, number>;
56
+ }
57
+ /** Enabled-state check. Default: false. */
58
+ export declare function isTelemetryEnabled(): boolean;
59
+ export declare function setTelemetryEnabled(enabled: boolean): void;
60
+ export declare function readConsent(): ConsentRecord;
61
+ /** Stable per-install random UUID. Generated lazily on first write. */
62
+ export declare function getOrCreateInstallId(): string;
63
+ /**
64
+ * Sanitize a SessionMeta into a telemetry record. No content is added here
65
+ * that isn't already present in the meta — the sanitization rule is that
66
+ * every field must be count-level or identifier-level, never user content.
67
+ */
68
+ export declare function sessionMetaToRecord(meta: SessionMeta, installId: string, chain?: string): TelemetryRecord;
69
+ /** Append one record to the telemetry log. Silent no-op if disabled. */
70
+ export declare function recordSession(meta: SessionMeta, chain?: string): void;
71
+ /**
72
+ * Locate the session that just finished by ID or by "newest in the sessions
73
+ * directory whose workDir matches", then record it. Used by start.ts at
74
+ * exit since interactiveSession() doesn't currently thread the session id
75
+ * back to the caller.
76
+ */
77
+ export declare function recordLatestSessionIfEnabled(workingDir: string, chain?: string): void;
78
+ /** Read every record in the log. Returns [] if the file is missing. */
79
+ export declare function readAllRecords(): TelemetryRecord[];
80
+ /** File paths — surfaced so the CLI can show users where data lives. */
81
+ export declare const telemetryPaths: {
82
+ consent: string;
83
+ log: string;
84
+ installId: string;
85
+ };
86
+ export {};
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Opt-in local telemetry.
3
+ *
4
+ * Principles (non-negotiable):
5
+ * 1. Opt-in only — default is off. A fresh install collects nothing.
6
+ * 2. Local only — this module writes to ~/.blockrun/telemetry.jsonl.
7
+ * No network transmission, ever. A future opt-in "upload" feature
8
+ * would be a separate module with its own consent gate.
9
+ * 3. No content — never log prompts, tool inputs, tool outputs, file
10
+ * paths, or wallet addresses. Count-level aggregates only.
11
+ * 4. Inspectable — the log is plain JSONL, one record per session.
12
+ * `franklin telemetry view` prints it. Users see exactly what was
13
+ * recorded before ever considering sharing it.
14
+ * 5. Revocable — `franklin telemetry disable` stops future writes
15
+ * and leaves historical data intact. `franklin telemetry reset`
16
+ * (future) would wipe the log.
17
+ *
18
+ * Data model is a sanitized projection of SessionMeta. Nothing original
19
+ * is stored here that isn't already derivable from the session meta
20
+ * files — telemetry is just a stable, aggregation-friendly view of
21
+ * information the user already has.
22
+ */
23
+ import fs from 'node:fs';
24
+ import path from 'node:path';
25
+ import crypto from 'node:crypto';
26
+ import { BLOCKRUN_DIR, VERSION } from '../config.js';
27
+ const CONSENT_FILE = path.join(BLOCKRUN_DIR, 'telemetry-consent.json');
28
+ const LOG_FILE = path.join(BLOCKRUN_DIR, 'telemetry.jsonl');
29
+ const INSTALL_ID_FILE = path.join(BLOCKRUN_DIR, 'telemetry-install-id.txt');
30
+ function ensureDir() {
31
+ fs.mkdirSync(BLOCKRUN_DIR, { recursive: true });
32
+ }
33
+ /** Enabled-state check. Default: false. */
34
+ export function isTelemetryEnabled() {
35
+ try {
36
+ const raw = fs.readFileSync(CONSENT_FILE, 'utf-8');
37
+ const record = JSON.parse(raw);
38
+ return record.enabled === true;
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ }
44
+ export function setTelemetryEnabled(enabled) {
45
+ ensureDir();
46
+ const existing = readConsent();
47
+ const next = enabled
48
+ ? { enabled: true, enabledAt: Date.now() }
49
+ : { enabled: false, enabledAt: existing.enabledAt, disabledAt: Date.now() };
50
+ fs.writeFileSync(CONSENT_FILE, JSON.stringify(next, null, 2));
51
+ }
52
+ export function readConsent() {
53
+ try {
54
+ return JSON.parse(fs.readFileSync(CONSENT_FILE, 'utf-8'));
55
+ }
56
+ catch {
57
+ return { enabled: false };
58
+ }
59
+ }
60
+ /** Stable per-install random UUID. Generated lazily on first write. */
61
+ export function getOrCreateInstallId() {
62
+ try {
63
+ const raw = fs.readFileSync(INSTALL_ID_FILE, 'utf-8').trim();
64
+ if (raw.length > 0)
65
+ return raw;
66
+ }
67
+ catch { /* first run */ }
68
+ ensureDir();
69
+ const id = crypto.randomUUID();
70
+ fs.writeFileSync(INSTALL_ID_FILE, id);
71
+ return id;
72
+ }
73
+ /**
74
+ * Sanitize a SessionMeta into a telemetry record. No content is added here
75
+ * that isn't already present in the meta — the sanitization rule is that
76
+ * every field must be count-level or identifier-level, never user content.
77
+ */
78
+ export function sessionMetaToRecord(meta, installId, chain) {
79
+ return {
80
+ installId,
81
+ version: VERSION,
82
+ ts: new Date(meta.updatedAt).toISOString(),
83
+ turns: meta.turnCount ?? 0,
84
+ messages: meta.messageCount ?? 0,
85
+ inputTokens: meta.inputTokens ?? 0,
86
+ outputTokens: meta.outputTokens ?? 0,
87
+ costUsd: meta.costUsd ?? 0,
88
+ savedVsOpusUsd: meta.savedVsOpusUsd ?? 0,
89
+ model: meta.model ?? 'unknown',
90
+ chain,
91
+ // "cli" if no channel tag, else the channel string (e.g. "telegram:12345").
92
+ // Channel may include an owner id; we deliberately keep it because the
93
+ // install id is already user-agnostic and linking a driver to a user
94
+ // is necessary to distinguish "single user with Telegram" from "many
95
+ // users with CLI" in aggregate data. Users who don't want this strip
96
+ // it by running telemetry disable.
97
+ driver: meta.channel ?? 'cli',
98
+ ...(meta.toolCallCounts ? { toolCallCounts: meta.toolCallCounts } : {}),
99
+ };
100
+ }
101
+ /** Append one record to the telemetry log. Silent no-op if disabled. */
102
+ export function recordSession(meta, chain) {
103
+ if (!isTelemetryEnabled())
104
+ return;
105
+ ensureDir();
106
+ const record = sessionMetaToRecord(meta, getOrCreateInstallId(), chain);
107
+ try {
108
+ fs.appendFileSync(LOG_FILE, JSON.stringify(record) + '\n');
109
+ }
110
+ catch {
111
+ // Telemetry is best-effort — never block a user session on a disk write.
112
+ }
113
+ }
114
+ /**
115
+ * Locate the session that just finished by ID or by "newest in the sessions
116
+ * directory whose workDir matches", then record it. Used by start.ts at
117
+ * exit since interactiveSession() doesn't currently thread the session id
118
+ * back to the caller.
119
+ */
120
+ export function recordLatestSessionIfEnabled(workingDir, chain) {
121
+ if (!isTelemetryEnabled())
122
+ return;
123
+ // Lazy import to avoid a circular session/storage <-> telemetry dependency.
124
+ // Using require() here keeps this module synchronous for tests.
125
+ /* eslint-disable @typescript-eslint/no-require-imports */
126
+ const { listSessions } = require('../session/storage.js');
127
+ /* eslint-enable @typescript-eslint/no-require-imports */
128
+ const sessions = listSessions();
129
+ const match = sessions.find(s => s.workDir === workingDir);
130
+ if (!match)
131
+ return;
132
+ recordSession(match, chain);
133
+ }
134
+ /** Read every record in the log. Returns [] if the file is missing. */
135
+ export function readAllRecords() {
136
+ try {
137
+ const raw = fs.readFileSync(LOG_FILE, 'utf-8');
138
+ const out = [];
139
+ for (const line of raw.split('\n')) {
140
+ if (!line.trim())
141
+ continue;
142
+ try {
143
+ out.push(JSON.parse(line));
144
+ }
145
+ catch { /* skip corrupt */ }
146
+ }
147
+ return out;
148
+ }
149
+ catch {
150
+ return [];
151
+ }
152
+ }
153
+ /** File paths — surfaced so the CLI can show users where data lives. */
154
+ export const telemetryPaths = {
155
+ consent: CONSENT_FILE,
156
+ log: LOG_FILE,
157
+ installId: INSTALL_ID_FILE,
158
+ };
@@ -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.5",
3
+ "version": "3.8.7",
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": {
@@ -52,10 +52,10 @@
52
52
  "license": "Apache-2.0",
53
53
  "repository": {
54
54
  "type": "git",
55
- "url": "git+https://github.com/RunFranklin/franklin.git"
55
+ "url": "git+https://github.com/BlockRunAI/Franklin.git"
56
56
  },
57
57
  "bugs": {
58
- "url": "https://github.com/RunFranklin/franklin/issues"
58
+ "url": "https://github.com/BlockRunAI/Franklin/issues"
59
59
  },
60
60
  "homepage": "https://Franklin.run",
61
61
  "engines": {