@bike4mind/cli 1.2.0 → 2.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 +3 -1
- package/dist/{AgentHistoryStore-B2NEOvSW.mjs → AgentHistoryStore-DIMzvf-y.mjs} +460 -274
- package/dist/{ApiClient-D0fQ2FT6.mjs → ApiClient-BTI0-m0g.mjs} +44 -5
- package/dist/{ConfigStore-qV7NrCgZ.mjs → ConfigStore-DIs-K9oD.mjs} +3205 -1078
- package/dist/{buildAgent-C-C-VGff.mjs → buildAgent-DOvEp92a.mjs} +115 -10
- package/dist/commands/acpCommand.mjs +5 -4
- package/dist/commands/apiCommand.mjs +1 -1
- package/dist/commands/doctorCommand.mjs +1 -1
- package/dist/commands/envCommand.mjs +1 -1
- package/dist/commands/headlessCommand.mjs +4 -3
- package/dist/commands/mcpCommand.mjs +3 -3
- package/dist/commands/pluginCommand.mjs +1 -1
- package/dist/commands/updateCommand.mjs +1 -1
- package/dist/index.mjs +522 -34
- package/dist/{package-CGZIoxcs.mjs → package-Dl_F2Fah.mjs} +1 -1
- package/dist/{serve-CPXqcEZr.mjs → serve-Y2zMbfGs.mjs} +237 -73
- package/package.json +11 -11
package/dist/index.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as CustomCommandStore, A as createAgentDelegateTool, B as ALWAYS_DENIED_FOR_AGENTS, C as createTodoStore, D as createCoordinateTaskTool, E as parseAgentConfig, F as PermissionManager, H as DEFAULT_MAX_ITERATIONS, J as setWebSocketToolExecutor, K as clearFeatureModuleTools, L as wrapTools, M as substituteArguments, O as createResumeAgentTool, P as findIterationBoundary, Q as RemoteSkillSource, R as getProcessHooks, S as createFindDefinitionTool, T as createSkillTool, U as DEFAULT_RETRY_CONFIG, V as DEFAULT_AGENT_MODEL, W as DEFAULT_THOROUGHNESS, X as buildSystemPrompt, Y as getPlanModeFilePath, Z as buildSkillsPromptSection, _ as createDecisionLogTool, at as searchCommands, b as createGetFileStructureTool, ct as formatFileSize, d as createReviewGateStore, dt as COMPACTION_SUMMARY_MARKER, et as CheckpointStore, f as createReviewGateTool, g as formatBlockersOutput, h as createBlockerTools, j as FallbackLlmBackend, k as createBackgroundAgentTools, lt as searchFiles, m as createBlockerStore, nt as hasFileReferences, ot as mergeCommands, p as formatReviewGatesOutput, pt as MAX_PASTE_SIZE, q as registerFeatureModuleTools, rt as processFileReferences, s as buildSandbox, st as rewireReservedNames, tt as SessionStore, u as isTransientNetworkError, ut as warmFileCache, v as createDecisionStore, w as createWriteTodosTool, x as createWorkItemTools, y as formatDecisionsOutput } from "./AgentHistoryStore-
|
|
2
|
+
import { $ as CustomCommandStore, A as createAgentDelegateTool, B as ALWAYS_DENIED_FOR_AGENTS, C as createTodoStore, D as createCoordinateTaskTool, E as parseAgentConfig, F as PermissionManager, H as DEFAULT_MAX_ITERATIONS, J as setWebSocketToolExecutor, K as clearFeatureModuleTools, L as wrapTools, M as substituteArguments, O as createResumeAgentTool, P as findIterationBoundary, Q as RemoteSkillSource, R as getProcessHooks, S as createFindDefinitionTool, T as createSkillTool, U as DEFAULT_RETRY_CONFIG, V as DEFAULT_AGENT_MODEL, W as DEFAULT_THOROUGHNESS, X as buildSystemPrompt, Y as getPlanModeFilePath, Z as buildSkillsPromptSection, _ as createDecisionLogTool, at as searchCommands, b as createGetFileStructureTool, ct as formatFileSize, d as createReviewGateStore, dt as COMPACTION_SUMMARY_MARKER, et as CheckpointStore, f as createReviewGateTool, g as formatBlockersOutput, h as createBlockerTools, j as FallbackLlmBackend, k as createBackgroundAgentTools, lt as searchFiles, m as createBlockerStore, nt as hasFileReferences, ot as mergeCommands, p as formatReviewGatesOutput, pt as MAX_PASTE_SIZE, q as registerFeatureModuleTools, rt as processFileReferences, s as buildSandbox, st as rewireReservedNames, tt as SessionStore, u as isTransientNetworkError, ut as warmFileCache, v as createDecisionStore, w as createWriteTodosTool, x as createWorkItemTools, y as formatDecisionsOutput } from "./AgentHistoryStore-DIMzvf-y.mjs";
|
|
3
3
|
import { a as selectLiveSubagentTokens, i as selectLiveSubagentCredits, n as selectActiveBackgroundShells, o as useCliStore, r as selectCompletedBackgroundShells, t as selectActiveBackgroundAgents } from "./store-CvjTpQPs.mjs";
|
|
4
4
|
import { r as isFeatureEnabled, t as PluginStore } from "./PluginStore-DwvOJ-G3.mjs";
|
|
5
|
-
import {
|
|
6
|
-
import { a as deferredToolRegistry, i as createToolSearchTool, n as buildSupportingStores, o as NotifyingLlmBackend, r as buildLlmBackend, t as buildAgent } from "./buildAgent-
|
|
5
|
+
import { B as REVIEW_GATE_STATUS_VALUES, D as CREDIT_DEDUCT_TRANSACTION_TYPES, H as SUBQUEST_STATUS_VALUES, O as ChatModels, _ as requireApiUrl, _t as actorKindMarker, d as isReadOnlyTool, et as isEarlyStop, f as ApiEndpointUnconfiguredError, g as parseApiUrl, gt as actorColorIndex, h as getEnvironmentName, ht as ACTOR_COLOR_SLOTS, i as isValidSessionId, m as getCreditsUrl, n as logger, p as LOCAL_DEV_URL, r as SESSION_ID_PATTERN, s as extractCompactInstructions, t as ConfigStore, v as resolveApiEndpoint, vt as actorKindSchema, yt as selfClaimedActorKindSchema } from "./ConfigStore-DIs-K9oD.mjs";
|
|
6
|
+
import { a as deferredToolRegistry, i as createToolSearchTool, n as buildSupportingStores, o as NotifyingLlmBackend, r as buildLlmBackend, t as buildAgent } from "./buildAgent-DOvEp92a.mjs";
|
|
7
7
|
import { t as getShellSessionManager } from "./ShellSessionManager-6o8KZzl1-vrbPAUTq.mjs";
|
|
8
|
-
import {
|
|
8
|
+
import { i as OAuthClient, t as ApiClient } from "./ApiClient-BTI0-m0g.mjs";
|
|
9
|
+
import { t as version } from "./package-Dl_F2Fah.mjs";
|
|
9
10
|
import { i as getTokenCounter, n as ConversationContext, r as reconstructTurnBlocks, t as WorkItemsClient } from "./WorkItemsClient-Cow6nXx7.mjs";
|
|
10
|
-
import { t as version } from "./package-CGZIoxcs.mjs";
|
|
11
11
|
import { r as checkForUpdate } from "./updateChecker-CQW8bxo6.mjs";
|
|
12
12
|
import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState, useSyncExternalStore } from "react";
|
|
13
13
|
import { Box, Static, Text, render, useApp, useInput, usePaste, useStdout } from "ink";
|
|
@@ -1085,12 +1085,15 @@ function CompletedGroupNotification() {
|
|
|
1085
1085
|
//#endregion
|
|
1086
1086
|
//#region src/components/PermissionPrompt.tsx
|
|
1087
1087
|
/**
|
|
1088
|
-
* Escape characters that let
|
|
1089
|
-
* actually runs, into a visible escape form.
|
|
1090
|
-
*
|
|
1091
|
-
*
|
|
1092
|
-
*
|
|
1093
|
-
*
|
|
1088
|
+
* Escape characters that let untrusted text render differently from what
|
|
1089
|
+
* actually runs, into a visible escape form. Shared by every prompt that shows
|
|
1090
|
+
* untrusted text for the user to decide on: tool previews/arguments here,
|
|
1091
|
+
* repo-authored MCP definitions (McpApprovalPrompt), the folder-trust path
|
|
1092
|
+
* (FolderTrustPrompt), and model-authored ReviewGatePrompt / UserQuestionPrompt text. Display only - never escape the
|
|
1093
|
+
* value that is acted on or sent back.
|
|
1094
|
+
* These ranges are covered, deliberately and no wider:
|
|
1095
|
+
* - C0 controls (minus tab/newline), DEL and C1 controls -> `\xHH`: a raw
|
|
1096
|
+
* `\r`, ESC or 8-bit CSI (U+009B) could rewrite the line being approved.
|
|
1094
1097
|
* - Unicode bidi overrides U+202A..U+202E and isolates U+2066..U+2069
|
|
1095
1098
|
* -> `\uXXXX`: these reorder displayed glyphs without changing the
|
|
1096
1099
|
* underlying code points, spoofing the same way.
|
|
@@ -1098,7 +1101,7 @@ function CompletedGroupNotification() {
|
|
|
1098
1101
|
* Widen the scope here, so both call sites gain it at once.
|
|
1099
1102
|
*/
|
|
1100
1103
|
function escapeTerminalControlChars(text) {
|
|
1101
|
-
return text.replace(/[\x00-\x08\x0b-\x1f\x7f\u202a-\u202e\u2066-\u2069]/g, (ch) => {
|
|
1104
|
+
return text.replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f\u202a-\u202e\u2066-\u2069]/g, (ch) => {
|
|
1102
1105
|
const code = ch.charCodeAt(0);
|
|
1103
1106
|
return code > 255 ? `\\u${code.toString(16).padStart(4, "0")}` : `\\x${code.toString(16).padStart(2, "0")}`;
|
|
1104
1107
|
});
|
|
@@ -1232,7 +1235,7 @@ function PermissionPrompt({ toolName, toolDescription, args, preview, canBeTrust
|
|
|
1232
1235
|
}, headerText)), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Tool: "), /* @__PURE__ */ React.createElement(Text, {
|
|
1233
1236
|
bold: true,
|
|
1234
1237
|
color: "cyan"
|
|
1235
|
-
}, toolName)), toolDescription && /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Action: "), /* @__PURE__ */ React.createElement(Text, null, toolDescription)), !hideArgs && /* @__PURE__ */ React.createElement(Box, {
|
|
1238
|
+
}, escapeTerminalControlChars(toolName))), toolDescription && /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Action: "), /* @__PURE__ */ React.createElement(Text, null, escapeTerminalControlChars(toolDescription))), !hideArgs && /* @__PURE__ */ React.createElement(Box, {
|
|
1236
1239
|
marginTop: 1,
|
|
1237
1240
|
flexDirection: "column"
|
|
1238
1241
|
}, /* @__PURE__ */ React.createElement(Text, { bold: true }, "Arguments:"), /* @__PURE__ */ React.createElement(Box, {
|
|
@@ -1408,7 +1411,7 @@ function UserQuestionPrompt({ payload, onResponse }) {
|
|
|
1408
1411
|
}, /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, {
|
|
1409
1412
|
bold: true,
|
|
1410
1413
|
color: "cyan"
|
|
1411
|
-
}, "? Question", totalQuestions > 1 ? ` ${questionIndex + 1}/${totalQuestions}` : "")), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true }, currentQuestion.question)), /* @__PURE__ */ React.createElement(Box, {
|
|
1414
|
+
}, "? Question", totalQuestions > 1 ? ` ${questionIndex + 1}/${totalQuestions}` : "")), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true }, escapeTerminalControlChars(currentQuestion.question))), /* @__PURE__ */ React.createElement(Box, {
|
|
1412
1415
|
marginTop: 1,
|
|
1413
1416
|
flexDirection: "column"
|
|
1414
1417
|
}, options.map((opt, idx) => {
|
|
@@ -1418,12 +1421,12 @@ function UserQuestionPrompt({ payload, onResponse }) {
|
|
|
1418
1421
|
return /* @__PURE__ */ React.createElement(Box, { key: idx }, /* @__PURE__ */ React.createElement(Text, { color: "cyan" }, idx + 1, "."), /* @__PURE__ */ React.createElement(Text, {
|
|
1419
1422
|
color: isHighlighted ? "cyan" : void 0,
|
|
1420
1423
|
bold: isHighlighted
|
|
1421
|
-
}, " ", prefix, " ", opt.label), idx === otherIndex && isHighlighted ? /* @__PURE__ */ React.createElement(Box, { marginLeft: 1 }, /* @__PURE__ */ React.createElement(TextInput, {
|
|
1424
|
+
}, " ", prefix, " ", escapeTerminalControlChars(opt.label)), idx === otherIndex && isHighlighted ? /* @__PURE__ */ React.createElement(Box, { marginLeft: 1 }, /* @__PURE__ */ React.createElement(TextInput, {
|
|
1422
1425
|
value: otherText,
|
|
1423
1426
|
onChange: setOtherText,
|
|
1424
1427
|
onSubmit: submitOther,
|
|
1425
1428
|
placeholder: "Type your answer..."
|
|
1426
|
-
})) : opt.description && idx !== otherIndex && /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " - ", opt.description));
|
|
1429
|
+
})) : opt.description && idx !== otherIndex && /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " - ", escapeTerminalControlChars(opt.description)));
|
|
1427
1430
|
})), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, isOnOther ? "Type your answer, Enter to submit, or arrow keys to go back" : isMulti ? "Press 1-" + options.length + ", Space to toggle, Enter to confirm" : "Press 1-" + options.length + ", or arrow keys + Enter")));
|
|
1428
1431
|
}
|
|
1429
1432
|
//#endregion
|
|
@@ -1526,10 +1529,10 @@ function ReviewGatePrompt({ description, options, recommendation, onResponse })
|
|
|
1526
1529
|
}, /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, {
|
|
1527
1530
|
bold: true,
|
|
1528
1531
|
color: "magenta"
|
|
1529
|
-
}, "🛑 Review Gate")), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, description)), recommendation && /* @__PURE__ */ React.createElement(Box, {
|
|
1532
|
+
}, "🛑 Review Gate")), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, escapeTerminalControlChars(description))), recommendation && /* @__PURE__ */ React.createElement(Box, {
|
|
1530
1533
|
marginTop: 1,
|
|
1531
1534
|
flexDirection: "column"
|
|
1532
|
-
}, /* @__PURE__ */ React.createElement(Text, { bold: true }, "Recommendation:"), /* @__PURE__ */ React.createElement(Box, { paddingLeft: 2 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, recommendation))), options && options.length > 0 && /* @__PURE__ */ React.createElement(Box, {
|
|
1535
|
+
}, /* @__PURE__ */ React.createElement(Text, { bold: true }, "Recommendation:"), /* @__PURE__ */ React.createElement(Box, { paddingLeft: 2 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, escapeTerminalControlChars(recommendation)))), options && options.length > 0 && /* @__PURE__ */ React.createElement(Box, {
|
|
1533
1536
|
marginTop: 1,
|
|
1534
1537
|
flexDirection: "column"
|
|
1535
1538
|
}, /* @__PURE__ */ React.createElement(Text, { bold: true }, "Options:"), /* @__PURE__ */ React.createElement(Box, {
|
|
@@ -1538,7 +1541,7 @@ function ReviewGatePrompt({ description, options, recommendation, onResponse })
|
|
|
1538
1541
|
}, options.map((opt, idx) => /* @__PURE__ */ React.createElement(Text, {
|
|
1539
1542
|
key: idx,
|
|
1540
1543
|
dimColor: true
|
|
1541
|
-
}, "• ", opt)))), /* @__PURE__ */ React.createElement(Box, {
|
|
1544
|
+
}, "• ", escapeTerminalControlChars(opt))))), /* @__PURE__ */ React.createElement(Box, {
|
|
1542
1545
|
marginTop: 1,
|
|
1543
1546
|
flexDirection: "column"
|
|
1544
1547
|
}, ITEMS.map((item, index) => {
|
|
@@ -1816,6 +1819,18 @@ function buildConfigItems(availableModels, pluginDescriptors = []) {
|
|
|
1816
1819
|
enableCoordinatorMode: value
|
|
1817
1820
|
}
|
|
1818
1821
|
})
|
|
1822
|
+
}, {
|
|
1823
|
+
key: "postEditDiagnostics",
|
|
1824
|
+
label: "Post-Edit Diagnostics",
|
|
1825
|
+
type: "boolean",
|
|
1826
|
+
getValue: (config) => config.preferences.postEditDiagnostics ?? false,
|
|
1827
|
+
setValue: (config, value) => ({
|
|
1828
|
+
...config,
|
|
1829
|
+
preferences: {
|
|
1830
|
+
...config.preferences,
|
|
1831
|
+
postEditDiagnostics: value
|
|
1832
|
+
}
|
|
1833
|
+
})
|
|
1819
1834
|
}, {
|
|
1820
1835
|
key: "featuresTavern",
|
|
1821
1836
|
label: "Tavern",
|
|
@@ -2697,7 +2712,7 @@ function FolderTrustPrompt({ projectRoot, onSelect }) {
|
|
|
2697
2712
|
}, /* @__PURE__ */ React.createElement(Text, {
|
|
2698
2713
|
bold: true,
|
|
2699
2714
|
color: "yellow"
|
|
2700
|
-
}, "This project ships Bike4Mind config that can run code."), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, projectRoot), /* @__PURE__ */ React.createElement(Text, null, "Repo-committed config, MCP servers, agents and skills stay inert until you trust this folder. Only trust folders you would run code from.")), /* @__PURE__ */ React.createElement(SelectInput, {
|
|
2715
|
+
}, "This project ships Bike4Mind config that can run code."), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, escapeTerminalControlChars(projectRoot)), /* @__PURE__ */ React.createElement(Text, null, "Repo-committed config, MCP servers, agents and skills stay inert until you trust this folder. Only trust folders you would run code from.")), /* @__PURE__ */ React.createElement(SelectInput, {
|
|
2701
2716
|
items: [{
|
|
2702
2717
|
label: "Not now - keep repo config inert this session",
|
|
2703
2718
|
value: "not-now"
|
|
@@ -2710,6 +2725,52 @@ function FolderTrustPrompt({ projectRoot, onSelect }) {
|
|
|
2710
2725
|
}), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Use ↑↓ arrows to navigate, Enter to select. You can change this later with /trust folder.")));
|
|
2711
2726
|
}
|
|
2712
2727
|
//#endregion
|
|
2728
|
+
//#region src/components/McpApprovalPrompt.tsx
|
|
2729
|
+
const displaySafe = (s) => escapeTerminalControlChars(s).replace(/[\t\n]/g, (c) => c === "\n" ? "\\x0a" : "\\x09");
|
|
2730
|
+
const displayArg = (a) => {
|
|
2731
|
+
const safe = displaySafe(a);
|
|
2732
|
+
return /[\s"']/.test(a) ? `"${safe.replace(/"/g, "\\\"")}"` : safe;
|
|
2733
|
+
};
|
|
2734
|
+
/**
|
|
2735
|
+
* Startup prompt for repo-discovered MCP servers (trusted project) whose exact
|
|
2736
|
+
* definition hasn't been approved. Shows env/header KEY names; values only for
|
|
2737
|
+
* loader-style env keys (PATH, NODE_OPTIONS, LD_*, ...) that change what runs.
|
|
2738
|
+
* "Approve all" persists each definition's fingerprint (a changed definition asks
|
|
2739
|
+
* again); "Skip" persists nothing, so they stay off and re-prompt next launch.
|
|
2740
|
+
*
|
|
2741
|
+
* "Skip" is listed first so Enter-through never starts a server. Sibling of
|
|
2742
|
+
* FolderTrustPrompt.
|
|
2743
|
+
*/
|
|
2744
|
+
function McpApprovalPrompt({ projectRoot, servers, onSelect }) {
|
|
2745
|
+
return /* @__PURE__ */ React.createElement(Box, {
|
|
2746
|
+
flexDirection: "column",
|
|
2747
|
+
marginY: 1
|
|
2748
|
+
}, /* @__PURE__ */ React.createElement(Box, {
|
|
2749
|
+
marginBottom: 1,
|
|
2750
|
+
flexDirection: "column"
|
|
2751
|
+
}, /* @__PURE__ */ React.createElement(Text, {
|
|
2752
|
+
bold: true,
|
|
2753
|
+
color: "yellow"
|
|
2754
|
+
}, "This project defines MCP servers that will run code on your machine."), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, displaySafe(projectRoot))), /* @__PURE__ */ React.createElement(Box, {
|
|
2755
|
+
marginBottom: 1,
|
|
2756
|
+
flexDirection: "column"
|
|
2757
|
+
}, servers.map((s) => /* @__PURE__ */ React.createElement(Box, {
|
|
2758
|
+
key: s.name,
|
|
2759
|
+
flexDirection: "column",
|
|
2760
|
+
marginBottom: 1
|
|
2761
|
+
}, /* @__PURE__ */ React.createElement(Text, { bold: true }, displaySafe(s.name)), /* @__PURE__ */ React.createElement(Text, null, s.url !== void 0 ? displaySafe(s.url) : [s.command, ...s.args ?? []].filter((p) => p !== void 0).map(displayArg).join(" ")), s.envKeys.length > 0 && /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "env:", " ", s.envKeys.map((k) => k in s.envValues ? displaySafe(`${k}=${s.envValues[k]}`) : displaySafe(k)).join(", ")), s.headerKeys.length > 0 && /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "headers: ", s.headerKeys.map(displaySafe).join(", "))))), /* @__PURE__ */ React.createElement(SelectInput, {
|
|
2762
|
+
items: [{
|
|
2763
|
+
label: "Skip - don't start these this session",
|
|
2764
|
+
value: "skip"
|
|
2765
|
+
}, {
|
|
2766
|
+
label: "Approve all - start these servers (asks again if they change)",
|
|
2767
|
+
value: "approve"
|
|
2768
|
+
}],
|
|
2769
|
+
onSelect: (item) => onSelect(item.value),
|
|
2770
|
+
itemComponent: ({ isSelected, label }) => /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { color: isSelected ? "cyan" : void 0 }, isSelected ? "❯ " : " ", label))
|
|
2771
|
+
}), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Use ↑↓ arrows to navigate, Enter to select.")));
|
|
2772
|
+
}
|
|
2773
|
+
//#endregion
|
|
2713
2774
|
//#region src/components/RewindSelector.tsx
|
|
2714
2775
|
function RewindSelector({ messages, onSelect, onCancel }) {
|
|
2715
2776
|
const [step, setStep] = useState("selection");
|
|
@@ -2952,6 +3013,37 @@ function isBrowserOpenableUrl(raw) {
|
|
|
2952
3013
|
if (url.protocol === "https:") return true;
|
|
2953
3014
|
return url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1");
|
|
2954
3015
|
}
|
|
3016
|
+
/**
|
|
3017
|
+
* The device-flow response is server-controlled, so a compromised server could point the
|
|
3018
|
+
* verification page at a phishing origin. Only auto-open URLs on the configured API origin.
|
|
3019
|
+
* Strict compare: no subdomain aliasing and no localhost/127.0.0.1 equivalence.
|
|
3020
|
+
*/
|
|
3021
|
+
function isOnApiOrigin(raw, apiUrl) {
|
|
3022
|
+
try {
|
|
3023
|
+
return new URL(raw).origin === new URL(apiUrl).origin;
|
|
3024
|
+
} catch {
|
|
3025
|
+
return false;
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
function originOf(raw) {
|
|
3029
|
+
try {
|
|
3030
|
+
const { origin } = new URL(raw);
|
|
3031
|
+
return origin === "null" ? raw : origin;
|
|
3032
|
+
} catch {
|
|
3033
|
+
return raw;
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
/** Strips C0/C1 control chars so a server-supplied string can't inject terminal escape sequences. */
|
|
3037
|
+
function stripControlChars(raw) {
|
|
3038
|
+
return raw === void 0 ? raw : raw.replace(/[\u0000-\u001f\u007f-\u009f]/g, "");
|
|
3039
|
+
}
|
|
3040
|
+
function hostOf(raw) {
|
|
3041
|
+
try {
|
|
3042
|
+
return new URL(raw).host;
|
|
3043
|
+
} catch {
|
|
3044
|
+
return raw;
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
2955
3047
|
function extractErrorMessage(err) {
|
|
2956
3048
|
if (axios.isAxiosError(err)) {
|
|
2957
3049
|
if (err.response?.data) {
|
|
@@ -2973,13 +3065,19 @@ function LoginFlow({ apiUrl = "http://localhost:3000", configStore, onSuccess, o
|
|
|
2973
3065
|
const [deviceFlow, setDeviceFlow] = useState(null);
|
|
2974
3066
|
const [statusMessage, setStatusMessage] = useState("Initiating device authorization...");
|
|
2975
3067
|
const [error, setError] = useState(null);
|
|
3068
|
+
const [account, setAccount] = useState(null);
|
|
2976
3069
|
useEffect(() => {
|
|
2977
3070
|
const runLoginFlow = async () => {
|
|
2978
3071
|
const oauth = new OAuthClient(apiUrl);
|
|
2979
3072
|
try {
|
|
2980
3073
|
setStatus("initiating");
|
|
2981
3074
|
const deviceFlowResponse = await oauth.initiateDeviceFlow();
|
|
2982
|
-
setDeviceFlow(
|
|
3075
|
+
setDeviceFlow({
|
|
3076
|
+
...deviceFlowResponse,
|
|
3077
|
+
verification_uri: stripControlChars(deviceFlowResponse.verification_uri),
|
|
3078
|
+
verification_uri_complete: stripControlChars(deviceFlowResponse.verification_uri_complete),
|
|
3079
|
+
user_code: stripControlChars(deviceFlowResponse.user_code)
|
|
3080
|
+
});
|
|
2983
3081
|
setStatus("waiting");
|
|
2984
3082
|
setStatusMessage("Waiting for authorization...");
|
|
2985
3083
|
const tokens = await oauth.waitForAuthorization(deviceFlowResponse.device_code, deviceFlowResponse.interval, (message) => setStatusMessage(message));
|
|
@@ -2991,12 +3089,18 @@ function LoginFlow({ apiUrl = "http://localhost:3000", configStore, onSuccess, o
|
|
|
2991
3089
|
expiresAt,
|
|
2992
3090
|
userId
|
|
2993
3091
|
});
|
|
3092
|
+
setAccount({ userId });
|
|
2994
3093
|
setStatus("success");
|
|
2995
3094
|
setStatusMessage("Successfully authenticated!");
|
|
2996
3095
|
setTimeout(() => onSuccess(), 1500);
|
|
3096
|
+
new ApiClient(apiUrl, configStore).get("/api/identify").then((res) => setAccount({
|
|
3097
|
+
userId,
|
|
3098
|
+
email: stripControlChars(res?.user?.email),
|
|
3099
|
+
username: stripControlChars(res?.user?.username)
|
|
3100
|
+
})).catch(() => {});
|
|
2997
3101
|
} catch (err) {
|
|
2998
3102
|
setStatus("error");
|
|
2999
|
-
const errorMessage = extractErrorMessage(err);
|
|
3103
|
+
const errorMessage = stripControlChars(extractErrorMessage(err));
|
|
3000
3104
|
setError(errorMessage);
|
|
3001
3105
|
onError(new Error(errorMessage));
|
|
3002
3106
|
}
|
|
@@ -3008,12 +3112,19 @@ function LoginFlow({ apiUrl = "http://localhost:3000", configStore, onSuccess, o
|
|
|
3008
3112
|
onSuccess,
|
|
3009
3113
|
onError
|
|
3010
3114
|
]);
|
|
3115
|
+
const uris = deviceFlow ? [deviceFlow.verification_uri, deviceFlow.verification_uri_complete] : [];
|
|
3116
|
+
const isTrustedUri = (u) => isBrowserOpenableUrl(u) && isOnApiOrigin(u, apiUrl);
|
|
3117
|
+
const untrustedUri = uris.find((u) => !isTrustedUri(u));
|
|
3118
|
+
const trusted = uris.length > 0 && uris.every(isTrustedUri);
|
|
3011
3119
|
useEffect(() => {
|
|
3012
|
-
if (deviceFlow && status === "waiting"
|
|
3120
|
+
if (deviceFlow && status === "waiting" && trusted) open(deviceFlow.verification_uri_complete).catch((err) => {
|
|
3013
3121
|
console.error("Failed to auto-open browser:", err);
|
|
3014
3122
|
});
|
|
3015
|
-
|
|
3016
|
-
|
|
3123
|
+
}, [
|
|
3124
|
+
deviceFlow,
|
|
3125
|
+
status,
|
|
3126
|
+
trusted
|
|
3127
|
+
]);
|
|
3017
3128
|
if (status === "initiating") return /* @__PURE__ */ React.createElement(Box, {
|
|
3018
3129
|
flexDirection: "column",
|
|
3019
3130
|
padding: 1
|
|
@@ -3031,7 +3142,7 @@ function LoginFlow({ apiUrl = "http://localhost:3000", configStore, onSuccess, o
|
|
|
3031
3142
|
}, /* @__PURE__ */ React.createElement(Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, {
|
|
3032
3143
|
color: "green",
|
|
3033
3144
|
bold: true
|
|
3034
|
-
}, "✔ Successfully authenticated!")), /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text,
|
|
3145
|
+
}, "✔ Successfully authenticated!")), /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, null, "Logged in as ", account?.email || account?.username || `user ${account?.userId}`, " on ", hostOf(apiUrl))));
|
|
3035
3146
|
return /* @__PURE__ */ React.createElement(Box, {
|
|
3036
3147
|
flexDirection: "column",
|
|
3037
3148
|
padding: 1,
|
|
@@ -3040,7 +3151,10 @@ function LoginFlow({ apiUrl = "http://localhost:3000", configStore, onSuccess, o
|
|
|
3040
3151
|
}, /* @__PURE__ */ React.createElement(Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, {
|
|
3041
3152
|
color: "cyan",
|
|
3042
3153
|
bold: true
|
|
3043
|
-
}, "🔐 Device Authorization")), /* @__PURE__ */ React.createElement(Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Opening browser automatically... If it doesn't open, please visit:")), /* @__PURE__ */ React.createElement(Box, {
|
|
3154
|
+
}, "🔐 Device Authorization")), trusted ? /* @__PURE__ */ React.createElement(Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Opening browser automatically... If it doesn't open, please visit:")) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, {
|
|
3155
|
+
color: "red",
|
|
3156
|
+
bold: true
|
|
3157
|
+
}, !untrustedUri ? "Not opening browser: the server did not return a verification URL." : isOnApiOrigin(untrustedUri, apiUrl) ? `Not opening browser: verification URL ${originOf(untrustedUri)} is not https (plain http is only auto-opened on localhost). Only continue if you trust it.` : `Not opening browser: verification URL origin ${originOf(untrustedUri)} does not match the configured server ${originOf(apiUrl)}. Only continue if you trust it.`)), /* @__PURE__ */ React.createElement(Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Please visit:"))), /* @__PURE__ */ React.createElement(Box, {
|
|
3044
3158
|
marginBottom: 1,
|
|
3045
3159
|
paddingLeft: 2
|
|
3046
3160
|
}, /* @__PURE__ */ React.createElement(Text, {
|
|
@@ -4517,6 +4631,314 @@ Unlike agent_delegate (which uses pre-defined agents), this tool lets you compos
|
|
|
4517
4631
|
};
|
|
4518
4632
|
}
|
|
4519
4633
|
//#endregion
|
|
4634
|
+
//#region src/diagnostics/checkers.ts
|
|
4635
|
+
const CHECK_TIMEOUT_MS = 12e4;
|
|
4636
|
+
const MAX_OUTPUT_BYTES = 16777216;
|
|
4637
|
+
const TYPESCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
4638
|
+
".ts",
|
|
4639
|
+
".tsx",
|
|
4640
|
+
".mts",
|
|
4641
|
+
".cts"
|
|
4642
|
+
]);
|
|
4643
|
+
const ESLINT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
4644
|
+
...TYPESCRIPT_EXTENSIONS,
|
|
4645
|
+
".js",
|
|
4646
|
+
".jsx",
|
|
4647
|
+
".mjs",
|
|
4648
|
+
".cjs"
|
|
4649
|
+
]);
|
|
4650
|
+
const ESLINT_ERROR_SEVERITY = 2;
|
|
4651
|
+
const TSC_ENTRY = path.join("node_modules", "typescript", "bin", "tsc");
|
|
4652
|
+
const ESLINT_ENTRY = path.join("node_modules", "eslint", "bin", "eslint.js");
|
|
4653
|
+
const TSC_ERROR_LINE = /^(.+?)\((\d+),(\d+)\): error (TS\d+): (.*)$/;
|
|
4654
|
+
/**
|
|
4655
|
+
* Runs a binary without a shell (paths are passed as argv, never interpolated).
|
|
4656
|
+
* A non-zero exit is a normal outcome - tsc/eslint exit non-zero when they find errors.
|
|
4657
|
+
*/
|
|
4658
|
+
function runProcess(file, args, cwd) {
|
|
4659
|
+
return new Promise((resolve) => {
|
|
4660
|
+
execFile(file, args, {
|
|
4661
|
+
cwd,
|
|
4662
|
+
timeout: CHECK_TIMEOUT_MS,
|
|
4663
|
+
maxBuffer: MAX_OUTPUT_BYTES,
|
|
4664
|
+
killSignal: "SIGKILL",
|
|
4665
|
+
windowsHide: true
|
|
4666
|
+
}, (error, stdout, stderr) => {
|
|
4667
|
+
if (!error) {
|
|
4668
|
+
resolve({
|
|
4669
|
+
kind: "exited",
|
|
4670
|
+
stdout
|
|
4671
|
+
});
|
|
4672
|
+
return;
|
|
4673
|
+
}
|
|
4674
|
+
if (error.killed) {
|
|
4675
|
+
resolve({
|
|
4676
|
+
kind: "failed",
|
|
4677
|
+
reason: `timed out after ${CHECK_TIMEOUT_MS}ms`
|
|
4678
|
+
});
|
|
4679
|
+
return;
|
|
4680
|
+
}
|
|
4681
|
+
if (typeof error.code !== "number") {
|
|
4682
|
+
resolve({
|
|
4683
|
+
kind: "failed",
|
|
4684
|
+
reason: error.message
|
|
4685
|
+
});
|
|
4686
|
+
return;
|
|
4687
|
+
}
|
|
4688
|
+
if (!stdout.trim()) {
|
|
4689
|
+
resolve({
|
|
4690
|
+
kind: "failed",
|
|
4691
|
+
reason: `exit ${error.code}: ${stderr.trim().slice(0, 300)}`
|
|
4692
|
+
});
|
|
4693
|
+
return;
|
|
4694
|
+
}
|
|
4695
|
+
resolve({
|
|
4696
|
+
kind: "exited",
|
|
4697
|
+
stdout
|
|
4698
|
+
});
|
|
4699
|
+
});
|
|
4700
|
+
});
|
|
4701
|
+
}
|
|
4702
|
+
/**
|
|
4703
|
+
* Runs a package's JS entry under the current Node. `node_modules/.bin/*` are sh
|
|
4704
|
+
* shims (`.cmd` on Windows) that `execFile` cannot launch without a shell.
|
|
4705
|
+
*/
|
|
4706
|
+
function runNodeScript(script, args, cwd) {
|
|
4707
|
+
return runProcess(process.execPath, [script, ...args], cwd);
|
|
4708
|
+
}
|
|
4709
|
+
/** Nearest `relativePath` at or above `startDir`, or null. */
|
|
4710
|
+
function findUp(startDir, relativePath) {
|
|
4711
|
+
let dir = path.resolve(startDir);
|
|
4712
|
+
for (;;) {
|
|
4713
|
+
const candidate = path.join(dir, relativePath);
|
|
4714
|
+
if (existsSync(candidate)) return candidate;
|
|
4715
|
+
const parent = path.dirname(dir);
|
|
4716
|
+
if (parent === dir) return null;
|
|
4717
|
+
dir = parent;
|
|
4718
|
+
}
|
|
4719
|
+
}
|
|
4720
|
+
/** Groups files by a key derived from each; files whose key is null are dropped. */
|
|
4721
|
+
function groupBy(filePaths, keyOf) {
|
|
4722
|
+
const groups = /* @__PURE__ */ new Map();
|
|
4723
|
+
for (const filePath of filePaths) {
|
|
4724
|
+
const key = keyOf(filePath);
|
|
4725
|
+
if (!key) continue;
|
|
4726
|
+
groups.set(key, [...groups.get(key) ?? [], filePath]);
|
|
4727
|
+
}
|
|
4728
|
+
return groups;
|
|
4729
|
+
}
|
|
4730
|
+
const hasExtension = (extensions) => (filePath) => extensions.has(path.extname(filePath));
|
|
4731
|
+
function parseTscOutput(stdout, cwd) {
|
|
4732
|
+
return stdout.split("\n").flatMap((line) => {
|
|
4733
|
+
const match = TSC_ERROR_LINE.exec(line.trimEnd());
|
|
4734
|
+
if (!match) return [];
|
|
4735
|
+
const [, file, lineNumber, column, code, message] = match;
|
|
4736
|
+
return [{
|
|
4737
|
+
filePath: path.resolve(cwd, file),
|
|
4738
|
+
line: Number(lineNumber),
|
|
4739
|
+
column: Number(column),
|
|
4740
|
+
code,
|
|
4741
|
+
message
|
|
4742
|
+
}];
|
|
4743
|
+
});
|
|
4744
|
+
}
|
|
4745
|
+
function isEslintReport(value) {
|
|
4746
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "object" && entry !== null && typeof entry.filePath === "string" && Array.isArray(entry.messages));
|
|
4747
|
+
}
|
|
4748
|
+
function parseEslintOutput(stdout) {
|
|
4749
|
+
let report;
|
|
4750
|
+
try {
|
|
4751
|
+
report = JSON.parse(stdout);
|
|
4752
|
+
} catch {
|
|
4753
|
+
logger.debug(`[diagnostics] eslint produced non-JSON output: ${stdout.slice(0, 200)}`);
|
|
4754
|
+
return [];
|
|
4755
|
+
}
|
|
4756
|
+
if (!isEslintReport(report)) return [];
|
|
4757
|
+
return report.flatMap((fileResult) => fileResult.messages.filter((message) => message.severity === ESLINT_ERROR_SEVERITY).map((message) => ({
|
|
4758
|
+
filePath: path.resolve(fileResult.filePath),
|
|
4759
|
+
line: message.line ?? 1,
|
|
4760
|
+
column: message.column ?? 1,
|
|
4761
|
+
code: message.ruleId ?? "eslint",
|
|
4762
|
+
message: message.message
|
|
4763
|
+
})));
|
|
4764
|
+
}
|
|
4765
|
+
/**
|
|
4766
|
+
* Type-checks each changed file's nearest tsconfig project with the project's
|
|
4767
|
+
* own `tsc` (never a global one). Project-wide so path aliases and compiler
|
|
4768
|
+
* options are honored; output is filtered to the changed files by the caller.
|
|
4769
|
+
*/
|
|
4770
|
+
const checkTypeScript = async (filePaths) => {
|
|
4771
|
+
const projects = groupBy(filePaths.filter(hasExtension(TYPESCRIPT_EXTENSIONS)), (filePath) => findUp(path.dirname(filePath), "tsconfig.json"));
|
|
4772
|
+
const diagnostics = [];
|
|
4773
|
+
for (const tsconfigPath of projects.keys()) {
|
|
4774
|
+
const projectDir = path.dirname(tsconfigPath);
|
|
4775
|
+
const tsc = findUp(projectDir, TSC_ENTRY);
|
|
4776
|
+
if (!tsc) {
|
|
4777
|
+
logger.debug(`[diagnostics] no local tsc found for ${tsconfigPath}; skipping type check`);
|
|
4778
|
+
continue;
|
|
4779
|
+
}
|
|
4780
|
+
const outcome = await runNodeScript(tsc, [
|
|
4781
|
+
"--noEmit",
|
|
4782
|
+
"--pretty",
|
|
4783
|
+
"false",
|
|
4784
|
+
"-p",
|
|
4785
|
+
tsconfigPath
|
|
4786
|
+
], projectDir);
|
|
4787
|
+
if (outcome.kind === "failed") {
|
|
4788
|
+
logger.debug(`[diagnostics] tsc skipped for ${tsconfigPath}: ${outcome.reason}`);
|
|
4789
|
+
continue;
|
|
4790
|
+
}
|
|
4791
|
+
diagnostics.push(...parseTscOutput(outcome.stdout, projectDir));
|
|
4792
|
+
}
|
|
4793
|
+
return diagnostics;
|
|
4794
|
+
};
|
|
4795
|
+
/** Lints changed files with the eslint installed nearest to each file, run from that package root. */
|
|
4796
|
+
const checkEslint = async (filePaths) => {
|
|
4797
|
+
const groups = groupBy(filePaths.filter(hasExtension(ESLINT_EXTENSIONS)), (filePath) => findUp(path.dirname(filePath), ESLINT_ENTRY));
|
|
4798
|
+
const diagnostics = [];
|
|
4799
|
+
for (const [eslint, files] of groups) {
|
|
4800
|
+
const packageRoot = path.resolve(path.dirname(eslint), "..", "..", "..");
|
|
4801
|
+
const outcome = await runNodeScript(eslint, [
|
|
4802
|
+
"--format",
|
|
4803
|
+
"json",
|
|
4804
|
+
...files
|
|
4805
|
+
], packageRoot);
|
|
4806
|
+
if (outcome.kind === "failed") {
|
|
4807
|
+
logger.debug(`[diagnostics] eslint skipped under ${packageRoot}: ${outcome.reason}`);
|
|
4808
|
+
continue;
|
|
4809
|
+
}
|
|
4810
|
+
diagnostics.push(...parseEslintOutput(outcome.stdout));
|
|
4811
|
+
}
|
|
4812
|
+
return diagnostics;
|
|
4813
|
+
};
|
|
4814
|
+
/** The default checker: tsc and eslint side by side. */
|
|
4815
|
+
const checkTypeScriptAndEslint = async (filePaths) => {
|
|
4816
|
+
const [typeErrors, lintErrors] = await Promise.all([checkTypeScript(filePaths), checkEslint(filePaths)]);
|
|
4817
|
+
return [...typeErrors, ...lintErrors];
|
|
4818
|
+
};
|
|
4819
|
+
//#endregion
|
|
4820
|
+
//#region src/diagnostics/PostEditDiagnostics.ts
|
|
4821
|
+
/**
|
|
4822
|
+
* How long a drain waits for in-flight checks (ms). Between turns the model
|
|
4823
|
+
* will take another turn anyway, so a busy worker just defers to the next
|
|
4824
|
+
* one; at finalization there is no later turn, so it waits longer.
|
|
4825
|
+
*/
|
|
4826
|
+
const DRAIN_BUDGET_MS = {
|
|
4827
|
+
turn: 250,
|
|
4828
|
+
final: 3e4
|
|
4829
|
+
};
|
|
4830
|
+
const REPORT_HEADER = "[Post-edit diagnostics] Errors in files you changed. Fix them before you finish:";
|
|
4831
|
+
const MAX_REPORTED_ERRORS = 20;
|
|
4832
|
+
const MAX_MESSAGE_LENGTH = 300;
|
|
4833
|
+
/**
|
|
4834
|
+
* Background type/lint checking of files the agent edits, drained into the
|
|
4835
|
+
* agent loop via AgentRunOptions.drainFeedback. Edits never wait on a check.
|
|
4836
|
+
*
|
|
4837
|
+
* Queue semantics are re-edit-wins: a file edited again before its check runs
|
|
4838
|
+
* is checked once in its final state, and a file edited again after a check
|
|
4839
|
+
* started (in flight or already completed but not yet reported) has that stale
|
|
4840
|
+
* result discarded in favor of the queued re-check. Each completed result is
|
|
4841
|
+
* reported at most once.
|
|
4842
|
+
*
|
|
4843
|
+
* Only collects between beginTurn() and endTurn(); a no-op otherwise, which is
|
|
4844
|
+
* how the per-turn config gate in turnController switches it off.
|
|
4845
|
+
*/
|
|
4846
|
+
var PostEditDiagnostics = class {
|
|
4847
|
+
constructor({ workspaceRoot, check = checkTypeScriptAndEslint }) {
|
|
4848
|
+
this.pending = /* @__PURE__ */ new Set();
|
|
4849
|
+
this.results = /* @__PURE__ */ new Map();
|
|
4850
|
+
this.worker = null;
|
|
4851
|
+
this.active = false;
|
|
4852
|
+
this.generation = 0;
|
|
4853
|
+
this.workspaceRoot = workspaceRoot;
|
|
4854
|
+
this.check = check;
|
|
4855
|
+
}
|
|
4856
|
+
beginTurn() {
|
|
4857
|
+
this.reset();
|
|
4858
|
+
this.active = true;
|
|
4859
|
+
}
|
|
4860
|
+
endTurn() {
|
|
4861
|
+
this.reset();
|
|
4862
|
+
this.active = false;
|
|
4863
|
+
}
|
|
4864
|
+
enqueue(filePath) {
|
|
4865
|
+
if (!this.active) return;
|
|
4866
|
+
const resolved = path.resolve(this.workspaceRoot, filePath);
|
|
4867
|
+
this.results.delete(resolved);
|
|
4868
|
+
this.pending.add(resolved);
|
|
4869
|
+
this.worker ??= this.processQueue();
|
|
4870
|
+
}
|
|
4871
|
+
/**
|
|
4872
|
+
* Waits up to `budgetMs` (or until `signal` aborts) for queued checks, then
|
|
4873
|
+
* returns the errors found since the last drain, or null when there are none.
|
|
4874
|
+
*/
|
|
4875
|
+
async drain(budgetMs, signal) {
|
|
4876
|
+
if (this.worker) await settleWithin(this.worker, budgetMs, signal);
|
|
4877
|
+
return this.takeReport();
|
|
4878
|
+
}
|
|
4879
|
+
reset() {
|
|
4880
|
+
this.generation++;
|
|
4881
|
+
this.pending.clear();
|
|
4882
|
+
this.results.clear();
|
|
4883
|
+
}
|
|
4884
|
+
async processQueue() {
|
|
4885
|
+
try {
|
|
4886
|
+
while (this.pending.size > 0) {
|
|
4887
|
+
const batch = [...this.pending];
|
|
4888
|
+
this.pending.clear();
|
|
4889
|
+
const generation = this.generation;
|
|
4890
|
+
const diagnostics = await this.runCheck(batch);
|
|
4891
|
+
if (!diagnostics || generation !== this.generation) continue;
|
|
4892
|
+
for (const filePath of batch) {
|
|
4893
|
+
if (this.pending.has(filePath)) continue;
|
|
4894
|
+
this.results.set(filePath, diagnostics.filter((diagnostic) => diagnostic.filePath === filePath));
|
|
4895
|
+
}
|
|
4896
|
+
}
|
|
4897
|
+
} finally {
|
|
4898
|
+
this.worker = null;
|
|
4899
|
+
}
|
|
4900
|
+
}
|
|
4901
|
+
async runCheck(batch) {
|
|
4902
|
+
try {
|
|
4903
|
+
return await this.check(batch);
|
|
4904
|
+
} catch (error) {
|
|
4905
|
+
logger.debug(`[diagnostics] check failed; skipping ${batch.length} file(s): ${String(error)}`);
|
|
4906
|
+
return null;
|
|
4907
|
+
}
|
|
4908
|
+
}
|
|
4909
|
+
takeReport() {
|
|
4910
|
+
const errors = [...this.results.values()].flat();
|
|
4911
|
+
this.results.clear();
|
|
4912
|
+
if (errors.length === 0) return null;
|
|
4913
|
+
const lines = errors.slice(0, MAX_REPORTED_ERRORS).map((error) => this.formatError(error));
|
|
4914
|
+
const omitted = errors.length - lines.length;
|
|
4915
|
+
if (omitted > 0) lines.push(`(+${omitted} more)`);
|
|
4916
|
+
return [REPORT_HEADER, ...lines].join("\n");
|
|
4917
|
+
}
|
|
4918
|
+
formatError({ filePath, line, column, code, message }) {
|
|
4919
|
+
const relativePath = path.relative(this.workspaceRoot, filePath).split(path.sep).join("/");
|
|
4920
|
+
const firstLine = message.split("\n")[0];
|
|
4921
|
+
return `${relativePath}:${line}:${column} - ${code}: ${firstLine.length > MAX_MESSAGE_LENGTH ? `${firstLine.slice(0, MAX_MESSAGE_LENGTH)}...` : firstLine}`;
|
|
4922
|
+
}
|
|
4923
|
+
};
|
|
4924
|
+
/** Resolves when `work` settles, `budgetMs` elapses, or `signal` aborts - whichever is first. */
|
|
4925
|
+
async function settleWithin(work, budgetMs, signal) {
|
|
4926
|
+
if (signal?.aborted) return;
|
|
4927
|
+
let timer;
|
|
4928
|
+
let onAbort;
|
|
4929
|
+
const cutoff = new Promise((resolve) => {
|
|
4930
|
+
timer = setTimeout(resolve, budgetMs);
|
|
4931
|
+
onAbort = resolve;
|
|
4932
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
4933
|
+
});
|
|
4934
|
+
try {
|
|
4935
|
+
await Promise.race([work, cutoff]);
|
|
4936
|
+
} finally {
|
|
4937
|
+
clearTimeout(timer);
|
|
4938
|
+
if (onAbort) signal?.removeEventListener("abort", onAbort);
|
|
4939
|
+
}
|
|
4940
|
+
}
|
|
4941
|
+
//#endregion
|
|
4520
4942
|
//#region src/features/FeatureModuleRegistry.ts
|
|
4521
4943
|
/**
|
|
4522
4944
|
* Manages the lifecycle of opt-in CLI feature modules.
|
|
@@ -7685,7 +8107,7 @@ async function handleSandboxTrustDomainCommand(deps, domains) {
|
|
|
7685
8107
|
* recurses with the same context for messages submitted mid-turn.
|
|
7686
8108
|
*/
|
|
7687
8109
|
async function runTurn(message, ctx) {
|
|
7688
|
-
const { agent, sessionStore, configStore, commandHistoryStore, customCommandStore, messageBuilder, config, availableModels, agentStore, contextContent, additionalDirectories, featureRegistry, backgroundManager, todoStore, decisionStore, blockerStore, workflowStores, setCommandHistory, setAbortController } = ctx;
|
|
8110
|
+
const { agent, sessionStore, configStore, commandHistoryStore, customCommandStore, messageBuilder, config, availableModels, agentStore, contextContent, additionalDirectories, featureRegistry, backgroundManager, todoStore, decisionStore, blockerStore, postEditDiagnostics, workflowStores, setCommandHistory, setAbortController } = ctx;
|
|
7689
8111
|
const storeSession = useCliStore.getState().session;
|
|
7690
8112
|
if (!agent || !storeSession) {
|
|
7691
8113
|
console.error("❌ CLI failed to initialize. Try restarting b4m.\n");
|
|
@@ -7790,6 +8212,8 @@ async function runTurn(message, ctx) {
|
|
|
7790
8212
|
if (elided > 0) logger.debug(`[workflowReminder] elided ${elided} item(s) to fit the token cap`);
|
|
7791
8213
|
return text;
|
|
7792
8214
|
} : void 0;
|
|
8215
|
+
const diagnostics = cliConfig.preferences.postEditDiagnostics === true ? postEditDiagnostics : null;
|
|
8216
|
+
diagnostics?.beginTurn();
|
|
7793
8217
|
let result;
|
|
7794
8218
|
try {
|
|
7795
8219
|
result = await agent.run(messageContent, {
|
|
@@ -7798,9 +8222,11 @@ async function runTurn(message, ctx) {
|
|
|
7798
8222
|
parallelExecution: cliConfig.preferences.enableParallelToolExecution === true,
|
|
7799
8223
|
isReadOnlyTool,
|
|
7800
8224
|
workflowReminder,
|
|
8225
|
+
drainFeedback: diagnostics ? (phase) => diagnostics.drain(DRAIN_BUDGET_MS[phase], abortController.signal) : void 0,
|
|
7801
8226
|
onContextLimit: createReactiveCompactionHandler(agent, activeSession, 1 + previousMessages.length + 1)
|
|
7802
8227
|
});
|
|
7803
8228
|
} finally {
|
|
8229
|
+
diagnostics?.endTurn();
|
|
7804
8230
|
backgroundManager?.setCurrentTurn(null);
|
|
7805
8231
|
}
|
|
7806
8232
|
const permissionDenied = result.finalAnswer.startsWith("Permission denied for tool");
|
|
@@ -8101,6 +8527,22 @@ async function rewindSession(ctx, messageIndex) {
|
|
|
8101
8527
|
return { prefill: prefillContent };
|
|
8102
8528
|
}
|
|
8103
8529
|
//#endregion
|
|
8530
|
+
//#region src/session/modelOverride.ts
|
|
8531
|
+
async function runWithModelOverride(ctx, overrideModel, run) {
|
|
8532
|
+
const originalModel = useCliStore.getState().session?.model;
|
|
8533
|
+
ctx.applyModel(overrideModel);
|
|
8534
|
+
try {
|
|
8535
|
+
return await run();
|
|
8536
|
+
} finally {
|
|
8537
|
+
if (originalModel) await restoreModel(ctx, originalModel);
|
|
8538
|
+
}
|
|
8539
|
+
}
|
|
8540
|
+
async function restoreModel(ctx, originalModel) {
|
|
8541
|
+
ctx.applyModel(originalModel);
|
|
8542
|
+
const restoredSession = useCliStore.getState().session;
|
|
8543
|
+
if (restoredSession) await ctx.sessionStore.save(restoredSession);
|
|
8544
|
+
}
|
|
8545
|
+
//#endregion
|
|
8104
8546
|
//#region src/index.tsx
|
|
8105
8547
|
process.removeAllListeners("warning");
|
|
8106
8548
|
process.on("warning", (warning) => {
|
|
@@ -8200,6 +8642,7 @@ function CliApp() {
|
|
|
8200
8642
|
permissionPrompt: null,
|
|
8201
8643
|
trustLocationSelector: null,
|
|
8202
8644
|
folderTrustPrompt: null,
|
|
8645
|
+
mcpApprovalPrompt: null,
|
|
8203
8646
|
rewindSelector: null,
|
|
8204
8647
|
sessionSelector: null,
|
|
8205
8648
|
orchestrator: null,
|
|
@@ -8219,10 +8662,12 @@ function CliApp() {
|
|
|
8219
8662
|
const [commandHistory, setCommandHistory] = useState([]);
|
|
8220
8663
|
const imageStoreInitPromise = useRef(null);
|
|
8221
8664
|
const folderTrustResolvedRef = useRef(false);
|
|
8665
|
+
const mcpApprovalResolvedRef = useRef(false);
|
|
8222
8666
|
const todoStoreRef = useRef(createTodoStore());
|
|
8223
8667
|
const decisionStoreRef = useRef(createDecisionStore());
|
|
8224
8668
|
const blockerStoreRef = useRef(createBlockerStore());
|
|
8225
8669
|
const reviewGateStoreRef = useRef(createReviewGateStore());
|
|
8670
|
+
const postEditDiagnosticsRef = useRef(new PostEditDiagnostics({ workspaceRoot: process.cwd() }));
|
|
8226
8671
|
const setStoreSession = useCliStore((state) => state.setSession);
|
|
8227
8672
|
const enqueuePermissionPrompt = useCliStore((state) => state.enqueuePermissionPrompt);
|
|
8228
8673
|
const enqueueUserQuestionPrompt = useCliStore((state) => state.enqueueUserQuestionPrompt);
|
|
@@ -8334,6 +8779,20 @@ function CliApp() {
|
|
|
8334
8779
|
}));
|
|
8335
8780
|
return;
|
|
8336
8781
|
}
|
|
8782
|
+
const pendingMcp = state.configStore.getPendingMcpApprovals();
|
|
8783
|
+
if (pendingMcp.length > 0 && !mcpApprovalResolvedRef.current && Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY)) {
|
|
8784
|
+
mcpApprovalResolvedRef.current = true;
|
|
8785
|
+
setState((prev) => ({
|
|
8786
|
+
...prev,
|
|
8787
|
+
mcpApprovalPrompt: {
|
|
8788
|
+
projectRoot: state.configStore.getProjectRealPath() ?? process.cwd(),
|
|
8789
|
+
servers: pendingMcp
|
|
8790
|
+
},
|
|
8791
|
+
config
|
|
8792
|
+
}));
|
|
8793
|
+
return;
|
|
8794
|
+
}
|
|
8795
|
+
if (!mcpApprovalResolvedRef.current) state.configStore.warnPendingMcpApprovals();
|
|
8337
8796
|
const configDirs = await state.configStore.getAdditionalDirectories();
|
|
8338
8797
|
const flagDirs = process.env.B4M_ADDITIONAL_DIRS ? JSON.parse(process.env.B4M_ADDITIONAL_DIRS) : [];
|
|
8339
8798
|
const additionalDirectories = [.../* @__PURE__ */ new Set([...configDirs, ...flagDirs])];
|
|
@@ -8518,7 +8977,8 @@ function CliApp() {
|
|
|
8518
8977
|
};
|
|
8519
8978
|
const agentContext = {
|
|
8520
8979
|
currentAgent: null,
|
|
8521
|
-
observationQueue: []
|
|
8980
|
+
observationQueue: [],
|
|
8981
|
+
onFileChanged: (filePath) => postEditDiagnosticsRef.current.enqueue(filePath)
|
|
8522
8982
|
};
|
|
8523
8983
|
const { mcpManager, agentStore, contextResult, mcpTools, loadedB4mTools, deferredB4mTools, orchestrator, backgroundManager, historyStore } = await buildSupportingStores({
|
|
8524
8984
|
config,
|
|
@@ -9028,6 +9488,7 @@ function CliApp() {
|
|
|
9028
9488
|
todoStore: todoStoreRef.current,
|
|
9029
9489
|
decisionStore: decisionStoreRef.current,
|
|
9030
9490
|
blockerStore: blockerStoreRef.current,
|
|
9491
|
+
postEditDiagnostics: postEditDiagnosticsRef.current,
|
|
9031
9492
|
workflowStores: {
|
|
9032
9493
|
decisionStore: decisionStoreRef.current,
|
|
9033
9494
|
blockerStore: blockerStoreRef.current,
|
|
@@ -9497,11 +9958,10 @@ function CliApp() {
|
|
|
9497
9958
|
}
|
|
9498
9959
|
if (customCommand.model && state.agent) {
|
|
9499
9960
|
console.log(`🔄 Using model override: ${customCommand.model}`);
|
|
9500
|
-
|
|
9501
|
-
|
|
9502
|
-
|
|
9503
|
-
|
|
9504
|
-
if (overrideSession && originalModel) overrideSession.model = originalModel;
|
|
9961
|
+
await runWithModelOverride({
|
|
9962
|
+
applyModel: applyModelToSession,
|
|
9963
|
+
sessionStore: state.sessionStore
|
|
9964
|
+
}, customCommand.model, () => handleCustomCommandMessage(substitutedBody, displayMessage));
|
|
9505
9965
|
} else {
|
|
9506
9966
|
console.log("🤖 Sending to agent...\n");
|
|
9507
9967
|
await handleCustomCommandMessage(substitutedBody, displayMessage);
|
|
@@ -10664,6 +11124,34 @@ function CliApp() {
|
|
|
10664
11124
|
})();
|
|
10665
11125
|
}
|
|
10666
11126
|
});
|
|
11127
|
+
if (state.mcpApprovalPrompt) {
|
|
11128
|
+
const { projectRoot, servers } = state.mcpApprovalPrompt;
|
|
11129
|
+
return /* @__PURE__ */ React.createElement(McpApprovalPrompt, {
|
|
11130
|
+
projectRoot,
|
|
11131
|
+
servers,
|
|
11132
|
+
onSelect: (choice) => {
|
|
11133
|
+
(async () => {
|
|
11134
|
+
try {
|
|
11135
|
+
if (choice === "approve") {
|
|
11136
|
+
await state.configStore.approveMcpServers(servers);
|
|
11137
|
+
console.log("\nApproved. Starting these MCP servers; a changed definition will ask again.\n");
|
|
11138
|
+
} else console.log("\nThese MCP servers stay off this session. You will be asked again next launch.\n");
|
|
11139
|
+
} catch (err) {
|
|
11140
|
+
console.error("\nCould not save MCP approval:", err instanceof Error ? err.message : String(err), "\n");
|
|
11141
|
+
} finally {
|
|
11142
|
+
setState((prev) => ({
|
|
11143
|
+
...prev,
|
|
11144
|
+
mcpApprovalPrompt: null
|
|
11145
|
+
}));
|
|
11146
|
+
init().catch((err) => {
|
|
11147
|
+
console.error("\nInitialization failed:", err instanceof Error ? err.message : String(err), "\n");
|
|
11148
|
+
exit();
|
|
11149
|
+
});
|
|
11150
|
+
}
|
|
11151
|
+
})();
|
|
11152
|
+
}
|
|
11153
|
+
});
|
|
11154
|
+
}
|
|
10667
11155
|
const currentSession = useCliStore.getState().session;
|
|
10668
11156
|
if (state.rewindSelector && currentSession) return /* @__PURE__ */ React.createElement(RewindSelector, {
|
|
10669
11157
|
messages: currentSession.messages,
|