@blockrun/runcode 2.5.0 → 2.5.2

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.
@@ -179,7 +179,9 @@ const DIRECT_COMMANDS = {
179
179
  catch {
180
180
  checks.push('⚠ ripgrep not found (using native grep fallback)');
181
181
  }
182
- checks.push(fs.existsSync(path.join(BLOCKRUN_DIR, 'wallet.json')) ? '✓ wallet configured' : '⚠ no wallet — run: runcode setup');
182
+ const hasWallet = fs.existsSync(path.join(BLOCKRUN_DIR, 'wallet.json'))
183
+ || fs.existsSync(path.join(BLOCKRUN_DIR, 'solana-wallet.json'));
184
+ checks.push(hasWallet ? '✓ wallet configured' : '⚠ no wallet — run: runcode setup');
183
185
  checks.push(fs.existsSync(path.join(BLOCKRUN_DIR, 'runcode-config.json')) ? '✓ config file exists' : '⚠ no config — using defaults');
184
186
  // Check MCP
185
187
  const { listMcpServers } = await import('../mcp/client.js');
package/dist/agent/llm.js CHANGED
@@ -64,9 +64,16 @@ export class ModelClient {
64
64
  }
65
65
  if (!response.ok) {
66
66
  const errorBody = await response.text().catch(() => 'unknown error');
67
+ // Extract human-readable message from JSON error bodies ({"error":{"message":"..."}})
68
+ let message = errorBody;
69
+ try {
70
+ const parsed = JSON.parse(errorBody);
71
+ message = parsed?.error?.message || parsed?.message || errorBody;
72
+ }
73
+ catch { /* not JSON — use raw text */ }
67
74
  yield {
68
75
  kind: 'error',
69
- payload: { status: response.status, message: errorBody },
76
+ payload: { status: response.status, message },
70
77
  };
71
78
  return;
72
79
  }
@@ -267,6 +267,7 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
267
267
  if (microCompacted !== history) {
268
268
  history.length = 0;
269
269
  history.push(...microCompacted);
270
+ resetTokenAnchor(); // History shrunk — resync token tracking
270
271
  }
271
272
  }
272
273
  // 3. Auto-compact: summarize history if approaching context limit
@@ -395,18 +396,21 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
395
396
  onEvent({ kind: 'turn_done', reason: 'error', error: errMsg + suggestion });
396
397
  break;
397
398
  }
399
+ // When API doesn't return input tokens (some models return 0), estimate from history
400
+ const inputTokens = usage.inputTokens > 0
401
+ ? usage.inputTokens
402
+ : estimateHistoryTokens(history);
398
403
  // Anchor token tracking to actual API counts
399
- updateActualTokens(usage.inputTokens, usage.outputTokens, history.length);
404
+ updateActualTokens(inputTokens, usage.outputTokens, history.length);
400
405
  onEvent({
401
406
  kind: 'usage',
402
- inputTokens: usage.inputTokens,
407
+ inputTokens,
403
408
  outputTokens: usage.outputTokens,
404
409
  model: config.model,
405
410
  });
406
411
  // Record usage for stats tracking (runcode stats command)
407
- // Rough cost estimate: use typical pricing if unknown
408
- const costEstimate = estimateCost(config.model, usage.inputTokens, usage.outputTokens);
409
- recordUsage(config.model, usage.inputTokens, usage.outputTokens, costEstimate, 0);
412
+ const costEstimate = estimateCost(config.model, inputTokens, usage.outputTokens);
413
+ recordUsage(config.model, inputTokens, usage.outputTokens, costEstimate, 0);
410
414
  // ── Max output tokens recovery ──
