@blockrun/runcode 2.5.0 → 2.5.1

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.
@@ -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
@@ -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) {
@@ -170,7 +170,6 @@ async function runWithInkUI(agentConfig, model, workDir, version, walletInfo, on
170
170
  flushStats();
171
171
  await disconnectMcpServers();
172
172
  console.log(chalk.dim('\nGoodbye.\n'));
173
- process.exit(0);
174
173
  }
175
174
  // ─── Basic readline UI (piped input) ───────────────────────────────────────
176
175
  async function runWithBasicUI(agentConfig, model, workDir) {
@@ -224,7 +223,6 @@ async function runWithBasicUI(agentConfig, model, workDir) {
224
223
  }
225
224
  ui.printGoodbye();
226
225
  flushStats();
227
- process.exit(0);
228
226
  }
229
227
  async function handleSlashCommand(cmd, config, ui) {
230
228
  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.1",
4
4
  "description": "RunCode — AI coding agent powered by 41+ models. Pay per use with USDC.",
5
5
  "type": "module",
6
6
  "bin": {