@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.
- package/dist/agent/commands.js +3 -1
- package/dist/agent/llm.js +8 -1
- package/dist/agent/loop.js +9 -5
- package/dist/agent/permissions.js +9 -2
- package/dist/agent/streaming-executor.js +19 -13
- package/dist/commands/start.js +3 -3
- package/dist/index.js +2 -0
- package/dist/tools/bash.js +16 -9
- package/dist/ui/terminal.d.ts +9 -1
- package/dist/ui/terminal.js +55 -36
- package/package.json +1 -1
package/dist/agent/commands.js
CHANGED
|
@@ -179,7 +179,9 @@ const DIRECT_COMMANDS = {
|
|
|
179
179
|
catch {
|
|
180
180
|
checks.push('⚠ ripgrep not found (using native grep fallback)');
|
|
181
181
|
}
|
|
182
|
-
|
|
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
|
|
76
|
+
payload: { status: response.status, message },
|
|
70
77
|
};
|
|
71
78
|
return;
|
|
72
79
|
}
|
package/dist/agent/loop.js
CHANGED
|
@@ -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(
|
|
404
|
+
updateActualTokens(inputTokens, usage.outputTokens, history.length);
|
|
400
405
|
onEvent({
|
|
401
406
|
kind: 'usage',
|
|
402
|
-
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
|
-
|
|
408
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
results
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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) {
|
package/dist/commands/start.js
CHANGED
|
@@ -123,7 +123,9 @@ export async function startCommand(options) {
|
|
|
123
123
|
capabilities,
|
|
124
124
|
maxTurns: 100,
|
|
125
125
|
workingDir: workDir,
|
|
126
|
-
|
|
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();
|
package/dist/tools/bash.js
CHANGED
|
@@ -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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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;
|
package/dist/ui/terminal.d.ts
CHANGED
|
@@ -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;
|
package/dist/ui/terminal.js
CHANGED
|
@@ -131,38 +131,65 @@ export class TerminalUI {
|
|
|
131
131
|
totalInputTokens = 0;
|
|
132
132
|
totalOutputTokens = 0;
|
|
133
133
|
mdRenderer = new MarkdownRenderer();
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
}
|