411
415
  if (stopReason === 'max_tokens' && recoveryAttempts < 3) {
412
416
  recoveryAttempts++;
@@ -177,10 +177,17 @@ export class PermissionManager {
177
177
  }
178
178
  // ─── Helpers ───────────────────────────────────────────────────────────────
179
179
  function askQuestion(prompt) {
180
+ // Non-TTY (piped/scripted) input: cannot ask interactively — auto-allow.
181
+ // The caller (permissionMode logic in start.ts) already routes piped sessions
182
+ // to trust mode, so this path is rarely hit. Guard here for safety.
183
+ if (!process.stdin.isTTY) {
184
+ process.stderr.write(prompt + 'y (auto-approved: non-interactive mode)\n');
185
+ return Promise.resolve('y');
186
+ }
180
187
  const rl = readline.createInterface({
181
188
  input: process.stdin,
182
189
  output: process.stderr,
183
- terminal: process.stdin.isTTY ?? false,
190
+ terminal: true,
184
191
  });
185
192
  return new Promise((resolve) => {
186
193
  let answered = false;
@@ -191,7 +198,7 @@ function askQuestion(prompt) {
191
198
  });
192
199
  rl.on('close', () => {
193
200
  if (!answered)
194
- resolve('n'); // Default deny on EOF (piped input) for safety
201
+ resolve('n'); // Default deny on EOF for safety
195
202
  });
196
203
  });
197
204
  }
