@bike4mind/cli 2.1.0 → 3.0.0
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 +1 -1
- package/dist/{AgentHistoryStore-DdYKfx7C.mjs → AgentHistoryStore-DVxv1kSO.mjs} +7905 -7029
- package/dist/{ConfigStore-BygzEI6H.mjs → ConfigStore-51UaP9pJ.mjs} +10645 -6760
- package/dist/{ImageStore-kVo-oHoS.mjs → ImageStore-DuI3tgwQ.mjs} +8 -6
- package/dist/SandboxRuntimeAdapter-Bt1wPpld.mjs +2 -0
- package/dist/{WorkItemsClient-ClBC49y0.mjs → WorkItemsClient-BMY0Qq1s.mjs} +27 -2
- package/dist/{buildAgent-B7cMwRAy.mjs → buildAgent-CrSRKYU_.mjs} +75 -43
- package/dist/commands/acpCommand.mjs +26 -35
- package/dist/commands/apiCommand.mjs +1 -1
- package/dist/commands/doctorCommand.mjs +3 -3
- package/dist/commands/envCommand.mjs +1 -1
- package/dist/commands/headlessCommand.mjs +15 -24
- package/dist/commands/mcpCommand.mjs +3 -3
- package/dist/commands/pluginCommand.mjs +2 -2
- package/dist/commands/updateCommand.mjs +5 -15
- package/dist/dist-BsG2Hlp-.mjs +739 -0
- package/dist/index.mjs +602 -324
- package/dist/{package-CRNnNXxq.mjs → package-DEsNSP54.mjs} +1 -1
- package/dist/{serve-DM7ld3-o.mjs → serve-BaHzVbcm.mjs} +300 -154
- package/dist/store-BkMur6uk.mjs +2 -0
- package/dist/{store-CvjTpQPs.mjs → store-BxmPEI3v.mjs} +10 -0
- package/dist/types-I45lMh7Q.mjs +2 -0
- package/dist/{updateChecker-CQW8bxo6.mjs → updateChecker-CCLVVdH0.mjs} +36 -13
- package/package.json +24 -23
- package/dist/ApiClient-dIB3YQc2.mjs +0 -316
- package/dist/SandboxRuntimeAdapter-BmHELLuM.mjs +0 -3
- package/dist/store-BG3e54c8.mjs +0 -3
- package/dist/types-CdIKgWWe.mjs +0 -3
- package/dist/{bashExecute-CrdPpBqk-DCATrE-D.mjs → bashExecute-CrdPpBqk-CgK1gZa2.mjs} +1 -1
- package/dist/{grepSearch-BgoOOwGe-DtlV8Gn-.mjs → grepSearch-BgoOOwGe-B388GxRD.mjs} +2 -2
package/dist/index.mjs
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as
|
|
3
|
-
import { a as selectLiveSubagentTokens, i as selectLiveSubagentCredits, n as selectActiveBackgroundShells, o as useCliStore, r as selectCompletedBackgroundShells, t as selectActiveBackgroundAgents } from "./store-
|
|
2
|
+
import { $ as processFileReferences, A as createAgentDelegateTool, C as createTodoStore, D as createCoordinateTaskTool, E as parseAgentConfig, F as PermissionManager, G as DEFAULT_THOROUGHNESS, H as DEFAULT_AGENT_MODEL, I as buildCliToolList, J as getPlanModeFilePath, M as substituteArguments, O as createResumeAgentTool, P as findIterationBoundary, Q as CustomCommandStore, S as createFindDefinitionTool, T as createSkillTool, U as DEFAULT_MAX_ITERATIONS, V as ALWAYS_DENIED_FOR_AGENTS, W as DEFAULT_RETRY_CONFIG, X as buildSkillsPromptSection, Y as buildSystemPrompt, Z as RemoteSkillSource, _ as createDecisionLogTool, at as rewireReservedNames, b as createGetFileStructureTool, ct as warmFileCache, d as createReviewGateStore, et as hasFileReferences, f as createReviewGateTool, ft as MAX_PASTE_SIZE, g as formatBlockersOutput, h as createBlockerTools, it as mergeCommands, j as FallbackLlmBackend, k as createBackgroundAgentTools, m as createBlockerStore, nt as SessionStore, ot as formatFileSize, p as formatReviewGatesOutput, q as setWebSocketToolExecutor, rt as searchCommands, s as buildSandbox, st as searchFiles, tt as CheckpointStore, u as isConnectionDrop, ut as COMPACTION_SUMMARY_MARKER, v as createDecisionStore, w as createWriteTodosTool, x as createWorkItemTools, y as formatDecisionsOutput, z as getProcessHooks } from "./AgentHistoryStore-DVxv1kSO.mjs";
|
|
3
|
+
import { a as selectLiveSubagentTokens, i as selectLiveSubagentCredits, n as selectActiveBackgroundShells, o as useCliStore, r as selectCompletedBackgroundShells, t as selectActiveBackgroundAgents } from "./store-BxmPEI3v.mjs";
|
|
4
|
+
import { Bt as selfClaimedActorKindSchema, F as ChatModels, Ft as DELEGATION_PAYLOAD_SCHEMA_NAME, It as actorColorIndex, Lt as actorKindMarker, P as CREDIT_DEDUCT_TRANSACTION_TYPES, Pt as ACTOR_COLOR_SLOTS, Rt as actorKindSchema, Z as REVIEW_GATE_STATUS_VALUES, _ as logger, c as getCreditsUrl, d as resolveApiEndpoint, et as SUBQUEST_STATUS_VALUES, g as parseApiUrl, ht as isEarlyStop, kt as tokenEstimateMultiplier, l as getEnvironmentName, m as LOCAL_DEV_URL, n as extractCompactInstructions, o as isReadOnlyTool, s as ApiEndpointUnconfiguredError, t as ConfigStore, u as requireApiUrl, v as SESSION_ID_PATTERN, y as isValidSessionId, zt as delegationPayloadSchema } from "./ConfigStore-51UaP9pJ.mjs";
|
|
4
5
|
import { r as isFeatureEnabled, t as PluginStore } from "./PluginStore-DwvOJ-G3.mjs";
|
|
5
|
-
import {
|
|
6
|
-
import { i as
|
|
7
|
-
import { a as deferredToolRegistry, i as createToolSearchTool, n as buildSupportingStores, o as NotifyingLlmBackend, r as buildLlmBackend, t as buildAgent } from "./buildAgent-B7cMwRAy.mjs";
|
|
6
|
+
import { i as getTokenCounter, n as ConversationContext, r as reconstructTurnBlocks, t as WorkItemsClient } from "./WorkItemsClient-BMY0Qq1s.mjs";
|
|
7
|
+
import { a as deferredToolRegistry, i as createToolSearchTool, n as buildSupportingStores, o as NotifyingLlmBackend, r as buildLlmBackend, t as buildAgent } from "./buildAgent-CrSRKYU_.mjs";
|
|
8
8
|
import { t as getShellSessionManager } from "./ShellSessionManager-6o8KZzl1-vrbPAUTq.mjs";
|
|
9
|
-
import {
|
|
10
|
-
import { t as version } from "./package-
|
|
11
|
-
import {
|
|
9
|
+
import { a as ApiClient, s as OAuthClient } from "./dist-BsG2Hlp-.mjs";
|
|
10
|
+
import { t as version } from "./package-DEsNSP54.mjs";
|
|
11
|
+
import { s as getStartupUpdateStatus } from "./updateChecker-CCLVVdH0.mjs";
|
|
12
12
|
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState, useSyncExternalStore } from "react";
|
|
13
13
|
import { Box, Static, Text, measureElement, render, useApp, useInput, usePaste, useStdout } from "ink";
|
|
14
|
-
import { execFile, execSync, spawn } from "child_process";
|
|
15
14
|
import { randomBytes } from "crypto";
|
|
16
15
|
import { constants, existsSync, promises, readFileSync, statSync } from "fs";
|
|
17
16
|
import { homedir } from "os";
|
|
18
17
|
import path, { basename, extname, join } from "path";
|
|
19
18
|
import { v4 } from "uuid";
|
|
20
19
|
import * as path$1 from "node:path";
|
|
20
|
+
import { z as z$1 } from "zod";
|
|
21
|
+
import axios, { isAxiosError } from "axios";
|
|
21
22
|
import Spinner from "ink-spinner";
|
|
22
23
|
import { useShallow } from "zustand/react/shallow";
|
|
23
24
|
import TextInput from "ink-text-input";
|
|
24
|
-
import {
|
|
25
|
-
import
|
|
25
|
+
import { execFile, spawn } from "child_process";
|
|
26
|
+
import { promisify } from "util";
|
|
26
27
|
import { marked } from "marked";
|
|
27
28
|
import { highlight } from "cli-highlight";
|
|
28
|
-
import { promisify } from "util";
|
|
29
29
|
import SelectInput from "ink-select-input";
|
|
30
30
|
import jwt from "jsonwebtoken";
|
|
31
31
|
import open from "open";
|
|
@@ -121,52 +121,54 @@ function CustomTextInput({ value, onChange, onSubmit, onPaste, pasteIndicator, p
|
|
|
121
121
|
setCursorOffset(cursorOffset + 1);
|
|
122
122
|
return;
|
|
123
123
|
}
|
|
124
|
-
if (key.meta && !key.ctrl)
|
|
125
|
-
if (key.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
124
|
+
if (key.meta && !key.ctrl) {
|
|
125
|
+
if (key.shift) {
|
|
126
|
+
if (key.leftArrow) {
|
|
127
|
+
const newPos = findPreviousWordBoundary(value, cursorOffset);
|
|
128
|
+
setCursorOffset(newPos);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (key.rightArrow) {
|
|
132
|
+
const newPos = findNextWordBoundary(value, cursorOffset);
|
|
133
|
+
setCursorOffset(newPos);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (key.backspace) {
|
|
137
|
+
const beforeCursor = value.slice(0, cursorOffset);
|
|
138
|
+
const afterCursor = value.slice(cursorOffset);
|
|
139
|
+
const newPos = findPreviousWordBoundary(beforeCursor, beforeCursor.length);
|
|
140
|
+
const newValue = beforeCursor.slice(0, newPos) + afterCursor;
|
|
141
|
+
emitChange(newValue);
|
|
142
|
+
setCursorOffset(newPos);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (key.delete) {
|
|
146
|
+
const beforeCursor = value.slice(0, cursorOffset);
|
|
147
|
+
const afterCursor = value.slice(cursorOffset);
|
|
148
|
+
const newPos = findNextWordBoundary(afterCursor, 0);
|
|
149
|
+
const newValue = beforeCursor + afterCursor.slice(newPos);
|
|
150
|
+
emitChange(newValue);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
if (key.leftArrow) {
|
|
155
|
+
setCursorOffset(0);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (key.rightArrow) {
|
|
159
|
+
setCursorOffset(value.length);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (key.backspace) {
|
|
163
|
+
const afterCursor = value.slice(cursorOffset);
|
|
164
|
+
emitChange(afterCursor);
|
|
165
|
+
setCursorOffset(0);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (key.delete) {
|
|
169
|
+
emitChange(value.slice(0, cursorOffset));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
170
172
|
}
|
|
171
173
|
}
|
|
172
174
|
if (key.home) {
|
|
@@ -2107,6 +2109,138 @@ function McpViewer({ config, mcpManager, onClose }) {
|
|
|
2107
2109
|
}, "Press Esc or q to close")));
|
|
2108
2110
|
}
|
|
2109
2111
|
//#endregion
|
|
2112
|
+
//#region src/storage/CommandHistoryStore.ts
|
|
2113
|
+
/**
|
|
2114
|
+
* Maximum number of history entries to keep
|
|
2115
|
+
*/
|
|
2116
|
+
const MAX_HISTORY_ENTRIES = 1e3;
|
|
2117
|
+
/**
|
|
2118
|
+
* Manages command history stored in JSONL format
|
|
2119
|
+
* Uses append-friendly JSONL for efficient writes
|
|
2120
|
+
*/
|
|
2121
|
+
var CommandHistoryStore = class {
|
|
2122
|
+
constructor(historyPath) {
|
|
2123
|
+
this.history = null;
|
|
2124
|
+
this.historyPath = historyPath || path.join(homedir(), ".bike4mind", "history.jsonl");
|
|
2125
|
+
}
|
|
2126
|
+
/**
|
|
2127
|
+
* Initialize history directory
|
|
2128
|
+
*/
|
|
2129
|
+
async init() {
|
|
2130
|
+
const dir = path.dirname(this.historyPath);
|
|
2131
|
+
try {
|
|
2132
|
+
await promises.mkdir(dir, { recursive: true });
|
|
2133
|
+
} catch (error) {
|
|
2134
|
+
console.error("Failed to initialize history directory:", error);
|
|
2135
|
+
throw error;
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
/**
|
|
2139
|
+
* Load command history from disk
|
|
2140
|
+
* Returns array of commands (newest first)
|
|
2141
|
+
*/
|
|
2142
|
+
async load() {
|
|
2143
|
+
if (this.history) return this.history;
|
|
2144
|
+
try {
|
|
2145
|
+
const entries = (await promises.readFile(this.historyPath, "utf-8")).trim().split("\n").filter((line) => line.length > 0).map((line) => {
|
|
2146
|
+
try {
|
|
2147
|
+
return JSON.parse(line);
|
|
2148
|
+
} catch {
|
|
2149
|
+
return null;
|
|
2150
|
+
}
|
|
2151
|
+
}).filter((entry) => entry !== null);
|
|
2152
|
+
this.history = entries.sort((a, b) => b.timestamp - a.timestamp).map((entry) => entry.command);
|
|
2153
|
+
return this.history;
|
|
2154
|
+
} catch (error) {
|
|
2155
|
+
if (error.code === "ENOENT") {
|
|
2156
|
+
this.history = [];
|
|
2157
|
+
return this.history;
|
|
2158
|
+
}
|
|
2159
|
+
throw error;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
/**
|
|
2163
|
+
* Add a command to history
|
|
2164
|
+
* Skips if it's the same as the most recent command (deduplicate)
|
|
2165
|
+
*/
|
|
2166
|
+
async add(command) {
|
|
2167
|
+
await this.load();
|
|
2168
|
+
if (!command || !command.trim()) return;
|
|
2169
|
+
if (this.history && this.history.length > 0 && this.history[0] === command) return;
|
|
2170
|
+
const entry = {
|
|
2171
|
+
command,
|
|
2172
|
+
timestamp: Date.now()
|
|
2173
|
+
};
|
|
2174
|
+
await this.init();
|
|
2175
|
+
const line = JSON.stringify(entry) + "\n";
|
|
2176
|
+
await promises.appendFile(this.historyPath, line, "utf-8");
|
|
2177
|
+
this.history = [command, ...this.history || []];
|
|
2178
|
+
if (this.history.length > MAX_HISTORY_ENTRIES) await this.trim();
|
|
2179
|
+
}
|
|
2180
|
+
/**
|
|
2181
|
+
* Trim history to max entries
|
|
2182
|
+
* Rewrites the entire file with the most recent entries
|
|
2183
|
+
*/
|
|
2184
|
+
async trim() {
|
|
2185
|
+
if (!this.history || this.history.length <= MAX_HISTORY_ENTRIES) return;
|
|
2186
|
+
const trimmedCommands = this.history.slice(0, MAX_HISTORY_ENTRIES);
|
|
2187
|
+
const lines = trimmedCommands.reverse().map((command) => {
|
|
2188
|
+
return JSON.stringify({
|
|
2189
|
+
command,
|
|
2190
|
+
timestamp: Date.now()
|
|
2191
|
+
});
|
|
2192
|
+
}).join("\n");
|
|
2193
|
+
await promises.writeFile(this.historyPath, lines + "\n", "utf-8");
|
|
2194
|
+
this.history = trimmedCommands;
|
|
2195
|
+
}
|
|
2196
|
+
/**
|
|
2197
|
+
* Clear all command history
|
|
2198
|
+
*/
|
|
2199
|
+
async clear() {
|
|
2200
|
+
try {
|
|
2201
|
+
await promises.unlink(this.historyPath);
|
|
2202
|
+
} catch (error) {
|
|
2203
|
+
if (error.code !== "ENOENT") throw error;
|
|
2204
|
+
}
|
|
2205
|
+
this.history = [];
|
|
2206
|
+
}
|
|
2207
|
+
/**
|
|
2208
|
+
* Get all commands (newest first)
|
|
2209
|
+
*/
|
|
2210
|
+
async list() {
|
|
2211
|
+
return this.load();
|
|
2212
|
+
}
|
|
2213
|
+
};
|
|
2214
|
+
//#endregion
|
|
2215
|
+
//#region src/storage/buildGlobalConfigPatch.ts
|
|
2216
|
+
/**
|
|
2217
|
+
* Compute the minimal global-config patch for a /config editor save: only the
|
|
2218
|
+
* editor-owned fields (defaultModel, preferences, features) that the user
|
|
2219
|
+
* actually changed vs the current effective (merged) config. Unchanged fields
|
|
2220
|
+
* are omitted, so a repo-injected value the user never touched is never
|
|
2221
|
+
* laundered into the global layer by a save() that spreads the merged config.
|
|
2222
|
+
*
|
|
2223
|
+
* defaultModel and preferences diff per key. features stays whole-object when
|
|
2224
|
+
* anything changed, on purpose: ConfigStore.save() feeds config.features to
|
|
2225
|
+
* mergeFeatures as the caller's COMPLETE desired set (absence of a key means
|
|
2226
|
+
* "removed"), so a per-key diff would make save() delete untouched keys. That
|
|
2227
|
+
* whole-object features path is only latently launderable today (no repo layer
|
|
2228
|
+
* populates features); tightening it needs a coordinated change to save()'s
|
|
2229
|
+
* concurrent-writer merge and is tracked separately.
|
|
2230
|
+
*/
|
|
2231
|
+
function buildGlobalConfigPatch(prev, next) {
|
|
2232
|
+
const patch = {};
|
|
2233
|
+
if (next.defaultModel !== prev?.defaultModel) patch.defaultModel = next.defaultModel;
|
|
2234
|
+
const changedPreferences = {};
|
|
2235
|
+
for (const [key, value] of Object.entries(next.preferences)) {
|
|
2236
|
+
const prevValue = (prev?.preferences)?.[key];
|
|
2237
|
+
if (JSON.stringify(value) !== JSON.stringify(prevValue)) changedPreferences[key] = value;
|
|
2238
|
+
}
|
|
2239
|
+
if (Object.keys(changedPreferences).length > 0) patch.preferences = changedPreferences;
|
|
2240
|
+
if (JSON.stringify(next.features ?? {}) !== JSON.stringify(prev?.features ?? {})) patch.features = next.features;
|
|
2241
|
+
return patch;
|
|
2242
|
+
}
|
|
2243
|
+
//#endregion
|
|
2110
2244
|
//#region src/components/MarkdownRenderer.tsx
|
|
2111
2245
|
function MarkdownRenderer({ content, columns: columnsProp }) {
|
|
2112
2246
|
const columns = columnsProp ?? process.stdout.columns ?? 80;
|
|
@@ -2342,6 +2476,12 @@ function stepRows(steps, index, showThoughts) {
|
|
|
2342
2476
|
if (step.type !== "action") return 0;
|
|
2343
2477
|
return steps[index + 1]?.type === "observation" ? 3 : 2;
|
|
2344
2478
|
}
|
|
2479
|
+
/** Rows a whole window occupies, excluding the trace Box chrome. */
|
|
2480
|
+
function measureTraceRows(steps, { showThoughts = true } = {}) {
|
|
2481
|
+
let rows = 0;
|
|
2482
|
+
for (let i = 0; i < steps.length; i++) rows += stepRows(steps, i, showThoughts);
|
|
2483
|
+
return rows;
|
|
2484
|
+
}
|
|
2345
2485
|
const countRenderable = (steps, showThoughts) => steps.filter((step) => isRenderable(step, showThoughts)).length;
|
|
2346
2486
|
const EMPTY_WINDOW = {
|
|
2347
2487
|
steps: [],
|
|
@@ -2382,32 +2522,96 @@ function windowLiveTrace(steps, { rows, showThoughts = true }) {
|
|
|
2382
2522
|
hiddenSteps: countRenderable(steps.slice(0, newest), showThoughts)
|
|
2383
2523
|
};
|
|
2384
2524
|
}
|
|
2525
|
+
/** Rows the live reply Box spends beyond its text: its bottom margin. */
|
|
2526
|
+
const REPLY_BOX_CHROME_ROWS = 1;
|
|
2527
|
+
/**
|
|
2528
|
+
* Hard-wraps streamed reply text into rows of at most `columns` characters.
|
|
2529
|
+
* MessageItem renders each row as its own `truncate-end` line, so a row is
|
|
2530
|
+
* exactly one terminal row; a wide character can only clip a row's end in the
|
|
2531
|
+
* live view, never add a row. The full markdown render lands via <Static>.
|
|
2532
|
+
*/
|
|
2533
|
+
function wrapReplyRows(text, columns) {
|
|
2534
|
+
const width = Math.max(1, Math.floor(columns));
|
|
2535
|
+
const rows = [];
|
|
2536
|
+
for (const line of text.split("\n")) {
|
|
2537
|
+
if (line.length === 0) {
|
|
2538
|
+
rows.push("");
|
|
2539
|
+
continue;
|
|
2540
|
+
}
|
|
2541
|
+
for (let start = 0; start < line.length; start += width) rows.push(line.slice(start, start + width));
|
|
2542
|
+
}
|
|
2543
|
+
return rows;
|
|
2544
|
+
}
|
|
2545
|
+
/**
|
|
2546
|
+
* Splits the live frame's row budget between the step trace and the streaming
|
|
2547
|
+
* reply. The reply gets what the trace leaves over, and never less than half
|
|
2548
|
+
* the budget once it needs it, so a long trace cannot starve the answer the
|
|
2549
|
+
* user is waiting on. The reply shows its tail: the newest text is what moves.
|
|
2550
|
+
*/
|
|
2551
|
+
function splitLiveRows(rows, { steps, replyRows, showThoughts = true }) {
|
|
2552
|
+
const budget = Math.max(0, Math.floor(rows));
|
|
2553
|
+
if (replyRows.length === 0) return {
|
|
2554
|
+
traceRows: budget,
|
|
2555
|
+
replyRows: []
|
|
2556
|
+
};
|
|
2557
|
+
const traceNeeded = steps.length > 0 ? measureTraceRows(steps, { showThoughts }) + TRACE_BOX_CHROME_ROWS : 0;
|
|
2558
|
+
const replyNeeded = replyRows.length + REPLY_BOX_CHROME_ROWS;
|
|
2559
|
+
const replyBudget = Math.min(replyNeeded, Math.max(budget - traceNeeded, Math.ceil(budget / 2)));
|
|
2560
|
+
const visibleReplyRows = replyBudget - REPLY_BOX_CHROME_ROWS;
|
|
2561
|
+
if (visibleReplyRows <= 0) return {
|
|
2562
|
+
traceRows: budget,
|
|
2563
|
+
replyRows: []
|
|
2564
|
+
};
|
|
2565
|
+
return {
|
|
2566
|
+
traceRows: budget - replyBudget,
|
|
2567
|
+
replyRows: replyRows.slice(-visibleReplyRows)
|
|
2568
|
+
};
|
|
2569
|
+
}
|
|
2385
2570
|
//#endregion
|
|
2386
2571
|
//#region src/components/MessageItem.tsx
|
|
2387
2572
|
const TOOLS_WITH_HIDDEN_ARGS = /* @__PURE__ */ new Set(["edit_local_file"]);
|
|
2388
|
-
|
|
2573
|
+
/** Left padding of the reply body, in columns. */
|
|
2574
|
+
const REPLY_INDENT = 2;
|
|
2575
|
+
const MessageItem = React.memo(function MessageItem({ message, showThoughts = true, liveRows }) {
|
|
2389
2576
|
const isUser = message.role === "user";
|
|
2390
2577
|
const [terminalCols] = useStdoutDimensions();
|
|
2391
|
-
const isLive =
|
|
2578
|
+
const isLive = liveRows !== void 0;
|
|
2392
2579
|
const traceWrap = isLive ? "truncate-end" : "wrap";
|
|
2393
2580
|
const oneLine = (text) => isLive ? text.replace(/\s+/g, " ") : text;
|
|
2581
|
+
const hasReply = !isUser && message.content !== "...";
|
|
2394
2582
|
const allSteps = message.metadata?.steps;
|
|
2583
|
+
const liveSplit = React.useMemo(() => {
|
|
2584
|
+
if (liveRows === void 0) return void 0;
|
|
2585
|
+
const replyRows = hasReply ? wrapReplyRows(message.content, terminalCols - REPLY_INDENT) : [];
|
|
2586
|
+
return splitLiveRows(liveRows, {
|
|
2587
|
+
steps: allSteps ?? [],
|
|
2588
|
+
replyRows,
|
|
2589
|
+
showThoughts
|
|
2590
|
+
});
|
|
2591
|
+
}, [
|
|
2592
|
+
liveRows,
|
|
2593
|
+
hasReply,
|
|
2594
|
+
message.content,
|
|
2595
|
+
terminalCols,
|
|
2596
|
+
allSteps,
|
|
2597
|
+
showThoughts
|
|
2598
|
+
]);
|
|
2395
2599
|
const { steps: traceSteps, hiddenSteps } = React.useMemo(() => {
|
|
2396
2600
|
if (!allSteps) return {
|
|
2397
2601
|
steps: [],
|
|
2398
2602
|
hiddenSteps: 0
|
|
2399
2603
|
};
|
|
2400
|
-
if (
|
|
2604
|
+
if (liveSplit === void 0) return {
|
|
2401
2605
|
steps: allSteps,
|
|
2402
2606
|
hiddenSteps: 0
|
|
2403
2607
|
};
|
|
2404
2608
|
return windowLiveTrace(allSteps, {
|
|
2405
|
-
rows:
|
|
2609
|
+
rows: liveSplit.traceRows,
|
|
2406
2610
|
showThoughts
|
|
2407
2611
|
});
|
|
2408
2612
|
}, [
|
|
2409
2613
|
allSteps,
|
|
2410
|
-
|
|
2614
|
+
liveSplit,
|
|
2411
2615
|
showThoughts
|
|
2412
2616
|
]);
|
|
2413
2617
|
const visibleTraceSteps = traceSteps.filter((s) => showThoughts && s.type === "thought" || s.type === "action").length;
|
|
@@ -2452,13 +2656,20 @@ const MessageItem = React.memo(function MessageItem({ message, showThoughts = tr
|
|
|
2452
2656
|
}, `Result: ${oneLine(truncateValue(result, 200))}`)));
|
|
2453
2657
|
}
|
|
2454
2658
|
return null;
|
|
2455
|
-
}).filter(Boolean)),
|
|
2456
|
-
paddingLeft:
|
|
2659
|
+
}).filter(Boolean)), liveSplit && liveSplit.replyRows.length > 0 && /* @__PURE__ */ React.createElement(Box, {
|
|
2660
|
+
paddingLeft: REPLY_INDENT,
|
|
2661
|
+
marginBottom: 1,
|
|
2662
|
+
flexDirection: "column"
|
|
2663
|
+
}, liveSplit.replyRows.map((row, idx) => /* @__PURE__ */ React.createElement(Text, {
|
|
2664
|
+
key: idx,
|
|
2665
|
+
wrap: "truncate-end"
|
|
2666
|
+
}, row || " "))), !isLive && hasReply && /* @__PURE__ */ React.createElement(Box, {
|
|
2667
|
+
paddingLeft: REPLY_INDENT,
|
|
2457
2668
|
marginBottom: 1
|
|
2458
2669
|
}, message.metadata?.permissionDenied ? /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "⚠️ ", message.content) : /* @__PURE__ */ React.createElement(MarkdownRenderer, {
|
|
2459
2670
|
content: message.content,
|
|
2460
2671
|
columns: terminalCols
|
|
2461
|
-
})), !isUser && message.metadata?.earlyStopReason && /* @__PURE__ */ React.createElement(Box, { paddingLeft: 2 }, /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, message.metadata.earlyStopReason === "degenerate_repetition" ? "⚠ Response stopped early: it began repeating itself. Try rephrasing the request." : "⚠ Response was cut off at the output-token limit. Type \"continue\" to resume, or raise Max Tokens in /config.")), !
|
|
2672
|
+
})), !isUser && message.metadata?.earlyStopReason && /* @__PURE__ */ React.createElement(Box, { paddingLeft: 2 }, /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, message.metadata.earlyStopReason === "degenerate_repetition" ? "⚠ Response stopped early: it began repeating itself. Try rephrasing the request." : "⚠ Response was cut off at the output-token limit. Type \"continue\" to resume, or raise Max Tokens in /config.")), !isLive && hasReply && message.metadata && /* @__PURE__ */ React.createElement(Box, {
|
|
2462
2673
|
paddingLeft: 2,
|
|
2463
2674
|
marginBottom: 1
|
|
2464
2675
|
}, (message.metadata.tokenUsage?.total || message.metadata.creditsUsed) && /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "(", message.metadata.tokenUsage?.total ? `${message.metadata.tokenUsage.total.toLocaleString()} tokens` : "", message.metadata.creditsUsed && message.metadata.creditsUsed > 0 ? (message.metadata.tokenUsage?.total ? " • " : "") + `used ${message.metadata.creditsUsed.toLocaleString()} ${message.metadata.creditsUsed === 1 ? "credit" : "credits"}` : "", ")")));
|
|
@@ -2542,7 +2753,7 @@ function App({ onMessage, onBackgroundCompletion, onCommand, onBashCommand, onPe
|
|
|
2542
2753
|
useInput(() => setPromptInputTick((tick) => tick + 1), { isActive: promptKey !== "" });
|
|
2543
2754
|
const promptRows = promptKey === "" ? 0 : measuredPrompt?.key === promptKey ? measuredPrompt.rows : Infinity;
|
|
2544
2755
|
const [terminalCols, terminalRows] = useStdoutDimensions();
|
|
2545
|
-
const
|
|
2756
|
+
const liveRows = Math.max(0, terminalRows - LIVE_CHROME_ROWS - messageQueue.length - promptRows);
|
|
2546
2757
|
const handleSubmit = React.useCallback(async (input) => {
|
|
2547
2758
|
const trimmed = input.trim();
|
|
2548
2759
|
if (!trimmed) return;
|
|
@@ -2610,7 +2821,7 @@ function App({ onMessage, onBackgroundCompletion, onCommand, onBashCommand, onPe
|
|
|
2610
2821
|
}, /* @__PURE__ */ React.createElement(MessageItem, {
|
|
2611
2822
|
message,
|
|
2612
2823
|
showThoughts,
|
|
2613
|
-
|
|
2824
|
+
liveRows
|
|
2614
2825
|
})))), /* @__PURE__ */ React.createElement(Box, {
|
|
2615
2826
|
ref: promptRef,
|
|
2616
2827
|
flexDirection: "column"
|
|
@@ -2813,10 +3024,12 @@ function RewindSelector({ messages, onSelect, onCancel }) {
|
|
|
2813
3024
|
const [step, setStep] = useState("selection");
|
|
2814
3025
|
const [selectedMessageIndex, setSelectedMessageIndex] = useState(null);
|
|
2815
3026
|
useInput((input, key) => {
|
|
2816
|
-
if (key.escape)
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
3027
|
+
if (key.escape) {
|
|
3028
|
+
if (step === "confirmation") {
|
|
3029
|
+
setStep("selection");
|
|
3030
|
+
setSelectedMessageIndex(null);
|
|
3031
|
+
} else onCancel();
|
|
3032
|
+
}
|
|
2820
3033
|
});
|
|
2821
3034
|
const items = messages.map((msg, idx) => ({
|
|
2822
3035
|
msg,
|
|
@@ -2879,10 +3092,12 @@ function SessionSelector({ sessions, currentSession, onSelect, onCancel }) {
|
|
|
2879
3092
|
const [step, setStep] = useState("selection");
|
|
2880
3093
|
const [selectedSession, setSelectedSession] = useState(null);
|
|
2881
3094
|
useInput((_input, key) => {
|
|
2882
|
-
if (key.escape)
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
3095
|
+
if (key.escape) {
|
|
3096
|
+
if (step === "confirmation") {
|
|
3097
|
+
setStep("selection");
|
|
3098
|
+
setSelectedSession(null);
|
|
3099
|
+
} else onCancel();
|
|
3100
|
+
}
|
|
2886
3101
|
});
|
|
2887
3102
|
const hasUnsavedWork = currentSession && currentSession.messages.length > 0;
|
|
2888
3103
|
const formatTimeAgo = (timestamp) => {
|
|
@@ -3206,138 +3421,6 @@ function LoginFlow({ apiUrl = "http://localhost:3000", configStore, onSuccess, o
|
|
|
3206
3421
|
}, deviceFlow?.user_code)), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "cyan" }, /* @__PURE__ */ React.createElement(Spinner, { type: "dots" }), " ", statusMessage)), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Expires in ", deviceFlow ? Math.floor(deviceFlow.expires_in / 60) : 0, " minutes")));
|
|
3207
3422
|
}
|
|
3208
3423
|
//#endregion
|
|
3209
|
-
//#region src/storage/CommandHistoryStore.ts
|
|
3210
|
-
/**
|
|
3211
|
-
* Maximum number of history entries to keep
|
|
3212
|
-
*/
|
|
3213
|
-
const MAX_HISTORY_ENTRIES = 1e3;
|
|
3214
|
-
/**
|
|
3215
|
-
* Manages command history stored in JSONL format
|
|
3216
|
-
* Uses append-friendly JSONL for efficient writes
|
|
3217
|
-
*/
|
|
3218
|
-
var CommandHistoryStore = class {
|
|
3219
|
-
constructor(historyPath) {
|
|
3220
|
-
this.history = null;
|
|
3221
|
-
this.historyPath = historyPath || path.join(homedir(), ".bike4mind", "history.jsonl");
|
|
3222
|
-
}
|
|
3223
|
-
/**
|
|
3224
|
-
* Initialize history directory
|
|
3225
|
-
*/
|
|
3226
|
-
async init() {
|
|
3227
|
-
const dir = path.dirname(this.historyPath);
|
|
3228
|
-
try {
|
|
3229
|
-
await promises.mkdir(dir, { recursive: true });
|
|
3230
|
-
} catch (error) {
|
|
3231
|
-
console.error("Failed to initialize history directory:", error);
|
|
3232
|
-
throw error;
|
|
3233
|
-
}
|
|
3234
|
-
}
|
|
3235
|
-
/**
|
|
3236
|
-
* Load command history from disk
|
|
3237
|
-
* Returns array of commands (newest first)
|
|
3238
|
-
*/
|
|
3239
|
-
async load() {
|
|
3240
|
-
if (this.history) return this.history;
|
|
3241
|
-
try {
|
|
3242
|
-
const entries = (await promises.readFile(this.historyPath, "utf-8")).trim().split("\n").filter((line) => line.length > 0).map((line) => {
|
|
3243
|
-
try {
|
|
3244
|
-
return JSON.parse(line);
|
|
3245
|
-
} catch {
|
|
3246
|
-
return null;
|
|
3247
|
-
}
|
|
3248
|
-
}).filter((entry) => entry !== null);
|
|
3249
|
-
this.history = entries.sort((a, b) => b.timestamp - a.timestamp).map((entry) => entry.command);
|
|
3250
|
-
return this.history;
|
|
3251
|
-
} catch (error) {
|
|
3252
|
-
if (error.code === "ENOENT") {
|
|
3253
|
-
this.history = [];
|
|
3254
|
-
return this.history;
|
|
3255
|
-
}
|
|
3256
|
-
throw error;
|
|
3257
|
-
}
|
|
3258
|
-
}
|
|
3259
|
-
/**
|
|
3260
|
-
* Add a command to history
|
|
3261
|
-
* Skips if it's the same as the most recent command (deduplicate)
|
|
3262
|
-
*/
|
|
3263
|
-
async add(command) {
|
|
3264
|
-
await this.load();
|
|
3265
|
-
if (!command || !command.trim()) return;
|
|
3266
|
-
if (this.history && this.history.length > 0 && this.history[0] === command) return;
|
|
3267
|
-
const entry = {
|
|
3268
|
-
command,
|
|
3269
|
-
timestamp: Date.now()
|
|
3270
|
-
};
|
|
3271
|
-
await this.init();
|
|
3272
|
-
const line = JSON.stringify(entry) + "\n";
|
|
3273
|
-
await promises.appendFile(this.historyPath, line, "utf-8");
|
|
3274
|
-
this.history = [command, ...this.history || []];
|
|
3275
|
-
if (this.history.length > MAX_HISTORY_ENTRIES) await this.trim();
|
|
3276
|
-
}
|
|
3277
|
-
/**
|
|
3278
|
-
* Trim history to max entries
|
|
3279
|
-
* Rewrites the entire file with the most recent entries
|
|
3280
|
-
*/
|
|
3281
|
-
async trim() {
|
|
3282
|
-
if (!this.history || this.history.length <= MAX_HISTORY_ENTRIES) return;
|
|
3283
|
-
const trimmedCommands = this.history.slice(0, MAX_HISTORY_ENTRIES);
|
|
3284
|
-
const lines = trimmedCommands.reverse().map((command) => {
|
|
3285
|
-
return JSON.stringify({
|
|
3286
|
-
command,
|
|
3287
|
-
timestamp: Date.now()
|
|
3288
|
-
});
|
|
3289
|
-
}).join("\n");
|
|
3290
|
-
await promises.writeFile(this.historyPath, lines + "\n", "utf-8");
|
|
3291
|
-
this.history = trimmedCommands;
|
|
3292
|
-
}
|
|
3293
|
-
/**
|
|
3294
|
-
* Clear all command history
|
|
3295
|
-
*/
|
|
3296
|
-
async clear() {
|
|
3297
|
-
try {
|
|
3298
|
-
await promises.unlink(this.historyPath);
|
|
3299
|
-
} catch (error) {
|
|
3300
|
-
if (error.code !== "ENOENT") throw error;
|
|
3301
|
-
}
|
|
3302
|
-
this.history = [];
|
|
3303
|
-
}
|
|
3304
|
-
/**
|
|
3305
|
-
* Get all commands (newest first)
|
|
3306
|
-
*/
|
|
3307
|
-
async list() {
|
|
3308
|
-
return this.load();
|
|
3309
|
-
}
|
|
3310
|
-
};
|
|
3311
|
-
//#endregion
|
|
3312
|
-
//#region src/storage/buildGlobalConfigPatch.ts
|
|
3313
|
-
/**
|
|
3314
|
-
* Compute the minimal global-config patch for a /config editor save: only the
|
|
3315
|
-
* editor-owned fields (defaultModel, preferences, features) that the user
|
|
3316
|
-
* actually changed vs the current effective (merged) config. Unchanged fields
|
|
3317
|
-
* are omitted, so a repo-injected value the user never touched is never
|
|
3318
|
-
* laundered into the global layer by a save() that spreads the merged config.
|
|
3319
|
-
*
|
|
3320
|
-
* defaultModel and preferences diff per key. features stays whole-object when
|
|
3321
|
-
* anything changed, on purpose: ConfigStore.save() feeds config.features to
|
|
3322
|
-
* mergeFeatures as the caller's COMPLETE desired set (absence of a key means
|
|
3323
|
-
* "removed"), so a per-key diff would make save() delete untouched keys. That
|
|
3324
|
-
* whole-object features path is only latently launderable today (no repo layer
|
|
3325
|
-
* populates features); tightening it needs a coordinated change to save()'s
|
|
3326
|
-
* concurrent-writer merge and is tracked separately.
|
|
3327
|
-
*/
|
|
3328
|
-
function buildGlobalConfigPatch(prev, next) {
|
|
3329
|
-
const patch = {};
|
|
3330
|
-
if (next.defaultModel !== prev?.defaultModel) patch.defaultModel = next.defaultModel;
|
|
3331
|
-
const changedPreferences = {};
|
|
3332
|
-
for (const [key, value] of Object.entries(next.preferences)) {
|
|
3333
|
-
const prevValue = (prev?.preferences)?.[key];
|
|
3334
|
-
if (JSON.stringify(value) !== JSON.stringify(prevValue)) changedPreferences[key] = value;
|
|
3335
|
-
}
|
|
3336
|
-
if (Object.keys(changedPreferences).length > 0) patch.preferences = changedPreferences;
|
|
3337
|
-
if (JSON.stringify(next.features ?? {}) !== JSON.stringify(prev?.features ?? {})) patch.features = next.features;
|
|
3338
|
-
return patch;
|
|
3339
|
-
}
|
|
3340
|
-
//#endregion
|
|
3341
3424
|
//#region src/utils/formatStep.ts
|
|
3342
3425
|
const MAX_INPUT_LENGTH = 500;
|
|
3343
3426
|
/**
|
|
@@ -4054,7 +4137,7 @@ async function writeLocalHandoffFile(session, options = {}) {
|
|
|
4054
4137
|
function isLlmUnavailableError(error) {
|
|
4055
4138
|
if (!(error instanceof Error)) return false;
|
|
4056
4139
|
const message = error.message;
|
|
4057
|
-
return message.includes("Rate limit exceeded") || message.includes("Authentication expired") || message.includes("Authentication failed") || message.includes("Cannot connect to Bike4Mind server") || message.includes("ECONNREFUSED") || message.includes("ETIMEDOUT") || message.includes("ENOTFOUND") || message.includes("ECONNRESET") || message.includes("Failed to complete LLM request") || /Request failed with status 5\d\d/.test(message) || /^5\d\d\b/.test(message);
|
|
4140
|
+
return message.includes("Rate limit exceeded") || message.includes("Authentication expired") || message.includes("Authentication failed") || message.includes("Cannot connect to Bike4Mind server") || message.includes("ECONNREFUSED") || message.includes("ETIMEDOUT") || message.includes("ENOTFOUND") || message.includes("ECONNRESET") || message.includes("Failed to complete LLM request") || /Request failed with status 5\d\d/.test(message) || /^5\d\d\b/.test(message) || /\(upstream 5\d\d\)/.test(message);
|
|
4058
4141
|
}
|
|
4059
4142
|
//#endregion
|
|
4060
4143
|
//#region src/utils/compaction.ts
|
|
@@ -4187,18 +4270,14 @@ function flattenContent(content) {
|
|
|
4187
4270
|
* @param session - The session this turn belongs to. Only used for
|
|
4188
4271
|
* `createCompactedSession`'s summary-message formatting (and workflow
|
|
4189
4272
|
* handoff parity); its `messages` are not read or mutated.
|
|
4190
|
-
* @param initialMessageCount - Length of the protected prefix (system +
|
|
4191
|
-
* previousMessages + current query) passed into the matching `agent.run()`
|
|
4192
|
-
* call. Must match exactly - it is the boundary below which messages are
|
|
4193
|
-
* never touched, so the turn's own query is never summarized away.
|
|
4194
4273
|
*
|
|
4195
4274
|
* Workflow state (decisions/blockers) needs no flush here: this handler only
|
|
4196
4275
|
* rewrites the in-flight `IMessage[]` working history and discards the
|
|
4197
4276
|
* compacted session's metadata, leaving the stores and `session.metadata`
|
|
4198
4277
|
* untouched. The turn's own post-run save syncs the stores afterwards.
|
|
4199
4278
|
*/
|
|
4200
|
-
function createReactiveCompactionHandler(agent, session
|
|
4201
|
-
return async (messages) => {
|
|
4279
|
+
function createReactiveCompactionHandler(agent, session) {
|
|
4280
|
+
return async (messages, initialMessageCount) => {
|
|
4202
4281
|
const cutIndex = findIterationBoundary(messages, PRESERVE_RECENT_ITERATIONS, initialMessageCount);
|
|
4203
4282
|
const toSummarize = messages.slice(initialMessageCount, cutIndex);
|
|
4204
4283
|
if (toSummarize.length === 0) return null;
|
|
@@ -4419,13 +4498,147 @@ var MessageBuilder = class {
|
|
|
4419
4498
|
if (typeof message.content === "string") return message.content;
|
|
4420
4499
|
return message.content.map((item) => {
|
|
4421
4500
|
if (item.type === "text") return item.text;
|
|
4422
|
-
else if (item.type === "image")
|
|
4423
|
-
|
|
4501
|
+
else if (item.type === "image") {
|
|
4502
|
+
if (item.source.type === "url") return `[Image: ${item.source.url}]`;
|
|
4503
|
+
else return `[Image: base64 ${item.source.media_type}]`;
|
|
4504
|
+
}
|
|
4424
4505
|
return "";
|
|
4425
4506
|
}).join("");
|
|
4426
4507
|
}
|
|
4427
4508
|
};
|
|
4428
4509
|
//#endregion
|
|
4510
|
+
//#region src/utils/runBangCommand.ts
|
|
4511
|
+
/** How long a `!command` may run before it is killed (ms). */
|
|
4512
|
+
const BANG_COMMAND_TIMEOUT_MS = 3e5;
|
|
4513
|
+
const BANG_COMMAND_MAX_OUTPUT_CHARS = 10485760;
|
|
4514
|
+
const USE_PROCESS_GROUP = process.platform !== "win32";
|
|
4515
|
+
const runningCommands = /* @__PURE__ */ new Map();
|
|
4516
|
+
/** Signal the command's whole process tree, so `make && ./server` does not outlive a kill. */
|
|
4517
|
+
function killCommand(child) {
|
|
4518
|
+
try {
|
|
4519
|
+
if (USE_PROCESS_GROUP && child.pid !== void 0) process.kill(-child.pid, "SIGTERM");
|
|
4520
|
+
else child.kill("SIGTERM");
|
|
4521
|
+
} catch {}
|
|
4522
|
+
}
|
|
4523
|
+
/** Kills every `!command` still running; called on CLI exit so none is orphaned. Their results resolve `null`. */
|
|
4524
|
+
function killRunningBangCommands() {
|
|
4525
|
+
for (const stop of runningCommands.values()) stop("exit");
|
|
4526
|
+
runningCommands.clear();
|
|
4527
|
+
}
|
|
4528
|
+
/**
|
|
4529
|
+
* Runs a `!command` typed in the TUI without blocking the event loop, through the platform shell
|
|
4530
|
+
* like the execSync it replaces. Never rejects: every failure comes back as `isError` output, except
|
|
4531
|
+
* a command killed because the CLI is exiting, which resolves `null` (nothing should be recorded).
|
|
4532
|
+
*/
|
|
4533
|
+
function runBangCommand(command, { cwd, timeoutMs = BANG_COMMAND_TIMEOUT_MS, signal }) {
|
|
4534
|
+
return new Promise((resolve) => {
|
|
4535
|
+
let child;
|
|
4536
|
+
try {
|
|
4537
|
+
child = spawn(command, {
|
|
4538
|
+
cwd,
|
|
4539
|
+
shell: true,
|
|
4540
|
+
detached: USE_PROCESS_GROUP,
|
|
4541
|
+
stdio: [
|
|
4542
|
+
"ignore",
|
|
4543
|
+
"pipe",
|
|
4544
|
+
"pipe"
|
|
4545
|
+
]
|
|
4546
|
+
});
|
|
4547
|
+
} catch (error) {
|
|
4548
|
+
resolve({
|
|
4549
|
+
output: error instanceof Error ? error.message : String(error),
|
|
4550
|
+
isError: true
|
|
4551
|
+
});
|
|
4552
|
+
return;
|
|
4553
|
+
}
|
|
4554
|
+
let stdout = "";
|
|
4555
|
+
let stderr = "";
|
|
4556
|
+
let failure = null;
|
|
4557
|
+
const stop = (reason) => {
|
|
4558
|
+
failure ??= reason;
|
|
4559
|
+
killCommand(child);
|
|
4560
|
+
};
|
|
4561
|
+
runningCommands.set(child, stop);
|
|
4562
|
+
const collect = (append) => (text) => {
|
|
4563
|
+
if (failure) return;
|
|
4564
|
+
append(text);
|
|
4565
|
+
if (stdout.length + stderr.length > BANG_COMMAND_MAX_OUTPUT_CHARS) stop("overflow");
|
|
4566
|
+
};
|
|
4567
|
+
child.stdout?.setEncoding("utf-8");
|
|
4568
|
+
child.stderr?.setEncoding("utf-8");
|
|
4569
|
+
child.stdout?.on("data", collect((text) => stdout += text));
|
|
4570
|
+
child.stderr?.on("data", collect((text) => stderr += text));
|
|
4571
|
+
const timer = setTimeout(() => stop("timeout"), timeoutMs);
|
|
4572
|
+
const cancel = () => stop("cancelled");
|
|
4573
|
+
if (signal?.aborted) cancel();
|
|
4574
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
4575
|
+
const finish = (result) => {
|
|
4576
|
+
clearTimeout(timer);
|
|
4577
|
+
signal?.removeEventListener("abort", cancel);
|
|
4578
|
+
runningCommands.delete(child);
|
|
4579
|
+
resolve(result);
|
|
4580
|
+
};
|
|
4581
|
+
child.on("error", (error) => finish({
|
|
4582
|
+
output: error.message,
|
|
4583
|
+
isError: true
|
|
4584
|
+
}));
|
|
4585
|
+
child.on("close", (code) => {
|
|
4586
|
+
if (failure === "exit") finish(null);
|
|
4587
|
+
else if (failure === "timeout") {
|
|
4588
|
+
const partial = stdout ? `${stdout.trimEnd()}\n` : "";
|
|
4589
|
+
finish({
|
|
4590
|
+
output: `${partial}Command timed out after ${Math.round(timeoutMs / 1e3)}s`,
|
|
4591
|
+
isError: true
|
|
4592
|
+
});
|
|
4593
|
+
} else if (failure === "cancelled") finish({
|
|
4594
|
+
output: "Command cancelled",
|
|
4595
|
+
isError: true
|
|
4596
|
+
});
|
|
4597
|
+
else if (failure === "overflow") finish({
|
|
4598
|
+
output: `Command output exceeded ${BANG_COMMAND_MAX_OUTPUT_CHARS / 1024 / 1024} MB`,
|
|
4599
|
+
isError: true
|
|
4600
|
+
});
|
|
4601
|
+
else if (code === 0) finish({
|
|
4602
|
+
output: stdout,
|
|
4603
|
+
isError: false
|
|
4604
|
+
});
|
|
4605
|
+
else finish({
|
|
4606
|
+
output: stderr || stdout || `Command failed: ${command}`,
|
|
4607
|
+
isError: true
|
|
4608
|
+
});
|
|
4609
|
+
});
|
|
4610
|
+
});
|
|
4611
|
+
}
|
|
4612
|
+
//#endregion
|
|
4613
|
+
//#region src/session/whenTurnIdle.ts
|
|
4614
|
+
/**
|
|
4615
|
+
* Runs `callback` once nothing holds the session (see `holdSession` in store/index.ts). Agent
|
|
4616
|
+
* turns, custom commands, background completion and compaction write the session back from a
|
|
4617
|
+
* snapshot taken at their start, so anything appended to it meanwhile would be clobbered.
|
|
4618
|
+
*
|
|
4619
|
+
* The callback runs synchronously as the last hold is released, before a queued turn can start
|
|
4620
|
+
* and take its own snapshot. It runs inside that release, so a throw is logged rather than
|
|
4621
|
+
* escaping into the releasing operation's `finally`.
|
|
4622
|
+
*/
|
|
4623
|
+
function whenTurnIdle(callback) {
|
|
4624
|
+
const runCallback = () => {
|
|
4625
|
+
try {
|
|
4626
|
+
callback();
|
|
4627
|
+
} catch (error) {
|
|
4628
|
+
logger.debug(`[whenTurnIdle] callback failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
4629
|
+
}
|
|
4630
|
+
};
|
|
4631
|
+
if (useCliStore.getState().sessionHolds === 0) {
|
|
4632
|
+
runCallback();
|
|
4633
|
+
return;
|
|
4634
|
+
}
|
|
4635
|
+
const unsubscribe = useCliStore.subscribe((state) => {
|
|
4636
|
+
if (state.sessionHolds > 0) return;
|
|
4637
|
+
unsubscribe();
|
|
4638
|
+
runCallback();
|
|
4639
|
+
});
|
|
4640
|
+
}
|
|
4641
|
+
//#endregion
|
|
4429
4642
|
//#region src/utils/peonNotifier.ts
|
|
4430
4643
|
/**
|
|
4431
4644
|
* peon-ping notifier - native b4m adapter for peon-ping (https://peonping.com).
|
|
@@ -7767,6 +7980,12 @@ var BridgePresence = class {
|
|
|
7767
7980
|
const bridgePresence = new BridgePresence();
|
|
7768
7981
|
//#endregion
|
|
7769
7982
|
//#region src/bootstrap/wireAgentEvents.ts
|
|
7983
|
+
/** Apply `update` to the in-flight assistant message, if one is pending. */
|
|
7984
|
+
function updatePendingAssistant(update) {
|
|
7985
|
+
const { pendingMessages, updatePendingMessage } = useCliStore.getState();
|
|
7986
|
+
const lastIdx = pendingMessages.length - 1;
|
|
7987
|
+
if (lastIdx >= 0 && pendingMessages[lastIdx].role === "assistant") updatePendingMessage(lastIdx, update(pendingMessages[lastIdx]));
|
|
7988
|
+
}
|
|
7770
7989
|
/**
|
|
7771
7990
|
* Wire the main agent's step events to the UI store and the tavern transcript,
|
|
7772
7991
|
* and mirror the same handlers onto delegated subagents via the orchestrator's
|
|
@@ -7778,26 +7997,52 @@ const bridgePresence = new BridgePresence();
|
|
|
7778
7997
|
* singleton; neither owns React-lifecycle state.
|
|
7779
7998
|
*/
|
|
7780
7999
|
function wireAgentEvents(input) {
|
|
7781
|
-
const { agent,
|
|
7782
|
-
agent.observationQueue = agentContext.observationQueue;
|
|
8000
|
+
const { agent, orchestrator } = input;
|
|
7783
8001
|
const stepHandler = (step) => {
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
7796
|
-
|
|
8002
|
+
updatePendingAssistant((message) => ({
|
|
8003
|
+
...message,
|
|
8004
|
+
metadata: {
|
|
8005
|
+
...message.metadata,
|
|
8006
|
+
steps: [...message.metadata?.steps || [], formatStep(step)]
|
|
8007
|
+
}
|
|
8008
|
+
}));
|
|
8009
|
+
};
|
|
8010
|
+
let liveText = {
|
|
8011
|
+
messageId: "",
|
|
8012
|
+
iteration: -1,
|
|
8013
|
+
text: ""
|
|
8014
|
+
};
|
|
8015
|
+
const textDeltaHandler = ({ delta, iteration }) => {
|
|
8016
|
+
updatePendingAssistant((message) => {
|
|
8017
|
+
const sameStream = liveText.messageId === message.id && liveText.iteration === iteration;
|
|
8018
|
+
liveText = {
|
|
8019
|
+
messageId: message.id,
|
|
8020
|
+
iteration,
|
|
8021
|
+
text: (sameStream ? liveText.text : "") + delta
|
|
8022
|
+
};
|
|
8023
|
+
return {
|
|
8024
|
+
...message,
|
|
8025
|
+
content: liveText.text
|
|
8026
|
+
};
|
|
8027
|
+
});
|
|
8028
|
+
};
|
|
8029
|
+
const clearLiveText = () => {
|
|
8030
|
+
if (!liveText.text) return;
|
|
8031
|
+
liveText = {
|
|
8032
|
+
...liveText,
|
|
8033
|
+
text: ""
|
|
8034
|
+
};
|
|
8035
|
+
updatePendingAssistant((message) => ({
|
|
8036
|
+
...message,
|
|
8037
|
+
content: "..."
|
|
8038
|
+
}));
|
|
7797
8039
|
};
|
|
7798
8040
|
agent.on("thought", stepHandler);
|
|
7799
8041
|
agent.on("observation", stepHandler);
|
|
7800
8042
|
agent.on("action", stepHandler);
|
|
8043
|
+
agent.on("text_delta", textDeltaHandler);
|
|
8044
|
+
agent.on("thought", clearLiveText);
|
|
8045
|
+
agent.on("action", clearLiveText);
|
|
7801
8046
|
const pendingToolUseIds = /* @__PURE__ */ new Map();
|
|
7802
8047
|
const summarizeToolInput = (input) => {
|
|
7803
8048
|
if (input == null) return void 0;
|
|
@@ -8152,6 +8397,14 @@ async function handleSandboxTrustDomainCommand(deps, domains) {
|
|
|
8152
8397
|
* recurses with the same context for messages submitted mid-turn.
|
|
8153
8398
|
*/
|
|
8154
8399
|
async function runTurn(message, ctx) {
|
|
8400
|
+
const releaseSession = useCliStore.getState().holdSession();
|
|
8401
|
+
try {
|
|
8402
|
+
await runHeldTurn(message, ctx);
|
|
8403
|
+
} finally {
|
|
8404
|
+
releaseSession();
|
|
8405
|
+
}
|
|
8406
|
+
}
|
|
8407
|
+
async function runHeldTurn(message, ctx) {
|
|
8155
8408
|
const { agent, sessionStore, configStore, commandHistoryStore, customCommandStore, messageBuilder, config, availableModels, agentStore, contextContent, additionalDirectories, featureRegistry, backgroundManager, todoStore, decisionStore, blockerStore, postEditDiagnostics, workflowStores, setCommandHistory, setAbortController } = ctx;
|
|
8156
8409
|
const storeSession = useCliStore.getState().session;
|
|
8157
8410
|
if (!agent || !storeSession) {
|
|
@@ -8194,7 +8447,10 @@ async function runTurn(message, ctx) {
|
|
|
8194
8447
|
try {
|
|
8195
8448
|
const { prompt: compactionPrompt, preservedMessages } = buildCompactionPrompt(activeSession.messages, { claudeMdInstructions: extractCompactInstructions(contextContent || "") });
|
|
8196
8449
|
if (compactionPrompt) {
|
|
8197
|
-
const result = await agent.run(compactionPrompt, {
|
|
8450
|
+
const result = await agent.run(compactionPrompt, {
|
|
8451
|
+
maxIterations: 1,
|
|
8452
|
+
streamText: false
|
|
8453
|
+
});
|
|
8198
8454
|
const sessionToCompact = withFlushedWorkflowState(activeSession, workflowStores);
|
|
8199
8455
|
await sessionStore.save(sessionToCompact);
|
|
8200
8456
|
const newSession = createCompactedSession(sessionToCompact, result.finalAnswer, preservedMessages, !!(process.env.B4M_SESSION_ID || process.env.B4M_RESUME_ID));
|
|
@@ -8270,7 +8526,7 @@ async function runTurn(message, ctx) {
|
|
|
8270
8526
|
isReadOnlyTool,
|
|
8271
8527
|
workflowReminder,
|
|
8272
8528
|
drainFeedback: diagnostics ? (phase) => diagnostics.drain(DRAIN_BUDGET_MS[phase], abortController.signal) : void 0,
|
|
8273
|
-
onContextLimit: createReactiveCompactionHandler(agent, activeSession
|
|
8529
|
+
onContextLimit: createReactiveCompactionHandler(agent, activeSession)
|
|
8274
8530
|
});
|
|
8275
8531
|
} finally {
|
|
8276
8532
|
diagnostics?.endTurn();
|
|
@@ -8348,7 +8604,7 @@ async function runTurn(message, ctx) {
|
|
|
8348
8604
|
}
|
|
8349
8605
|
}
|
|
8350
8606
|
const rawMessage = error instanceof Error ? error.message : String(error);
|
|
8351
|
-
if (
|
|
8607
|
+
if (isConnectionDrop(error)) {
|
|
8352
8608
|
console.error("\n❌ The connection to the server dropped mid-response. Type \"continue\" to resume.\n");
|
|
8353
8609
|
logger.debug(`Full error details: ${error.stack || error.message}`);
|
|
8354
8610
|
return;
|
|
@@ -8500,6 +8756,14 @@ async function resumeSession(ctx, selected) {
|
|
|
8500
8756
|
* console notice) when there is no session/agent or too little to compact.
|
|
8501
8757
|
*/
|
|
8502
8758
|
async function compactSession(ctx, options = {}) {
|
|
8759
|
+
const releaseSession = useCliStore.getState().holdSession();
|
|
8760
|
+
try {
|
|
8761
|
+
await compactHeldSession(ctx, options);
|
|
8762
|
+
} finally {
|
|
8763
|
+
releaseSession();
|
|
8764
|
+
}
|
|
8765
|
+
}
|
|
8766
|
+
async function compactHeldSession(ctx, options) {
|
|
8503
8767
|
const session = useCliStore.getState().session;
|
|
8504
8768
|
if (!session || !ctx.agent) {
|
|
8505
8769
|
console.log("No active session");
|
|
@@ -8520,7 +8784,10 @@ async function compactSession(ctx, options = {}) {
|
|
|
8520
8784
|
console.log("🗜️ Compacting conversation...\n");
|
|
8521
8785
|
useCliStore.getState().setIsThinking(true);
|
|
8522
8786
|
try {
|
|
8523
|
-
const summary = (await ctx.agent.run(compactionPrompt, {
|
|
8787
|
+
const summary = (await ctx.agent.run(compactionPrompt, {
|
|
8788
|
+
maxIterations: 1,
|
|
8789
|
+
streamText: false
|
|
8790
|
+
})).finalAnswer;
|
|
8524
8791
|
await ctx.sessionStore.save(session);
|
|
8525
8792
|
const oldSessionName = session.name;
|
|
8526
8793
|
const newSession = createCompactedSession(session, summary, preservedMessages, !!(process.env.B4M_SESSION_ID || process.env.B4M_RESUME_ID));
|
|
@@ -8735,6 +9002,7 @@ function CliApp() {
|
|
|
8735
9002
|
} catch (err) {
|
|
8736
9003
|
logger.debug(`[CLEANUP] Shell session reap error: ${err}`);
|
|
8737
9004
|
}
|
|
9005
|
+
killRunningBangCommands();
|
|
8738
9006
|
const session = useCliStore.getState().session;
|
|
8739
9007
|
if (session) cleanupTasks.push(state.sessionStore.save(session).catch((err) => {
|
|
8740
9008
|
logger.debug(`[CLEANUP] Session save error: ${err.message}`);
|
|
@@ -9028,7 +9296,6 @@ function CliApp() {
|
|
|
9028
9296
|
};
|
|
9029
9297
|
const agentContext = {
|
|
9030
9298
|
currentAgent: null,
|
|
9031
|
-
observationQueue: [],
|
|
9032
9299
|
onFileChanged: (filePath) => postEditDiagnosticsRef.current.enqueue(filePath)
|
|
9033
9300
|
};
|
|
9034
9301
|
const { mcpManager, agentStore, contextResult, mcpTools, loadedB4mTools, deferredB4mTools, orchestrator, backgroundManager, historyStore } = await buildSupportingStores({
|
|
@@ -9095,7 +9362,7 @@ function CliApp() {
|
|
|
9095
9362
|
allowedDirectories: additionalDirectories
|
|
9096
9363
|
};
|
|
9097
9364
|
const enableSkillTool = config.preferences.enableSkillTool !== false;
|
|
9098
|
-
const skillTool = enableSkillTool ?
|
|
9365
|
+
const skillTool = enableSkillTool ? createSkillTool({
|
|
9099
9366
|
customCommandStore: state.customCommandStore,
|
|
9100
9367
|
subagentOrchestrator: orchestrator,
|
|
9101
9368
|
sessionId: newSession.id,
|
|
@@ -9104,28 +9371,29 @@ function CliApp() {
|
|
|
9104
9371
|
promptFn
|
|
9105
9372
|
},
|
|
9106
9373
|
allowedDirectories: additionalDirectories
|
|
9107
|
-
})
|
|
9108
|
-
const findDefinitionTool = wrapTools([createFindDefinitionTool(additionalDirectories)], cliWrapDeps)[0];
|
|
9109
|
-
const getFileStructureTool = wrapTools([createGetFileStructureTool(additionalDirectories)], cliWrapDeps)[0];
|
|
9374
|
+
}) : null;
|
|
9110
9375
|
const workItemTools = config.preferences.enableWorkItemTools ? createWorkItemTools(new WorkItemsClient(apiClient)) : [];
|
|
9111
|
-
const
|
|
9376
|
+
const agentToolsRef = { current: null };
|
|
9377
|
+
const toolSearchTool = deferredToolRegistry.size() > 0 ? createToolSearchTool(() => {
|
|
9378
|
+
if (!agentToolsRef.current) throw new Error("tool_search invoked before agent context was wired");
|
|
9379
|
+
return agentToolsRef.current;
|
|
9380
|
+
}) : null;
|
|
9381
|
+
const cliTools = buildCliToolList({
|
|
9382
|
+
toolSearchTool,
|
|
9112
9383
|
agentDelegateTool,
|
|
9113
|
-
|
|
9384
|
+
backgroundTools,
|
|
9114
9385
|
resumeAgentTool,
|
|
9115
9386
|
writeTodosTool,
|
|
9116
9387
|
decisionLogTool,
|
|
9117
|
-
|
|
9388
|
+
blockerTools,
|
|
9118
9389
|
reviewGateTool,
|
|
9119
|
-
findDefinitionTool,
|
|
9120
|
-
getFileStructureTool,
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
|
|
9126
|
-
const coordinateTaskTool = createCoordinateTaskTool(orchestrator, agentStore, newSession.id);
|
|
9127
|
-
cliTools.push(coordinateTaskTool);
|
|
9128
|
-
}
|
|
9390
|
+
findDefinitionTool: createFindDefinitionTool(additionalDirectories),
|
|
9391
|
+
getFileStructureTool: createGetFileStructureTool(additionalDirectories),
|
|
9392
|
+
workItemTools,
|
|
9393
|
+
skillTool,
|
|
9394
|
+
dynamicAgentTool,
|
|
9395
|
+
coordinateTaskTool: config.preferences.enableCoordinatorMode === true ? createCoordinateTaskTool(orchestrator, agentStore, newSession.id) : null
|
|
9396
|
+
}, cliWrapDeps);
|
|
9129
9397
|
const pluginDescriptors = await new PluginStore().discover();
|
|
9130
9398
|
const { registry: featureRegistry, loaded: loadedPlugins, skipped: skippedPlugins } = await buildFeatureRegistry({
|
|
9131
9399
|
builtins: createBuiltinModules(config, apiClient),
|
|
@@ -9135,18 +9403,10 @@ function CliApp() {
|
|
|
9135
9403
|
reservedToolNames: [...loadedB4mTools, ...cliTools].map((t) => t.toolSchema.name).concat("tool_search")
|
|
9136
9404
|
});
|
|
9137
9405
|
rewireReservedNames(state, featureRegistry);
|
|
9138
|
-
const featureModuleToolNames = featureRegistry.getAllToolNames();
|
|
9139
|
-
if (featureModuleToolNames.length > 0) registerFeatureModuleTools(featureModuleToolNames);
|
|
9140
9406
|
if (wsManager && featureRegistry.hasModules) featureRegistry.registerAllWsHandlers(wsManager);
|
|
9141
9407
|
const featureTools = featureRegistry.getAllTools();
|
|
9142
|
-
const agentToolsRef = { current: null };
|
|
9143
|
-
const toolSearchTool = deferredToolRegistry.size() > 0 ? createToolSearchTool(() => {
|
|
9144
|
-
if (!agentToolsRef.current) throw new Error("tool_search invoked before agent context was wired");
|
|
9145
|
-
return agentToolsRef.current;
|
|
9146
|
-
}) : null;
|
|
9147
9408
|
const allTools = [
|
|
9148
9409
|
...loadedB4mTools,
|
|
9149
|
-
...toolSearchTool ? [toolSearchTool] : [],
|
|
9150
9410
|
...cliTools,
|
|
9151
9411
|
...featureTools
|
|
9152
9412
|
];
|
|
@@ -9163,7 +9423,7 @@ function CliApp() {
|
|
|
9163
9423
|
}
|
|
9164
9424
|
if (loadedPlugins.length > 0) startupLog.push(`🔌 Plugins loaded: ${loadedPlugins.join(", ")}`);
|
|
9165
9425
|
if (skippedPlugins.length > 0) startupLog.push(`⚠️ Plugins skipped: ${skippedPlugins.map((s) => s.name).join(", ")} (see debug log)`);
|
|
9166
|
-
logger.debug(`Total tools available to agent: ${allTools.length} (${loadedB4mTools.length} B4M loaded + ${cliTools.length} CLI + ${featureTools.length} feature
|
|
9426
|
+
logger.debug(`Total tools available to agent: ${allTools.length} (${loadedB4mTools.length} B4M loaded + ${cliTools.length} CLI + ${featureTools.length} feature, ${deferredB4mTools.length} B4M + ${mcpTools.length} MCP deferred)`);
|
|
9167
9427
|
if (contextResult.globalContext) startupLog.push(`📄 Global context: ${contextResult.globalContext.filename}`);
|
|
9168
9428
|
if (contextResult.projectContext) startupLog.push(`📄 Project context: ${contextResult.projectContext.filename}`);
|
|
9169
9429
|
for (const error of contextResult.errors) startupLog.push(`⚠️ Context file error: ${error}`);
|
|
@@ -9193,7 +9453,6 @@ function CliApp() {
|
|
|
9193
9453
|
});
|
|
9194
9454
|
wireAgentEvents({
|
|
9195
9455
|
agent,
|
|
9196
|
-
agentContext,
|
|
9197
9456
|
orchestrator
|
|
9198
9457
|
});
|
|
9199
9458
|
setState((prev) => ({
|
|
@@ -9329,6 +9588,7 @@ function CliApp() {
|
|
|
9329
9588
|
return;
|
|
9330
9589
|
}
|
|
9331
9590
|
useCliStore.getState().setIsThinking(true);
|
|
9591
|
+
const releaseSession = useCliStore.getState().holdSession();
|
|
9332
9592
|
const abortController = new AbortController();
|
|
9333
9593
|
setState((prev) => ({
|
|
9334
9594
|
...prev,
|
|
@@ -9403,7 +9663,7 @@ function CliApp() {
|
|
|
9403
9663
|
signal: abortController.signal,
|
|
9404
9664
|
parallelExecution: cliConfig.preferences.enableParallelToolExecution === true,
|
|
9405
9665
|
isReadOnlyTool,
|
|
9406
|
-
onContextLimit: createReactiveCompactionHandler(state.agent, session
|
|
9666
|
+
onContextLimit: createReactiveCompactionHandler(state.agent, session)
|
|
9407
9667
|
});
|
|
9408
9668
|
} finally {
|
|
9409
9669
|
state.backgroundManager?.setCurrentTurn(null);
|
|
@@ -9520,6 +9780,7 @@ function CliApp() {
|
|
|
9520
9780
|
}));
|
|
9521
9781
|
state.agent.off("thought", stepHandler);
|
|
9522
9782
|
state.agent.off("action", stepHandler);
|
|
9783
|
+
releaseSession();
|
|
9523
9784
|
}
|
|
9524
9785
|
};
|
|
9525
9786
|
const handleMessage = (message) => runTurn(message, {
|
|
@@ -9572,11 +9833,12 @@ function CliApp() {
|
|
|
9572
9833
|
* without adding a user message to the conversation.
|
|
9573
9834
|
*/
|
|
9574
9835
|
const handleBackgroundCompletion = async () => {
|
|
9575
|
-
const session = useCliStore.getState().session;
|
|
9576
|
-
if (!state.agent || !session) return;
|
|
9577
9836
|
const authTokens = await state.configStore.getAuthTokens();
|
|
9578
9837
|
if (!authTokens || new Date(authTokens.expiresAt) <= /* @__PURE__ */ new Date()) return;
|
|
9838
|
+
const session = useCliStore.getState().session;
|
|
9839
|
+
if (!state.agent || !session) return;
|
|
9579
9840
|
useCliStore.getState().setIsThinking(true);
|
|
9841
|
+
const releaseSession = useCliStore.getState().holdSession();
|
|
9580
9842
|
const abortController = new AbortController();
|
|
9581
9843
|
setState((prev) => ({
|
|
9582
9844
|
...prev,
|
|
@@ -9649,59 +9911,74 @@ function CliApp() {
|
|
|
9649
9911
|
abortController: null
|
|
9650
9912
|
}));
|
|
9651
9913
|
useCliStore.getState().setIsThinking(false);
|
|
9914
|
+
releaseSession();
|
|
9652
9915
|
}
|
|
9653
9916
|
};
|
|
9654
|
-
const handleBashCommand = useCallback((command) => {
|
|
9655
|
-
|
|
9656
|
-
|
|
9657
|
-
|
|
9658
|
-
|
|
9659
|
-
|
|
9660
|
-
|
|
9661
|
-
|
|
9662
|
-
|
|
9663
|
-
|
|
9664
|
-
|
|
9665
|
-
|
|
9917
|
+
const handleBashCommand = useCallback(async (command) => {
|
|
9918
|
+
if (!useCliStore.getState().session) return;
|
|
9919
|
+
const ownsInput = !useCliStore.getState().isThinking;
|
|
9920
|
+
const sessionId = useCliStore.getState().session?.id;
|
|
9921
|
+
const abortController = new AbortController();
|
|
9922
|
+
if (ownsInput) {
|
|
9923
|
+
useCliStore.getState().setIsThinking(true);
|
|
9924
|
+
setState((prev) => ({
|
|
9925
|
+
...prev,
|
|
9926
|
+
abortController
|
|
9927
|
+
}));
|
|
9928
|
+
}
|
|
9929
|
+
const result = await runBangCommand(command, {
|
|
9930
|
+
cwd: process.cwd(),
|
|
9931
|
+
signal: abortController.signal
|
|
9932
|
+
});
|
|
9933
|
+
if (!result) return;
|
|
9934
|
+
const { output, isError } = result;
|
|
9935
|
+
whenTurnIdle(() => {
|
|
9936
|
+
const session = useCliStore.getState().session;
|
|
9937
|
+
if (!session || session.id !== sessionId) return;
|
|
9938
|
+
const userMessage = {
|
|
9939
|
+
id: v4(),
|
|
9940
|
+
role: "user",
|
|
9941
|
+
content: `$ ${command}`,
|
|
9942
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
9943
|
+
};
|
|
9944
|
+
const assistantMessage = {
|
|
9945
|
+
id: v4(),
|
|
9946
|
+
role: "assistant",
|
|
9947
|
+
content: isError ? `\u274c Error:\n${output}` : output.trim() || "(no output)",
|
|
9948
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
9949
|
+
};
|
|
9950
|
+
const updatedSession = {
|
|
9951
|
+
...session,
|
|
9952
|
+
messages: [
|
|
9953
|
+
...session.messages,
|
|
9954
|
+
userMessage,
|
|
9955
|
+
assistantMessage
|
|
9666
9956
|
],
|
|
9667
|
-
|
|
9668
|
-
|
|
9957
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9958
|
+
};
|
|
9959
|
+
setStoreSession(updatedSession);
|
|
9960
|
+
state.sessionStore.save(updatedSession).catch((error) => {
|
|
9961
|
+
logger.debug(`[BASH] Session save error: ${error instanceof Error ? error.message : String(error)}`);
|
|
9962
|
+
});
|
|
9963
|
+
});
|
|
9964
|
+
setState((prev) => prev.abortController === abortController ? {
|
|
9965
|
+
...prev,
|
|
9966
|
+
abortController: null
|
|
9967
|
+
} : prev);
|
|
9968
|
+
if (ownsInput && !abortController.signal.aborted && useCliStore.getState().sessionHolds === 0) {
|
|
9969
|
+
useCliStore.getState().setIsThinking(false);
|
|
9970
|
+
const queued = useCliStore.getState().dequeueAllMessages();
|
|
9971
|
+
if (queued.length > 0) setImmediate(() => {
|
|
9972
|
+
handleMessageRef.current?.(queued.join("\n\n"));
|
|
9669
9973
|
});
|
|
9670
|
-
} catch (error) {
|
|
9671
|
-
const execError = error;
|
|
9672
|
-
output = execError.stderr || execError.message || "Command failed";
|
|
9673
|
-
isError = true;
|
|
9674
9974
|
}
|
|
9675
|
-
const userMessage = {
|
|
9676
|
-
id: v4(),
|
|
9677
|
-
role: "user",
|
|
9678
|
-
content: `$ ${command}`,
|
|
9679
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
9680
|
-
};
|
|
9681
|
-
const assistantMessage = {
|
|
9682
|
-
id: v4(),
|
|
9683
|
-
role: "assistant",
|
|
9684
|
-
content: isError ? `❌ Error:\n${output}` : output.trim() || "(no output)",
|
|
9685
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
9686
|
-
};
|
|
9687
|
-
const updatedSession = {
|
|
9688
|
-
...session,
|
|
9689
|
-
messages: [
|
|
9690
|
-
...session.messages,
|
|
9691
|
-
userMessage,
|
|
9692
|
-
assistantMessage
|
|
9693
|
-
],
|
|
9694
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9695
|
-
};
|
|
9696
|
-
setStoreSession(updatedSession);
|
|
9697
|
-
state.sessionStore.save(updatedSession);
|
|
9698
9975
|
}, [state.sessionStore]);
|
|
9699
9976
|
const handleImageDetected = async (imageData) => {
|
|
9700
9977
|
try {
|
|
9701
9978
|
let imageStore = state.imageStore;
|
|
9702
9979
|
if (!imageStore) {
|
|
9703
9980
|
if (!imageStoreInitPromise.current) imageStoreInitPromise.current = (async () => {
|
|
9704
|
-
const { ImageStore: ImageStoreClass } = await import("./ImageStore-
|
|
9981
|
+
const { ImageStore: ImageStoreClass } = await import("./ImageStore-DuI3tgwQ.mjs");
|
|
9705
9982
|
const newImageStore = new ImageStoreClass();
|
|
9706
9983
|
setState((prev) => ({
|
|
9707
9984
|
...prev,
|
|
@@ -9820,8 +10097,12 @@ function CliApp() {
|
|
|
9820
10097
|
if (!prompt) return null;
|
|
9821
10098
|
console.log("📝 Generating session handoff...");
|
|
9822
10099
|
useCliStore.getState().setIsThinking(true);
|
|
10100
|
+
const releaseSession = useCliStore.getState().holdSession();
|
|
9823
10101
|
try {
|
|
9824
|
-
const result = await state.agent.run(prompt, {
|
|
10102
|
+
const result = await state.agent.run(prompt, {
|
|
10103
|
+
maxIterations: 1,
|
|
10104
|
+
streamText: false
|
|
10105
|
+
});
|
|
9825
10106
|
const handoff = parseHandoffResponse(result.finalAnswer);
|
|
9826
10107
|
if (!handoff) {
|
|
9827
10108
|
console.warn("⚠️ Handoff generation returned no parseable JSON; skipping.");
|
|
@@ -9852,6 +10133,7 @@ function CliApp() {
|
|
|
9852
10133
|
return null;
|
|
9853
10134
|
} finally {
|
|
9854
10135
|
useCliStore.getState().setIsThinking(false);
|
|
10136
|
+
releaseSession();
|
|
9855
10137
|
}
|
|
9856
10138
|
};
|
|
9857
10139
|
/**
|
|
@@ -10424,7 +10706,7 @@ function CliApp() {
|
|
|
10424
10706
|
return;
|
|
10425
10707
|
}
|
|
10426
10708
|
try {
|
|
10427
|
-
const diffOutput = state.checkpointStore.getCheckpointDiff(diffTarget);
|
|
10709
|
+
const diffOutput = await state.checkpointStore.getCheckpointDiff(diffTarget);
|
|
10428
10710
|
if (!diffOutput.trim()) console.log("No differences found between checkpoint and current state.");
|
|
10429
10711
|
else console.log(diffOutput);
|
|
10430
10712
|
} catch (err) {
|
|
@@ -10462,12 +10744,13 @@ function CliApp() {
|
|
|
10462
10744
|
timestamp: now
|
|
10463
10745
|
};
|
|
10464
10746
|
} catch (error) {
|
|
10465
|
-
if (isAxiosError(error))
|
|
10466
|
-
|
|
10467
|
-
|
|
10468
|
-
|
|
10469
|
-
|
|
10470
|
-
|
|
10747
|
+
if (isAxiosError(error)) {
|
|
10748
|
+
if (error.response?.status === 401) console.log("\n❌ Authentication failed. Run /login to re-authenticate.\n");
|
|
10749
|
+
else if (error.response?.status === 403) console.log("\n❌ Access denied. You may not have permission to view usage data.\n");
|
|
10750
|
+
else if (error.response?.status === 404) console.log("\n❌ Usage endpoint not available on this server.\n");
|
|
10751
|
+
else if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") console.log("\n❌ Cannot connect to server. Check your API configuration with /api-info\n");
|
|
10752
|
+
else console.log(`\n❌ Error fetching usage data: ${error.message}\n`);
|
|
10753
|
+
} else if (error instanceof Error) console.log(`\n❌ Error: ${error.message}\n`);
|
|
10471
10754
|
else console.log("\n❌ Unexpected error fetching usage data. Please try again.\n");
|
|
10472
10755
|
break;
|
|
10473
10756
|
}
|
|
@@ -11038,7 +11321,6 @@ function CliApp() {
|
|
|
11038
11321
|
let newFeatureRegistry = state.featureRegistry;
|
|
11039
11322
|
if (featuresChanged && state.agent) {
|
|
11040
11323
|
state.featureRegistry?.disposeAll();
|
|
11041
|
-
clearFeatureModuleTools();
|
|
11042
11324
|
const apiClient = new ApiClient(requireApiUrl(updatedConfig.apiConfig), state.configStore);
|
|
11043
11325
|
const oldFeatureToolNames = new Set(state.featureRegistry?.getAllToolNames() ?? []);
|
|
11044
11326
|
const baseTools = state.agent.getTools().filter((t) => !oldFeatureToolNames.has(t.toolSchema.name));
|
|
@@ -11053,8 +11335,6 @@ function CliApp() {
|
|
|
11053
11335
|
newFeatureRegistry = rebuiltRegistry;
|
|
11054
11336
|
rewireReservedNames(state, rebuiltRegistry);
|
|
11055
11337
|
for (const skippedPlugin of rebuilt.skipped) console.error(`\n\x1b[33m⚠️ Plugin ${skippedPlugin.name} skipped: ${skippedPlugin.reason}\x1b[0m`);
|
|
11056
|
-
const newToolNames = newFeatureRegistry.getAllToolNames();
|
|
11057
|
-
if (newToolNames.length > 0) registerFeatureModuleTools(newToolNames);
|
|
11058
11338
|
if (state.wsManager && newFeatureRegistry.hasModules) newFeatureRegistry.registerAllWsHandlers(state.wsManager);
|
|
11059
11339
|
const newFeatureTools = newFeatureRegistry.getAllTools();
|
|
11060
11340
|
state.agent.setTools([...baseTools, ...newFeatureTools]);
|
|
@@ -11360,10 +11640,8 @@ function CliApp() {
|
|
|
11360
11640
|
}
|
|
11361
11641
|
});
|
|
11362
11642
|
}
|
|
11363
|
-
|
|
11364
|
-
|
|
11365
|
-
if (updateResult?.updateAvailable) console.log(`\x1b[33m ⬆ Update available: v${updateResult.currentVersion} → v${updateResult.latestVersion} (run: b4m update)\x1b[0m\n`);
|
|
11366
|
-
} catch {}
|
|
11643
|
+
const cachedUpdate = await getStartupUpdateStatus(version);
|
|
11644
|
+
if (cachedUpdate?.updateAvailable) console.log(`\x1b[33m \u2b06 Update available: v${cachedUpdate.currentVersion} \u2192 v${cachedUpdate.latestVersion} (run: b4m update)\x1b[0m\n`);
|
|
11367
11645
|
if (import.meta.url.includes("/src/") || process.env.NODE_ENV === "development") logger.debug("🔧 Running in development mode (using TypeScript source)\n");
|
|
11368
11646
|
warmFileCache();
|
|
11369
11647
|
render(/* @__PURE__ */ React.createElement(CliApp, null), { exitOnCtrlC: false });
|