@blockrun/runcode 2.5.1 → 2.5.3
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 +110 -10
- package/dist/agent/llm.js +8 -1
- package/dist/agent/loop.d.ts +0 -5
- package/dist/agent/loop.js +28 -188
- package/dist/agent/permissions.js +9 -2
- package/dist/commands/start.js +3 -1
- package/dist/ui/app.js +4 -2
- package/dist/ui/terminal.js +3 -1
- package/package.json +1 -1
package/dist/agent/commands.js
CHANGED
|
@@ -12,6 +12,8 @@ import { execSync } from 'node:child_process';
|
|
|
12
12
|
import { BLOCKRUN_DIR, VERSION } from '../config.js';
|
|
13
13
|
import { estimateHistoryTokens, getAnchoredTokenCount, getContextWindow, resetTokenAnchor } from './tokens.js';
|
|
14
14
|
import { forceCompact } from './compact.js';
|
|
15
|
+
import { getStatsSummary } from '../stats/tracker.js';
|
|
16
|
+
import { resolveModel } from '../ui/model-picker.js';
|
|
15
17
|
import { listSessions, loadSessionHistory, } from '../session/storage.js';
|
|
16
18
|
// ─── Git helpers ──────────────────────────────────────────────────────────
|
|
17
19
|
function gitExec(cmd, cwd, timeout = 5000, maxBuffer) {
|
|
@@ -28,7 +30,12 @@ function gitCmd(ctx, cmd, timeout, maxBuffer) {
|
|
|
28
30
|
return gitExec(cmd, ctx.config.workingDir || process.cwd(), timeout, maxBuffer);
|
|
29
31
|
}
|
|
30
32
|
catch (e) {
|
|
31
|
-
|
|
33
|
+
// Prefer stderr (actual git error message) over the noisy "Command failed: ..." header
|
|
34
|
+
const errObj = e;
|
|
35
|
+
const stderr = errObj.stderr ? String(errObj.stderr).trim() : '';
|
|
36
|
+
// Take only the first meaningful line (git sometimes dumps full usage on errors)
|
|
37
|
+
const firstLine = (stderr || errObj.message || 'unknown').split('\n')[0].trim();
|
|
38
|
+
ctx.onEvent({ kind: 'text_delta', text: `Git: ${firstLine}\n` });
|
|
32
39
|
return null;
|
|
33
40
|
}
|
|
34
41
|
}
|
|
@@ -41,34 +48,51 @@ const DIRECT_COMMANDS = {
|
|
|
41
48
|
'/stash': (ctx) => {
|
|
42
49
|
const r = gitCmd(ctx, 'git stash push -m "runcode auto-stash"', 10000);
|
|
43
50
|
if (r !== null)
|
|
44
|
-
ctx.onEvent({ kind: 'text_delta', text: r
|
|
51
|
+
ctx.onEvent({ kind: 'text_delta', text: r ? `${r}\n` : 'No changes to stash.\n' });
|
|
45
52
|
emitDone(ctx);
|
|
46
53
|
},
|
|
47
54
|
'/unstash': (ctx) => {
|
|
48
55
|
const r = gitCmd(ctx, 'git stash pop', 10000);
|
|
49
56
|
if (r !== null)
|
|
50
|
-
ctx.onEvent({ kind: 'text_delta', text: r
|
|
57
|
+
ctx.onEvent({ kind: 'text_delta', text: r ? `${r}\n` : 'Stash applied.\n' });
|
|
51
58
|
emitDone(ctx);
|
|
52
59
|
},
|
|
53
60
|
'/log': (ctx) => {
|
|
54
61
|
const r = gitCmd(ctx, 'git log --oneline -15 --no-color');
|
|
55
|
-
|
|
62
|
+
if (r !== null)
|
|
63
|
+
ctx.onEvent({ kind: 'text_delta', text: r ? `\`\`\`\n${r}\n\`\`\`\n` : 'No commits yet.\n' });
|
|
56
64
|
emitDone(ctx);
|
|
57
65
|
},
|
|
58
66
|
'/status': (ctx) => {
|
|
59
67
|
const r = gitCmd(ctx, 'git status --short --branch');
|
|
60
|
-
|
|
68
|
+
if (r !== null)
|
|
69
|
+
ctx.onEvent({ kind: 'text_delta', text: r ? `\`\`\`\n${r}\n\`\`\`\n` : 'Working tree clean.\n' });
|
|
61
70
|
emitDone(ctx);
|
|
62
71
|
},
|
|
63
72
|
'/diff': (ctx) => {
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
// git diff with stat header then full diff
|
|
74
|
+
const stat = gitCmd(ctx, 'git diff --stat --no-color');
|
|
75
|
+
if (stat === null) {
|
|
76
|
+
emitDone(ctx);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const full = gitCmd(ctx, 'git diff --no-color');
|
|
80
|
+
if (full === null) {
|
|
81
|
+
emitDone(ctx);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (!stat && !full) {
|
|
85
|
+
ctx.onEvent({ kind: 'text_delta', text: 'No unstaged changes.\n' });
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
ctx.onEvent({ kind: 'text_delta', text: `\`\`\`diff\n${[stat, full].filter(Boolean).join('\n---\n')}\n\`\`\`\n` });
|
|
89
|
+
}
|
|
66
90
|
emitDone(ctx);
|
|
67
91
|
},
|
|
68
92
|
'/undo': (ctx) => {
|
|
69
93
|
const r = gitCmd(ctx, 'git reset --soft HEAD~1');
|
|
70
94
|
if (r !== null)
|
|
71
|
-
ctx.onEvent({ kind: 'text_delta', text:
|
|
95
|
+
ctx.onEvent({ kind: 'text_delta', text: `Last commit undone. Changes preserved in staging.\n` });
|
|
72
96
|
emitDone(ctx);
|
|
73
97
|
},
|
|
74
98
|
'/tokens': (ctx) => {
|
|
@@ -179,7 +203,9 @@ const DIRECT_COMMANDS = {
|
|
|
179
203
|
catch {
|
|
180
204
|
checks.push('⚠ ripgrep not found (using native grep fallback)');
|
|
181
205
|
}
|
|
182
|
-
|
|
206
|
+
const hasWallet = fs.existsSync(path.join(BLOCKRUN_DIR, 'wallet.json'))
|
|
207
|
+
|| fs.existsSync(path.join(BLOCKRUN_DIR, 'solana-wallet.json'));
|
|
208
|
+
checks.push(hasWallet ? '✓ wallet configured' : '⚠ no wallet — run: runcode setup');
|
|
183
209
|
checks.push(fs.existsSync(path.join(BLOCKRUN_DIR, 'runcode-config.json')) ? '✓ config file exists' : '⚠ no config — using defaults');
|
|
184
210
|
// Check MCP
|
|
185
211
|
const { listMcpServers } = await import('../mcp/client.js');
|
|
@@ -254,6 +280,64 @@ const DIRECT_COMMANDS = {
|
|
|
254
280
|
}
|
|
255
281
|
emitDone(ctx);
|
|
256
282
|
},
|
|
283
|
+
'/cost': async (ctx) => {
|
|
284
|
+
const { stats, saved } = getStatsSummary();
|
|
285
|
+
ctx.onEvent({ kind: 'text_delta', text: `**Session Cost**\n` +
|
|
286
|
+
` Requests: ${stats.totalRequests}\n` +
|
|
287
|
+
` Cost: $${stats.totalCostUsd.toFixed(4)} USDC\n` +
|
|
288
|
+
` Saved: $${saved.toFixed(2)} vs Claude Opus\n` +
|
|
289
|
+
` Tokens: ${stats.totalInputTokens.toLocaleString()} in / ${stats.totalOutputTokens.toLocaleString()} out\n`
|
|
290
|
+
});
|
|
291
|
+
emitDone(ctx);
|
|
292
|
+
},
|
|
293
|
+
'/wallet': async (ctx) => {
|
|
294
|
+
const chain = (await import('../config.js')).loadChain();
|
|
295
|
+
try {
|
|
296
|
+
let address;
|
|
297
|
+
let balance;
|
|
298
|
+
if (chain === 'solana') {
|
|
299
|
+
const { getOrCreateSolanaWallet, setupAgentSolanaWallet } = await import('@blockrun/llm');
|
|
300
|
+
const w = await getOrCreateSolanaWallet();
|
|
301
|
+
address = w.address;
|
|
302
|
+
try {
|
|
303
|
+
const client = await setupAgentSolanaWallet({ silent: true });
|
|
304
|
+
const bal = await client.getBalance();
|
|
305
|
+
balance = `$${bal.toFixed(2)} USDC`;
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
balance = '(fetch failed)';
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
const { getOrCreateWallet, setupAgentWallet } = await import('@blockrun/llm');
|
|
313
|
+
const w = getOrCreateWallet();
|
|
314
|
+
address = w.address;
|
|
315
|
+
try {
|
|
316
|
+
const client = setupAgentWallet({ silent: true });
|
|
317
|
+
const bal = await client.getBalance();
|
|
318
|
+
balance = `$${bal.toFixed(2)} USDC`;
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
balance = '(fetch failed)';
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
ctx.onEvent({ kind: 'text_delta', text: `**Wallet**\n` +
|
|
325
|
+
` Chain: ${chain}\n` +
|
|
326
|
+
` Address: ${address}\n` +
|
|
327
|
+
` Balance: ${balance}\n`
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
catch (err) {
|
|
331
|
+
ctx.onEvent({ kind: 'text_delta', text: `Wallet error: ${err.message}\n` });
|
|
332
|
+
}
|
|
333
|
+
emitDone(ctx);
|
|
334
|
+
},
|
|
335
|
+
'/clear': (ctx) => {
|
|
336
|
+
ctx.history.length = 0;
|
|
337
|
+
resetTokenAnchor();
|
|
338
|
+
ctx.onEvent({ kind: 'text_delta', text: 'Conversation history cleared.\n' });
|
|
339
|
+
emitDone(ctx);
|
|
340
|
+
},
|
|
257
341
|
'/compact': async (ctx) => {
|
|
258
342
|
const beforeTokens = estimateHistoryTokens(ctx.history);
|
|
259
343
|
const { history: compacted, compacted: didCompact } = await forceCompact(ctx.history, ctx.config.model, ctx.client, ctx.config.debug);
|
|
@@ -318,12 +402,28 @@ export async function handleSlashCommand(input, ctx) {
|
|
|
318
402
|
await DIRECT_COMMANDS[input](ctx);
|
|
319
403
|
return { handled: true };
|
|
320
404
|
}
|
|
405
|
+
// /model — show current model or switch with /model <name>
|
|
406
|
+
if (input === '/model' || input.startsWith('/model ')) {
|
|
407
|
+
if (input === '/model') {
|
|
408
|
+
ctx.onEvent({ kind: 'text_delta', text: `Current model: **${ctx.config.model}**\n` +
|
|
409
|
+
`Switch with: \`/model <name>\` (e.g. \`/model sonnet\`, \`/model free\`, \`/model gemini\`)\n`
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
const newModel = resolveModel(input.slice(7).trim());
|
|
414
|
+
ctx.config.model = newModel;
|
|
415
|
+
ctx.onEvent({ kind: 'text_delta', text: `Model → **${newModel}**\n` });
|
|
416
|
+
}
|
|
417
|
+
emitDone(ctx);
|
|
418
|
+
return { handled: true };
|
|
419
|
+
}
|
|
321
420
|
// /branch has both no-arg and with-arg forms
|
|
322
421
|
if (input === '/branch' || input.startsWith('/branch ')) {
|
|
323
422
|
const cwd = ctx.config.workingDir || process.cwd();
|
|
324
423
|
if (input === '/branch') {
|
|
325
424
|
const r = gitCmd(ctx, 'git branch -v --no-color');
|
|
326
|
-
|
|
425
|
+
if (r !== null)
|
|
426
|
+
ctx.onEvent({ kind: 'text_delta', text: r ? `\`\`\`\n${r}\n\`\`\`\n` : 'No branches yet.\n' });
|
|
327
427
|
}
|
|
328
428
|
else {
|
|
329
429
|
const branchName = input.slice(8).trim();
|
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.d.ts
CHANGED
|
@@ -4,11 +4,6 @@
|
|
|
4
4
|
* Original implementation with different architecture from any reference codebase.
|
|
5
5
|
*/
|
|
6
6
|
import type { AgentConfig, Dialogue, StreamEvent } from './types.js';
|
|
7
|
-
/**
|
|
8
|
-
* Run the agent loop.
|
|
9
|
-
* Yields StreamEvents for the UI to render. Returns when the conversation ends.
|
|
10
|
-
*/
|
|
11
|
-
export declare function runAgent(config: AgentConfig, initialPrompt: string): AsyncGenerator<StreamEvent, void>;
|
|
12
7
|
/**
|
|
13
8
|
* Run a multi-turn interactive session.
|
|
14
9
|
* Each user message triggers a full agent loop.
|
package/dist/agent/loop.js
CHANGED
|
@@ -14,182 +14,6 @@ import { optimizeHistory, CAPPED_MAX_TOKENS, ESCALATED_MAX_TOKENS, getMaxOutputT
|
|
|
14
14
|
import { recordUsage } from '../stats/tracker.js';
|
|
15
15
|
import { estimateCost } from '../pricing.js';
|
|
16
16
|
import { createSessionId, appendToSession, updateSessionMeta, pruneOldSessions, } from '../session/storage.js';
|
|
17
|
-
// ─── Main Entry Point ──────────────────────────────────────────────────────
|
|
18
|
-
/**
|
|
19
|
-
* Run the agent loop.
|
|
20
|
-
* Yields StreamEvents for the UI to render. Returns when the conversation ends.
|
|
21
|
-
*/
|
|
22
|
-
export async function* runAgent(config, initialPrompt) {
|
|
23
|
-
const client = new ModelClient({
|
|
24
|
-
apiUrl: config.apiUrl,
|
|
25
|
-
chain: config.chain,
|
|
26
|
-
debug: config.debug,
|
|
27
|
-
});
|
|
28
|
-
const capabilityMap = new Map();
|
|
29
|
-
for (const cap of config.capabilities) {
|
|
30
|
-
capabilityMap.set(cap.spec.name, cap);
|
|
31
|
-
}
|
|
32
|
-
const toolDefs = config.capabilities.map((c) => c.spec);
|
|
33
|
-
const maxTurns = config.maxTurns ?? 100;
|
|
34
|
-
const workDir = config.workingDir ?? process.cwd();
|
|
35
|
-
const state = {
|
|
36
|
-
history: [
|
|
37
|
-
{ role: 'user', content: initialPrompt },
|
|
38
|
-
],
|
|
39
|
-
turnIndex: 0,
|
|
40
|
-
abort: new AbortController(),
|
|
41
|
-
};
|
|
42
|
-
// ─── Reasoning-Action Cycle ────────────────────────────────────────────
|
|
43
|
-
while (state.turnIndex < maxTurns) {
|
|
44
|
-
state.turnIndex++;
|
|
45
|
-
// 1. Call model
|
|
46
|
-
const { content: responseParts, usage } = await callModel(client, config, state, toolDefs);
|
|
47
|
-
// Emit usage
|
|
48
|
-
yield {
|
|
49
|
-
kind: 'usage',
|
|
50
|
-
inputTokens: usage.inputTokens,
|
|
51
|
-
outputTokens: usage.outputTokens,
|
|
52
|
-
model: config.model,
|
|
53
|
-
};
|
|
54
|
-
// 2. Classify response parts
|
|
55
|
-
const textParts = [];
|
|
56
|
-
const invocations = [];
|
|
57
|
-
for (const part of responseParts) {
|
|
58
|
-
if (part.type === 'text') {
|
|
59
|
-
textParts.push(part.text);
|
|
60
|
-
yield { kind: 'text_delta', text: part.text };
|
|
61
|
-
}
|
|
62
|
-
else if (part.type === 'tool_use') {
|
|
63
|
-
invocations.push(part);
|
|
64
|
-
}
|
|
65
|
-
else if (part.type === 'thinking') {
|
|
66
|
-
yield { kind: 'thinking_delta', text: part.thinking };
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
// 3. Append assistant response to history
|
|
70
|
-
state.history.push({
|
|
71
|
-
role: 'assistant',
|
|
72
|
-
content: responseParts,
|
|
73
|
-
});
|
|
74
|
-
// 4. If no capability invocations, the agent is done
|
|
75
|
-
if (invocations.length === 0) {
|
|
76
|
-
yield { kind: 'turn_done', reason: 'completed' };
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
// 5. Execute capabilities
|
|
80
|
-
const outcomes = await executeCapabilities(invocations, capabilityMap, workDir, state.abort, (evt) => { config.onEvent?.(evt); });
|
|
81
|
-
// Emit capability results
|
|
82
|
-
for (const [invocation, result] of outcomes) {
|
|
83
|
-
yield {
|
|
84
|
-
kind: 'capability_done',
|
|
85
|
-
id: invocation.id,
|
|
86
|
-
result,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
// 6. Append capability outcomes as user message
|
|
90
|
-
const outcomeContent = outcomes.map(([invocation, result]) => ({
|
|
91
|
-
type: 'tool_result',
|
|
92
|
-
tool_use_id: invocation.id,
|
|
93
|
-
content: result.output,
|
|
94
|
-
is_error: result.isError,
|
|
95
|
-
}));
|
|
96
|
-
state.history.push({
|
|
97
|
-
role: 'user',
|
|
98
|
-
content: outcomeContent,
|
|
99
|
-
});
|
|
100
|
-
// Continue to next cycle...
|
|
101
|
-
}
|
|
102
|
-
yield { kind: 'turn_done', reason: 'max_turns' };
|
|
103
|
-
}
|
|
104
|
-
// ─── Model Call ────────────────────────────────────────────────────────────
|
|
105
|
-
async function callModel(client, config, state, tools) {
|
|
106
|
-
const systemPrompt = config.systemInstructions.join('\n\n');
|
|
107
|
-
return client.complete({
|
|
108
|
-
model: config.model,
|
|
109
|
-
messages: state.history,
|
|
110
|
-
system: systemPrompt,
|
|
111
|
-
tools,
|
|
112
|
-
max_tokens: 16384,
|
|
113
|
-
stream: true,
|
|
114
|
-
}, state.abort.signal);
|
|
115
|
-
}
|
|
116
|
-
// ─── Capability Execution ──────────────────────────────────────────────────
|
|
117
|
-
async function executeCapabilities(invocations, handlers, workDir, abort, emitEvent, permissions) {
|
|
118
|
-
// Partition into concurrent-safe and sequential
|
|
119
|
-
const concurrent = [];
|
|
120
|
-
const sequential = [];
|
|
121
|
-
for (const inv of invocations) {
|
|
122
|
-
const handler = handlers.get(inv.name);
|
|
123
|
-
if (handler?.concurrent) {
|
|
124
|
-
concurrent.push(inv);
|
|
125
|
-
}
|
|
126
|
-
else {
|
|
127
|
-
sequential.push(inv);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
const results = [];
|
|
131
|
-
const scope = {
|
|
132
|
-
workingDir: workDir,
|
|
133
|
-
abortSignal: abort.signal,
|
|
134
|
-
};
|
|
135
|
-
// Run concurrent capabilities in parallel
|
|
136
|
-
if (concurrent.length > 0) {
|
|
137
|
-
const batch = concurrent.map(async (inv) => {
|
|
138
|
-
const result = await checkAndRun(inv, handlers, scope, permissions, emitEvent);
|
|
139
|
-
return [inv, result];
|
|
140
|
-
});
|
|
141
|
-
const batchResults = await Promise.all(batch);
|
|
142
|
-
results.push(...batchResults);
|
|
143
|
-
}
|
|
144
|
-
// Run sequential capabilities one at a time
|
|
145
|
-
for (const inv of sequential) {
|
|
146
|
-
const result = await checkAndRun(inv, handlers, scope, permissions, emitEvent);
|
|
147
|
-
results.push([inv, result]);
|
|
148
|
-
}
|
|
149
|
-
return results;
|
|
150
|
-
}
|
|
151
|
-
async function checkAndRun(invocation, handlers, scope, permissions, emitEvent) {
|
|
152
|
-
// Permission check
|
|
153
|
-
if (permissions) {
|
|
154
|
-
const decision = await permissions.check(invocation.name, invocation.input);
|
|
155
|
-
if (decision.behavior === 'deny') {
|
|
156
|
-
return {
|
|
157
|
-
output: `Permission denied for ${invocation.name}: ${decision.reason || 'denied by policy'}`,
|
|
158
|
-
isError: true,
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
if (decision.behavior === 'ask') {
|
|
162
|
-
const allowed = await permissions.promptUser(invocation.name, invocation.input);
|
|
163
|
-
if (!allowed) {
|
|
164
|
-
return {
|
|
165
|
-
output: `User denied permission for ${invocation.name}`,
|
|
166
|
-
isError: true,
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
emitEvent({ kind: 'capability_start', id: invocation.id, name: invocation.name });
|
|
172
|
-
return runSingleCapability(invocation, handlers, scope);
|
|
173
|
-
}
|
|
174
|
-
async function runSingleCapability(invocation, handlers, scope) {
|
|
175
|
-
const handler = handlers.get(invocation.name);
|
|
176
|
-
if (!handler) {
|
|
177
|
-
return {
|
|
178
|
-
output: `Unknown capability: ${invocation.name}`,
|
|
179
|
-
isError: true,
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
try {
|
|
183
|
-
return await handler.execute(invocation.input, scope);
|
|
184
|
-
}
|
|
185
|
-
catch (err) {
|
|
186
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
187
|
-
return {
|
|
188
|
-
output: `Error executing ${invocation.name}: ${message}`,
|
|
189
|
-
isError: true,
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
17
|
// ─── Interactive Session ───────────────────────────────────────────────────
|
|
194
18
|
/**
|
|
195
19
|
* Run a multi-turn interactive session.
|
|
@@ -211,6 +35,7 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
211
35
|
const workDir = config.workingDir ?? process.cwd();
|
|
212
36
|
const permissions = new PermissionManager(config.permissionMode ?? 'default');
|
|
213
37
|
const history = [];
|
|
38
|
+
let lastUserInput = ''; // For /retry
|
|
214
39
|
// Session persistence
|
|
215
40
|
const sessionId = createSessionId();
|
|
216
41
|
let turnCount = 0;
|
|
@@ -224,14 +49,26 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
224
49
|
continue; // Empty input → re-prompt
|
|
225
50
|
// ── Slash command dispatch ──
|
|
226
51
|
if (input.startsWith('/')) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
52
|
+
// /retry re-sends the last user message
|
|
53
|
+
if (input === '/retry') {
|
|
54
|
+
if (!lastUserInput) {
|
|
55
|
+
onEvent({ kind: 'text_delta', text: 'No previous message to retry.\n' });
|
|
56
|
+
onEvent({ kind: 'turn_done', reason: 'completed' });
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
input = lastUserInput;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
const cmdResult = await handleSlashCommand(input, {
|
|
63
|
+
history, config, client, sessionId, onEvent,
|
|
64
|
+
});
|
|
65
|
+
if (cmdResult.handled)
|
|
66
|
+
continue;
|
|
67
|
+
if (cmdResult.rewritten)
|
|
68
|
+
input = cmdResult.rewritten;
|
|
69
|
+
}
|
|
234
70
|
}
|
|
71
|
+
lastUserInput = input;
|
|
235
72
|
history.push({ role: 'user', content: input });
|
|
236
73
|
appendToSession(sessionId, { role: 'user', content: input });
|
|
237
74
|
turnCount++;
|
|
@@ -396,18 +233,21 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
396
233
|
onEvent({ kind: 'turn_done', reason: 'error', error: errMsg + suggestion });
|
|
397
234
|
break;
|
|
398
235
|
}
|
|
236
|
+
// When API doesn't return input tokens (some models return 0), estimate from history
|
|
237
|
+
const inputTokens = usage.inputTokens > 0
|
|
238
|
+
? usage.inputTokens
|
|
239
|
+
: estimateHistoryTokens(history);
|
|
399
240
|
// Anchor token tracking to actual API counts
|
|
400
|
-
updateActualTokens(
|
|
241
|
+
updateActualTokens(inputTokens, usage.outputTokens, history.length);
|
|
401
242
|
onEvent({
|
|
402
243
|
kind: 'usage',
|
|
403
|
-
inputTokens
|
|
244
|
+
inputTokens,
|
|
404
245
|
outputTokens: usage.outputTokens,
|
|
405
246
|
model: config.model,
|
|
406
247
|
});
|
|
407
248
|
// Record usage for stats tracking (runcode stats command)
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
recordUsage(config.model, usage.inputTokens, usage.outputTokens, costEstimate, 0);
|
|
249
|
+
const costEstimate = estimateCost(config.model, inputTokens, usage.outputTokens);
|
|
250
|
+
recordUsage(config.model, inputTokens, usage.outputTokens, costEstimate, 0);
|
|
411
251
|
// ── Max output tokens recovery ──
|
|
412
252
|
if (stopReason === 'max_tokens' && recoveryAttempts < 3) {
|
|
413
253
|
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
|
}
|
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
|
package/dist/ui/app.js
CHANGED
|
@@ -171,8 +171,10 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
|
|
|
171
171
|
setStreamText('');
|
|
172
172
|
setTools(new Map());
|
|
173
173
|
setTurnTokens({ input: 0, output: 0 });
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
setWaiting(true);
|
|
175
|
+
setReady(false);
|
|
176
|
+
// Pass through to agent loop to clear the actual conversation history
|
|
177
|
+
onSubmit('/clear');
|
|
176
178
|
return;
|
|
177
179
|
case '/retry':
|
|
178
180
|
if (!lastPrompt) {
|
package/dist/ui/terminal.js
CHANGED
|
@@ -154,7 +154,9 @@ export class TerminalUI {
|
|
|
154
154
|
});
|
|
155
155
|
rl.on('close', () => {
|
|
156
156
|
this.stdinEOF = true;
|
|
157
|
-
|
|
157
|
+
// Keep lineQueue intact — buffered lines should still drain before signaling EOF.
|
|
158
|
+
// If there are active waiters, queue is already empty (nextLine checks queue first),
|
|
159
|
+
// so it's safe to resolve them with null now.
|
|
158
160
|
for (const waiter of this.lineWaiters)
|
|
159
161
|
waiter(null);
|
|
160
162
|
this.lineWaiters = [];
|