@@ -39,21 +39,27 @@ export class StreamingExecutor {
39
39
  async collectResults(allInvocations) {
40
40
  const results = [];
41
41
  const alreadyStarted = new Set(this.pending.map(p => p.invocation.id));
42
- // Wait for concurrent results that were started during streaming
43
- for (const p of this.pending) {
44
- const result = await p.promise;
45
- results.push([p.invocation, result]);
42
+ const pendingSnapshot = [...this.pending];
43
+ this.pending = []; // Clear immediately so errors don't leave stale state
44
+ try {
45
+ // Wait for concurrent results that were started during streaming
46
+ for (const p of pendingSnapshot) {
47
+ const result = await p.promise;
48
+ results.push([p.invocation, result]);
49
+ }
50
+ // Execute sequential (non-concurrent) tools now
51
+ for (const inv of allInvocations) {
52
+ if (alreadyStarted.has(inv.id))
53
+ continue;
54
+ this.onStart(inv.id, inv.name);
55
+ const result = await this.executeWithPermissions(inv);
56
+ results.push([inv, result]);
57
+ }
46
58
  }
47
- // Execute sequential (non-concurrent) tools now
48
- for (const inv of allInvocations) {
49
- if (alreadyStarted.has(inv.id))
50
- continue;
51
- this.onStart(inv.id, inv.name);
52
- const result = await this.executeWithPermissions(inv);
53
- results.push([inv, result]);
59
+ catch (err) {
60
+ // Return partial results rather than losing them; caller handles errors
61
+ throw err;
54
62
  }
55
- // Clear for next round
56
- this.pending = [];
57
63
  return results;
58
64
  }
59
65
  async executeWithPermissions(invocation) {
@@ -123,7 +123,9 @@ export async function startCommand(options) {
123
123
  capabilities,
124
124
  maxTurns: 100,
125
125
  workingDir: workDir,
126
- permissionMode: options.trust ? 'trust' : 'default',
126
+ // Non-TTY (piped) input = scripted mode → trust all tools automatically.
127
+ // Interactive TTY = default mode (prompts for Bash/Write/Edit).
128
+ permissionMode: (options.trust || !process.stdin.isTTY) ? 'trust' : 'default',
127
129
  debug: options.debug,
128
130
  };
129
131
  // Use ink UI if TTY, fallback to basic readline for piped input
@@ -170,7 +172,6 @@ async function runWithInkUI(agentConfig, model, workDir, version, walletInfo, on
170
172
  flushStats();
171
173
  await disconnectMcpServers();
172
174
  console.log(chalk.dim('\nGoodbye.\n'));
173
- process.exit(0);
174
175
  }
175
176
  // ─── Basic readline UI (piped input) ───────────────────────────────────────
176
177
  async function runWithBasicUI(agentConfig, model, workDir) {
@@ -224,7 +225,6 @@ async function runWithBasicUI(agentConfig, model, workDir) {
224
225
  }
225
226
  ui.printGoodbye();
226
227
  flushStats();
227
- process.exit(0);
228
228
  }
229
229
  async function handleSlashCommand(cmd, config, ui) {
230
230
  const parts = cmd.trim().split(/\s+/);
package/dist/index.js CHANGED
@@ -108,6 +108,7 @@ if (firstArg === 'solana' || firstArg === 'base') {
108
108
  }
109
109
  }
110
110
  await startCommand(startOpts);
111
+ process.exit(0);
111
112
  }
112
113
  else if (!firstArg || (firstArg.startsWith('-') && !['-h', '--help', '-V', '--version'].includes(firstArg))) {
113
114
  // No subcommand or only flags — treat as 'start' with flags
@@ -122,6 +123,7 @@ else if (!firstArg || (firstArg.startsWith('-') && !['-h', '--help', '-V', '--ve
122
123
  }
123
124
  }
124
125
  await startCommand(startOpts);
126
+ process.exit(0);
125
127
  }
126
128
  else {
127
129
  program.parse();
@@ -12,15 +12,22 @@ async function execute(input, ctx) {
12
12
  const timeoutMs = Math.min(timeout ?? DEFAULT_TIMEOUT_MS, 600_000);
13
13
  return new Promise((resolve) => {
14
14
  const shell = process.env.SHELL || '/bin/bash';
15
- const child = spawn(shell, ['-c', command], {
16
- cwd: ctx.workingDir,
17
- env: {
18
- ...process.env,
19
- RUNCODE: '1', // Let scripts detect they're running inside runcode
20
- RUNCODE_WORKDIR: ctx.workingDir,
21
- },
22
- stdio: ['ignore', 'pipe', 'pipe'],
23
- });
15
+ let child;
16
+ try {
17
+ child = spawn(shell, ['-c', command], {
18
+ cwd: ctx.workingDir,
19
+ env: {
20
+ ...process.env,
21
+ RUNCODE: '1', // Let scripts detect they're running inside runcode
22
+ RUNCODE_WORKDIR: ctx.workingDir,
23
+ },
24
+ stdio: ['ignore', 'pipe', 'pipe'],
25
+ });
26
+ }
27
+ catch (spawnErr) {
28
+ resolve({ output: `Error spawning shell: ${spawnErr.message}`, isError: true });
29
+ return;
30
+ }
24
31
  let stdout = '';
25
32
  let stderr = '';
26
33
  let outputBytes = 0;
@@ -10,15 +10,23 @@ export declare class TerminalUI {
10
10
  private totalInputTokens;
11
11
  private totalOutputTokens;
12
12
  private mdRenderer;
13
+ private lineQueue;
14
+ private lineWaiters;
15
+ private stdinEOF;
16
+ constructor();
13
17
  /**
14
18
  * Prompt the user for input. Returns null on EOF/exit.
19
+ * Uses a line-queue approach so piped input works across multiple calls.
15
20
  */
16
21
  promptUser(promptText?: string): Promise<string | null>;
22
+ private nextLine;
23
+ /** No-op kept for API compatibility — readline closes when stdin EOF. */
24
+ closeInput(): void;
17
25
  /**
18
26
  * Handle a stream event from the agent loop.
19
27
  */
20
28
  handleEvent(event: StreamEvent): void;
21
- /** Check if input is a slash command. Returns true if handled. */
29
+ /** Check if input is a slash command. Returns true if handled locally (don't pass to agent). */
22
30
  handleSlashCommand(input: string): boolean;
23
31
  printWelcome(model: string, workDir: string): void;
24
32
  printUsageSummary(): void;
@@ -131,38 +131,65 @@ export class TerminalUI {
131
131
  totalInputTokens = 0;
132
132
  totalOutputTokens = 0;
133
133
  mdRenderer = new MarkdownRenderer();
134
- /**
135
- * Prompt the user for input. Returns null on EOF/exit.
136
- */
137
- async promptUser(promptText) {
134
+ // Line queue for piped (non-TTY) input — buffers all stdin lines eagerly
135
+ lineQueue = [];
136
+ lineWaiters = [];
137
+ stdinEOF = false;
138
+ constructor() {
138
139
  const rl = readline.createInterface({
139
140
  input: process.stdin,
140
141
  output: process.stderr,
141
- terminal: process.stdin.isTTY ?? false,
142
+ terminal: false, // Always treat as non-TTY so line events fire for piped input
143
+ });
144
+ rl.on('line', (line) => {
145
+ if (this.lineWaiters.length > 0) {
146
+ // Someone is already waiting — deliver immediately
147
+ const waiter = this.lineWaiters.shift();
148
+ waiter(line);
149
+ }
150
+ else {
151
+ // Buffer the line for the next promptUser() call
152
+ this.lineQueue.push(line);
153
+ }
154
+ });
155
+ rl.on('close', () => {
156
+ this.stdinEOF = true;
157
+ this.lineQueue = []; // Don't deliver buffered lines after EOF — signal exit cleanly
158
+ for (const waiter of this.lineWaiters)
159
+ waiter(null);
160
+ this.lineWaiters = [];
142
161
  });
162
+ }
163
+ /**
164
+ * Prompt the user for input. Returns null on EOF/exit.
165
+ * Uses a line-queue approach so piped input works across multiple calls.
166
+ */
167
+ async promptUser(promptText) {
168
+ const prompt = promptText ?? chalk.bold.green('> ');
169
+ process.stderr.write(prompt);
170
+ const raw = await this.nextLine();
171
+ if (raw === null)
172
+ return null;
173
+ const trimmed = raw.trim();
174
+ if (trimmed === '/exit' || trimmed === '/quit')
175
+ return null;
176
+ return trimmed;
177
+ }
178
+ nextLine() {
179
+ if (this.lineQueue.length > 0) {
180
+ return Promise.resolve(this.lineQueue.shift());
181
+ }
182
+ if (this.stdinEOF) {
183
+ return Promise.resolve(null);
184
+ }
143
185
  return new Promise((resolve) => {
144
- let answered = false;
145
- const prompt = promptText ?? chalk.bold.green('> ');
146
- rl.question(prompt, (answer) => {
147
- answered = true;
148
- rl.close();
149
- const trimmed = answer.trim();
150
- if (trimmed === '/exit' || trimmed === '/quit') {
151
- resolve(null);
152
- }
153
- else if (trimmed === '') {
154
- resolve('');
155
- }
156
- else {
157
- resolve(trimmed);
158
- }
159
- });
160
- rl.on('close', () => {
161
- if (!answered)
162
- resolve(null);
163
- });
186
+ this.lineWaiters.push(resolve);
164
187
  });
165
188
  }
189
+ /** No-op kept for API compatibility — readline closes when stdin EOF. */
190
+ closeInput() {
191
+ // Nothing to do — readline closes itself on stdin EOF
192
+ }
166
193
  /**
167
194
  * Handle a stream event from the agent loop.
168
195
  */
@@ -260,26 +287,17 @@ export class TerminalUI {
260
287
  }
261
288
  }
262
289
  }
263
- /** Check if input is a slash command. Returns true if handled. */
290
+ /** Check if input is a slash command. Returns true if handled locally (don't pass to agent). */
264
291
  handleSlashCommand(input) {
265
292
  const parts = input.trim().split(/\s+/);
266
293
  const cmd = parts[0].toLowerCase();
267
294
  switch (cmd) {
268
- case '/help':
269
- console.error(chalk.bold('\n Commands:'));
270
- console.error(' /model [name] — switch model (e.g. /model sonnet)');
271
- console.error(' /cost — session cost and tokens');
272
- console.error(' /retry — retry the last prompt');
273
- console.error(' /compact — compress conversation history');
274
- console.error(' /exit — quit');
275
- console.error(' /help — this help\n');
276
- console.error(chalk.dim(' Shortcuts: sonnet, opus, gpt, gemini, deepseek, flash, free, r1, o4\n'));
277
- return true;
278
295
  case '/cost':
279
296
  case '/usage':
280
297
  console.error(chalk.dim(`\n Tokens: ${this.totalInputTokens.toLocaleString()} in / ${this.totalOutputTokens.toLocaleString()} out\n`));
281
298
  return true;
282
299
  default:
300
+ // All other slash commands pass through to the agent loop (commands.ts handles them)
283
301
  return false;
284
302
  }
285
303
  }
@@ -294,6 +312,7 @@ export class TerminalUI {
294
312
  }
295
313
  }
296
314
  printGoodbye() {
315
+ this.closeInput();
297
316
  this.printUsageSummary();
298
317
  console.error(chalk.dim('\nGoodbye.\n'));
299
318
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/runcode",
3
- "version": "2.5.0",
3
+ "version": "2.5.2",
4
4
  "description": "RunCode — AI coding agent powered by 41+ models. Pay per use with USDC.",
5
5
  "type": "module",
6
6
  "bin": {