nolo-cli 0.8.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_executeModel-UD44KGZ4.js +203 -0
- package/action-ZJWE3Y5S.js +28 -0
- package/addContentAction-TVDBNJM5.js +26 -0
- package/addDialogAgentAction-EKH774VT.js +68 -0
- package/agentDeleteCommand-TCPEFJTF.js +327 -0
- package/agentEmailCommands-CZ3UILZ4.js +626 -0
- package/agentListCommands-SJKO2FFB.js +223 -0
- package/agentMachineCommands-PSNABISZ.js +425 -0
- package/agentPullCommand-O2MTUAFR.js +122 -0
- package/agentRecordCommands-MH2JKFHS.js +94 -0
- package/agentRunCommand-4KH6MIZR.js +110 -0
- package/agentRunControl-K4VKNEHF.js +57 -0
- package/agentRuntimeLocal-SKHJUWSS.js +104 -0
- package/agentSlice-3WXXN4VK.js +42 -0
- package/authCommand-PXFX6JDT.js +581 -0
- package/authCommands-356AYWEK.js +35 -0
- package/chunk-2EISRLRC.js +238 -0
- package/chunk-2FTTDCD4.js +675 -0
- package/chunk-2K2ECKSG.js +222 -0
- package/chunk-2PUGVR6E.js +19 -0
- package/chunk-3ASDTTEI.js +1774 -0
- package/chunk-3EUW3WEO.js +8 -0
- package/chunk-3LY4F7SV.js +9293 -0
- package/chunk-3TZDXYSF.js +71 -0
- package/chunk-3WYQEUBI.js +34 -0
- package/chunk-4CQLVGRV.js +50 -0
- package/chunk-4IJYA27C.js +342 -0
- package/chunk-4K2KKCSD.js +577 -0
- package/chunk-4SN6J65J.js +38 -0
- package/chunk-4VNS5WPM.js +42 -0
- package/chunk-4ZDM73SD.js +34 -0
- package/chunk-5CS33IBL.js +301 -0
- package/chunk-5KLGBAAR.js +386 -0
- package/chunk-5MIDPSSK.js +206 -0
- package/chunk-5MMAK75Q.js +15 -0
- package/chunk-5PJ3VWCS.js +360 -0
- package/chunk-63SLEOLK.js +175 -0
- package/chunk-63W5N6P3.js +150 -0
- package/chunk-6GANEXVO.js +231 -0
- package/chunk-6HYVJV7B.js +102 -0
- package/chunk-6WD3OOJS.js +20 -0
- package/chunk-7OOXPE3N.js +628 -0
- package/chunk-7QRKCBOP.js +1078 -0
- package/chunk-ATNTHIZI.js +13 -0
- package/chunk-AUQ747NZ.js +902 -0
- package/chunk-AVMKZHBC.js +85 -0
- package/chunk-AZPCASRE.js +36 -0
- package/chunk-BBYVOSU2.js +792 -0
- package/chunk-C3RUNRHT.js +51 -0
- package/chunk-CF2WZF4N.js +134 -0
- package/chunk-CF7PE763.js +10 -0
- package/chunk-CKOJQDKR.js +198 -0
- package/chunk-COMXDWNM.js +807 -0
- package/chunk-CPB2YM4D.js +171 -0
- package/chunk-CWMSB67P.js +34 -0
- package/chunk-DCOA4QSN.js +105 -0
- package/chunk-DFFIZ25G.js +30 -0
- package/chunk-DWLEOOGR.js +8 -0
- package/chunk-DZ3XJVET.js +231 -0
- package/chunk-EHLHSX7R.js +1433 -0
- package/chunk-ELRRLDWI.js +478 -0
- package/chunk-EXTIQUCI.js +482 -0
- package/chunk-EYEEHCU4.js +3066 -0
- package/chunk-G3SUCT4W.js +8 -0
- package/chunk-G43KEVSC.js +1447 -0
- package/chunk-GAKEBQD6.js +30 -0
- package/chunk-GBRG75BQ.js +86 -0
- package/chunk-GG4AJPSR.js +191 -0
- package/chunk-H33743XH.js +25 -0
- package/chunk-HNMXYYJR.js +12 -0
- package/chunk-I42VC5KR.js +15 -0
- package/chunk-IDZTNRP2.js +85 -0
- package/chunk-ISNCLGUZ.js +146 -0
- package/chunk-IWX4UW2E.js +548 -0
- package/chunk-JBTJAPUF.js +35 -0
- package/chunk-JEXVIMYZ.js +55 -0
- package/chunk-JRKKFRAV.js +537 -0
- package/chunk-JZPBVBIY.js +94 -0
- package/chunk-KKY4B2Q2.js +21 -0
- package/chunk-KOCA5FCS.js +4750 -0
- package/chunk-KQHGZWSV.js +63 -0
- package/chunk-KTVVPRJS.js +138 -0
- package/chunk-KUG52IAP.js +8 -0
- package/chunk-KX6YBIHG.js +8 -0
- package/chunk-LARL2V6G.js +9017 -0
- package/chunk-LCBAMY2M.js +60 -0
- package/chunk-LM6IYQ3F.js +16 -0
- package/chunk-M76ECQRD.js +13658 -0
- package/chunk-MGOJ5R6J.js +113 -0
- package/chunk-MKMWT7SA.js +27196 -0
- package/chunk-MRC4Q34M.js +188 -0
- package/chunk-OLZBQWZU.js +6 -0
- package/chunk-OPZYGHUC.js +89 -0
- package/chunk-OWOHNGCN.js +50 -0
- package/chunk-PAKN26FP.js +239 -0
- package/chunk-PCWC3JTN.js +91 -0
- package/chunk-PG5PNA7Y.js +939 -0
- package/chunk-Q2YZULBH.js +104 -0
- package/chunk-QSJ3HFEF.js +8 -0
- package/chunk-QYXLLLGL.js +4519 -0
- package/chunk-ROWQEJRP.js +139 -0
- package/chunk-S7Z2HMCN.js +25 -0
- package/chunk-SBR32ZZS.js +1901 -0
- package/chunk-SH2CFQOH.js +3391 -0
- package/chunk-SL35K7AH.js +226 -0
- package/chunk-TILFE6LV.js +381 -0
- package/chunk-TSS3XJY4.js +2052 -0
- package/chunk-U4TRT5V4.js +246 -0
- package/chunk-U6VC4MU4.js +8 -0
- package/chunk-UAD2WT54.js +64 -0
- package/chunk-UUJDGGAK.js +421 -0
- package/chunk-VF4K2QE6.js +21 -0
- package/chunk-VHBWHNVT.js +59 -0
- package/chunk-VIDPVFP3.js +20 -0
- package/chunk-VP4WJWKW.js +8882 -0
- package/chunk-W4NVOSFF.js +37 -0
- package/chunk-WKM6FH35.js +190 -0
- package/chunk-WWC7I6TA.js +8 -0
- package/chunk-X7W4TNB4.js +1550 -0
- package/chunk-XE635NI4.js +1948 -0
- package/chunk-XKIMRZO2.js +221 -0
- package/chunk-XS466O7S.js +933 -0
- package/chunk-Y5GCAMGP.js +9 -0
- package/chunk-YBWOP46I.js +3272 -0
- package/chunk-YPRZSPF3.js +148 -0
- package/chunk-YYUCRDOK.js +69 -0
- package/chunk-Z4NR32XR.js +50 -0
- package/chunk-ZIG5LJ7X.js +24765 -0
- package/chunk-ZNFJWEN4.js +56 -0
- package/chunk-ZQ2UM4KG.js +2878 -0
- package/cliExecutor-DNE7TCC4.js +36 -0
- package/clipboardy-BVE3LVEH.js +12001 -0
- package/createAgentAutomationAction-63OIP6X5.js +866 -0
- package/createDialogAction-ZLYZDYBB.js +54 -0
- package/createPageAction-AL5FSDCK.js +171 -0
- package/db-HCJ5PRB5.js +271 -0
- package/dbSlice-PR44QPXY.js +60 -0
- package/deleteFile-4SKGPHOZ.js +41 -0
- package/dialogCommands-F6YI5742.js +63 -0
- package/dialogSlice-JSBNZRLR.js +148 -0
- package/docCreateCommands-4CTO7ERT.js +219 -0
- package/docDeleteCommands-VBWERDRG.js +180 -0
- package/docReadCommands-KNP4MFYB.js +133 -0
- package/docStore-ADP5VYY4.js +98 -0
- package/docUpdateCommands-4BCBJQHA.js +317 -0
- package/fetchMessages-U6ZHN56A.js +17 -0
- package/getPricing-3E6UNPKL.js +30 -0
- package/handleSendMessageAction-IJDQ6ARA.js +185 -0
- package/index.js +188 -112943
- package/lib-FYYBQIXS.js +19839 -0
- package/localDialogRead-Z5UTAJPU.js +26 -0
- package/localFirstLog-DHNOY2NA.js +19 -0
- package/localRuntimeAuthority-TEGCOWX4.js +471 -0
- package/localRuntimeDb-LGDF6FQP.js +12 -0
- package/machineCommands-EA3M4XTN.js +779 -0
- package/machineInfo-M6KB6HZN.js +10 -0
- package/machineWsRunDispatch-NAPKE5ZA.js +110 -0
- package/memoryCommands-O4MIPIWQ.js +476 -0
- package/models-4OPOOLFS.js +9 -0
- package/offlineMarxistsAgentCommand-BCPJITJL.js +242 -0
- package/package.json +3 -25
- package/read-RIJCIVKX.js +17 -0
- package/readDocTool-GZD2N3LO.js +16 -0
- package/readlineWorkspace-KMWT4T4S.js +5509 -0
- package/removeDialogAgentAction-GJQJNDW6.js +73 -0
- package/runAgentClientLoop-U5Q6NDTJ.js +291 -0
- package/runtimeDoctorCommands-LRHLPDRC.js +289 -0
- package/setDialogExtraReferencesAction-M7OQG54N.js +60 -0
- package/setPrimaryDialogAgentAction-46UNFJDL.js +80 -0
- package/settingSlice-FVUUH4JA.js +199 -0
- package/skillSummaryMarker-42HKSXYY.js +9 -0
- package/slateToRenderMarkdown-Y5IYN73U.js +7 -0
- package/slateUtils-2M7MUTWX.js +19 -0
- package/spaceCommands-E2CFRPZJ.js +454 -0
- package/spaceSlice-L3R6IMXE.js +147 -0
- package/streamAgentChatTurn-UBDAOQWE.js +4528 -0
- package/tableCommands-GOFP7QKA.js +1286 -0
- package/toolApiClient-QI3YBNWB.js +26 -0
- package/tools-TMJXZTCA.js +85 -0
- package/tuiSummaryLlmCaller-RV3THLZF.js +78 -0
- package/understanding-GQKNDDWM.js +271 -0
- package/updateCommands-VOSPKXFF.js +31 -0
- package/updateDialogTitleAction-ZTWRDXI2.js +188 -0
|
@@ -0,0 +1,3272 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyChatCompletionDelta,
|
|
3
|
+
finalizeAccumulatedToolCalls,
|
|
4
|
+
flushChatCompletionStream,
|
|
5
|
+
readSseFrames,
|
|
6
|
+
throwIfChatCompletionStreamFailed,
|
|
7
|
+
toOpenAiCompatibleMessages
|
|
8
|
+
} from "./chunk-5KLGBAAR.js";
|
|
9
|
+
import {
|
|
10
|
+
parseToolArgumentsJson
|
|
11
|
+
} from "./chunk-XS466O7S.js";
|
|
12
|
+
import {
|
|
13
|
+
createThinkParserState,
|
|
14
|
+
extractThinkContent
|
|
15
|
+
} from "./chunk-OPZYGHUC.js";
|
|
16
|
+
import {
|
|
17
|
+
buildProviderAuthHeaders,
|
|
18
|
+
buildProviderExecutionPlan
|
|
19
|
+
} from "./chunk-JRKKFRAV.js";
|
|
20
|
+
import {
|
|
21
|
+
canonicalizeToolName,
|
|
22
|
+
createGlob,
|
|
23
|
+
resolveExecutableOnPath
|
|
24
|
+
} from "./chunk-AUQ747NZ.js";
|
|
25
|
+
import {
|
|
26
|
+
toErrorMessage
|
|
27
|
+
} from "./chunk-HNMXYYJR.js";
|
|
28
|
+
import {
|
|
29
|
+
asRecordOrEmpty
|
|
30
|
+
} from "./chunk-KX6YBIHG.js";
|
|
31
|
+
import {
|
|
32
|
+
dialogMessageKey
|
|
33
|
+
} from "./chunk-SH2CFQOH.js";
|
|
34
|
+
import {
|
|
35
|
+
asTrimmedNonEmptyStringArray
|
|
36
|
+
} from "./chunk-I42VC5KR.js";
|
|
37
|
+
import {
|
|
38
|
+
asOptionalFiniteNumber
|
|
39
|
+
} from "./chunk-WWC7I6TA.js";
|
|
40
|
+
import {
|
|
41
|
+
asTrimmedLowercaseString
|
|
42
|
+
} from "./chunk-QSJ3HFEF.js";
|
|
43
|
+
import {
|
|
44
|
+
asTrimmedString
|
|
45
|
+
} from "./chunk-3EUW3WEO.js";
|
|
46
|
+
import {
|
|
47
|
+
asOptionalPositiveFiniteNumber
|
|
48
|
+
} from "./chunk-U6VC4MU4.js";
|
|
49
|
+
import {
|
|
50
|
+
normalizeServerOrigin
|
|
51
|
+
} from "./chunk-CF7PE763.js";
|
|
52
|
+
import {
|
|
53
|
+
asOptionalTrimmedString
|
|
54
|
+
} from "./chunk-G3SUCT4W.js";
|
|
55
|
+
import {
|
|
56
|
+
isRecord
|
|
57
|
+
} from "./chunk-KUG52IAP.js";
|
|
58
|
+
|
|
59
|
+
// packages/agent-runtime/kimiUserAgent.ts
|
|
60
|
+
var KIMI_CODE_CLI_VERSION = "0.29.2";
|
|
61
|
+
var KIMI_CODE_USER_AGENT = `kimi-code-cli/${KIMI_CODE_CLI_VERSION} (nolo)`;
|
|
62
|
+
function isKimiEndpoint(url) {
|
|
63
|
+
return url.includes("api.kimi.com");
|
|
64
|
+
}
|
|
65
|
+
function kimiIdentityHeaders(url) {
|
|
66
|
+
return isKimiEndpoint(url) ? { "User-Agent": KIMI_CODE_USER_AGENT } : {};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// packages/agent-runtime/sseDataLine.ts
|
|
70
|
+
function parseSseDataLineJson(line) {
|
|
71
|
+
const trimmed = line.trim();
|
|
72
|
+
if (!trimmed.startsWith("data:")) return null;
|
|
73
|
+
const payload = trimmed.slice(5).trim();
|
|
74
|
+
if (!payload || payload === "[DONE]") return null;
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(payload);
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// packages/agent-runtime/openAiCompatibleProvider.ts
|
|
83
|
+
function buildOpenAiCompatibleChatCompletionRequest(args) {
|
|
84
|
+
const body = {
|
|
85
|
+
model: args.providerConfig.model,
|
|
86
|
+
messages: toOpenAiCompatibleMessages(args.messages),
|
|
87
|
+
stream: args.stream ?? false,
|
|
88
|
+
...args.providerConfig.requestOptions,
|
|
89
|
+
...args.tools && args.tools.length > 0 ? { tools: args.tools } : {},
|
|
90
|
+
...args.stream ? { stream_options: { include_usage: true } } : {}
|
|
91
|
+
};
|
|
92
|
+
const endpoint = args.providerConfig.endpoint;
|
|
93
|
+
const headers = {
|
|
94
|
+
"Content-Type": "application/json",
|
|
95
|
+
...buildProviderAuthHeaders({
|
|
96
|
+
endpoint,
|
|
97
|
+
apiKey: args.providerConfig.apiKey,
|
|
98
|
+
apiKeyHeader: args.providerConfig.apiKeyHeader
|
|
99
|
+
}),
|
|
100
|
+
...kimiIdentityHeaders(endpoint)
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
url: endpoint,
|
|
104
|
+
init: {
|
|
105
|
+
method: "POST",
|
|
106
|
+
headers,
|
|
107
|
+
body: JSON.stringify(body)
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function parseOpenAiCompatibleChatCompletionResponse(args) {
|
|
112
|
+
const choiceMessage = args.data?.choices?.[0]?.message ?? {};
|
|
113
|
+
const rawContent = String(choiceMessage?.content ?? "");
|
|
114
|
+
const { content, reasoning } = extractThinkContent(rawContent);
|
|
115
|
+
const rawFinishReason = args.data?.choices?.[0]?.finish_reason;
|
|
116
|
+
return {
|
|
117
|
+
content,
|
|
118
|
+
model: args.providerConfig.model,
|
|
119
|
+
provider: args.providerConfig.provider,
|
|
120
|
+
...Array.isArray(choiceMessage?.tool_calls) ? { tool_calls: choiceMessage.tool_calls } : {},
|
|
121
|
+
...reasoning ? { reasoning_content: reasoning } : {},
|
|
122
|
+
...typeof choiceMessage?.reasoning_content === "string" && choiceMessage.reasoning_content ? { reasoning_content: choiceMessage.reasoning_content } : {},
|
|
123
|
+
...typeof rawFinishReason === "string" && rawFinishReason.length > 0 ? { finish_reason: rawFinishReason } : {},
|
|
124
|
+
usage: args.data?.usage,
|
|
125
|
+
trace: args.trace
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function processOpenAiCompatibleSseEvent(event, state) {
|
|
129
|
+
for (const line of event.split("\n")) {
|
|
130
|
+
const parsed = parseSseDataLineJson(line);
|
|
131
|
+
if (parsed == null) continue;
|
|
132
|
+
applyChatCompletionDelta(parsed, state);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function readOpenAiCompatibleSseCompletion(args) {
|
|
136
|
+
const state = {
|
|
137
|
+
content: "",
|
|
138
|
+
reasoning: "",
|
|
139
|
+
usage: void 0,
|
|
140
|
+
accumulatedToolCalls: {},
|
|
141
|
+
thinkState: createThinkParserState(),
|
|
142
|
+
onTextDelta: args.onTextDelta,
|
|
143
|
+
onReasoningDelta: args.onReasoningDelta
|
|
144
|
+
};
|
|
145
|
+
for await (const frame of readSseFrames(args.response)) {
|
|
146
|
+
processOpenAiCompatibleSseEvent(frame, state);
|
|
147
|
+
}
|
|
148
|
+
flushChatCompletionStream(state);
|
|
149
|
+
throwIfChatCompletionStreamFailed(state);
|
|
150
|
+
const tool_calls = finalizeAccumulatedToolCalls(state.accumulatedToolCalls);
|
|
151
|
+
return {
|
|
152
|
+
content: state.content,
|
|
153
|
+
...state.reasoning ? { reasoning_content: state.reasoning } : {},
|
|
154
|
+
...tool_calls.length > 0 ? { tool_calls } : {},
|
|
155
|
+
...state.usage ? { usage: state.usage } : {},
|
|
156
|
+
...state.finishReason ? { finish_reason: state.finishReason } : {}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
async function executeOpenAiCompatibleChatCompletion(args) {
|
|
160
|
+
const send = async (apiKey2) => {
|
|
161
|
+
const request = buildOpenAiCompatibleChatCompletionRequest({
|
|
162
|
+
providerConfig: apiKey2 === args.providerConfig.apiKey ? args.providerConfig : { ...args.providerConfig, apiKey: apiKey2 },
|
|
163
|
+
messages: args.messages,
|
|
164
|
+
tools: args.tools,
|
|
165
|
+
stream: args.stream
|
|
166
|
+
});
|
|
167
|
+
return args.fetchImpl(request.url, {
|
|
168
|
+
...request.init,
|
|
169
|
+
...args.signal ? { signal: args.signal } : {}
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
let apiKey = args.providerConfig.apiKey;
|
|
173
|
+
if (args.resolveApiKey) {
|
|
174
|
+
apiKey = await args.resolveApiKey({ force: false }) ?? apiKey;
|
|
175
|
+
}
|
|
176
|
+
let res = await send(apiKey);
|
|
177
|
+
if (res.status === 401 && args.resolveApiKey) {
|
|
178
|
+
const refreshed = await args.resolveApiKey({ force: true });
|
|
179
|
+
if (refreshed && refreshed !== apiKey) {
|
|
180
|
+
await res.body?.cancel().catch(() => {
|
|
181
|
+
});
|
|
182
|
+
res = await send(refreshed);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (!res.ok) {
|
|
186
|
+
const raw2 = await res.text().catch(() => "");
|
|
187
|
+
let data2 = raw2;
|
|
188
|
+
try {
|
|
189
|
+
data2 = JSON.parse(raw2);
|
|
190
|
+
} catch {
|
|
191
|
+
}
|
|
192
|
+
throw new Error(`local provider failed: HTTP ${res.status} ${JSON.stringify(data2)}`);
|
|
193
|
+
}
|
|
194
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
195
|
+
const isEventStream = contentType.includes("text/event-stream");
|
|
196
|
+
if (isEventStream) {
|
|
197
|
+
const streamed = await readOpenAiCompatibleSseCompletion({
|
|
198
|
+
response: res,
|
|
199
|
+
...args.onTextDelta ? { onTextDelta: args.onTextDelta } : {},
|
|
200
|
+
...args.onReasoningDelta ? { onReasoningDelta: args.onReasoningDelta } : {}
|
|
201
|
+
});
|
|
202
|
+
return {
|
|
203
|
+
content: streamed.content,
|
|
204
|
+
model: args.providerConfig.model,
|
|
205
|
+
provider: args.providerConfig.provider,
|
|
206
|
+
...streamed.tool_calls ? { tool_calls: streamed.tool_calls } : {},
|
|
207
|
+
...streamed.reasoning_content ? { reasoning_content: streamed.reasoning_content } : {},
|
|
208
|
+
...streamed.usage ? { usage: streamed.usage } : {},
|
|
209
|
+
...streamed.finish_reason ? { finish_reason: streamed.finish_reason } : {},
|
|
210
|
+
trace: args.messages
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const raw = await res.text().catch(() => "");
|
|
214
|
+
let data = {};
|
|
215
|
+
try {
|
|
216
|
+
data = raw ? JSON.parse(raw) : {};
|
|
217
|
+
} catch {
|
|
218
|
+
data = {};
|
|
219
|
+
}
|
|
220
|
+
return parseOpenAiCompatibleChatCompletionResponse({
|
|
221
|
+
providerConfig: args.providerConfig,
|
|
222
|
+
data,
|
|
223
|
+
trace: args.messages
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// packages/agent-runtime/openAiCompatibleProviderConfig.ts
|
|
228
|
+
async function resolveOpenAiCompatibleProviderConfig(args) {
|
|
229
|
+
const plan = await buildProviderExecutionPlan({
|
|
230
|
+
agentConfig: args.agentConfig,
|
|
231
|
+
env: args.env,
|
|
232
|
+
runtimeKind: "local",
|
|
233
|
+
...args.apiKeyRefResolver ? { apiKeyRefResolver: args.apiKeyRefResolver } : {},
|
|
234
|
+
...args.credentialBroker ? { credentialBroker: args.credentialBroker } : {},
|
|
235
|
+
...args.syncFetcher ? { syncFetcher: args.syncFetcher } : {}
|
|
236
|
+
});
|
|
237
|
+
if (plan.mode === "cli" || plan.transport !== "direct") {
|
|
238
|
+
throw new Error("OpenAI-compatible provider config requires a direct provider execution plan.");
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
model: plan.model,
|
|
242
|
+
endpoint: plan.endpoint,
|
|
243
|
+
apiKey: plan.apiKey,
|
|
244
|
+
...plan.apiKeyHeader ? { apiKeyHeader: plan.apiKeyHeader } : {},
|
|
245
|
+
provider: plan.provider,
|
|
246
|
+
requestOptions: plan.requestOptions
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// packages/agent-runtime/windowsCredentialManagerBroker.ts
|
|
251
|
+
var WINDOWS_CREDENTIAL_MANAGER_SCRIPT = `
|
|
252
|
+
$ErrorActionPreference = 'Stop'
|
|
253
|
+
try {
|
|
254
|
+
$raw = [Console]::In.ReadToEnd()
|
|
255
|
+
if ([string]::IsNullOrWhiteSpace($raw)) { exit 1 }
|
|
256
|
+
$req = $raw | ConvertFrom-Json
|
|
257
|
+
$op = [string]$req.op
|
|
258
|
+
$target = [string]$req.target
|
|
259
|
+
if ([string]::IsNullOrWhiteSpace($op) -or [string]::IsNullOrWhiteSpace($target)) { exit 1 }
|
|
260
|
+
|
|
261
|
+
Add-Type @"
|
|
262
|
+
using System;
|
|
263
|
+
using System.Runtime.InteropServices;
|
|
264
|
+
using System.Text;
|
|
265
|
+
|
|
266
|
+
public class NoloCredNative {
|
|
267
|
+
public const int CRED_TYPE_GENERIC = 1;
|
|
268
|
+
public const int CRED_PERSIST_LOCAL_MACHINE = 2;
|
|
269
|
+
public const int ERROR_NOT_FOUND = 1168;
|
|
270
|
+
|
|
271
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
272
|
+
public struct CREDENTIAL {
|
|
273
|
+
public int Flags;
|
|
274
|
+
public int Type;
|
|
275
|
+
public string TargetName;
|
|
276
|
+
public string Comment;
|
|
277
|
+
public long LastWritten;
|
|
278
|
+
public int CredentialBlobSize;
|
|
279
|
+
public IntPtr CredentialBlob;
|
|
280
|
+
public int Persist;
|
|
281
|
+
public int AttributeCount;
|
|
282
|
+
public IntPtr Attributes;
|
|
283
|
+
public string TargetAlias;
|
|
284
|
+
public string UserName;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
288
|
+
public static extern bool CredWriteW(ref CREDENTIAL userCredential, uint flags);
|
|
289
|
+
|
|
290
|
+
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
291
|
+
public static extern bool CredReadW(string target, int type, int reservedFlag, out IntPtr credentialPtr);
|
|
292
|
+
|
|
293
|
+
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
294
|
+
public static extern bool CredDeleteW(string target, int type, int flags);
|
|
295
|
+
|
|
296
|
+
[DllImport("advapi32.dll", SetLastError = true)]
|
|
297
|
+
public static extern void CredFree(IntPtr buffer);
|
|
298
|
+
}
|
|
299
|
+
"@
|
|
300
|
+
|
|
301
|
+
if ($op -eq 'get') {
|
|
302
|
+
$ptr = [IntPtr]::Zero
|
|
303
|
+
$ok = [NoloCredNative]::CredReadW($target, [NoloCredNative]::CRED_TYPE_GENERIC, 0, [ref]$ptr)
|
|
304
|
+
if (-not $ok) {
|
|
305
|
+
exit [Runtime.InteropServices.Marshal]::GetLastWin32Error()
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
$cred = [Runtime.InteropServices.Marshal]::PtrToStructure($ptr, [type][NoloCredNative+CREDENTIAL])
|
|
309
|
+
if ($cred.CredentialBlobSize -le 0 -or $cred.CredentialBlob -eq [IntPtr]::Zero) {
|
|
310
|
+
$payload = @{ secret = '' } | ConvertTo-Json -Compress
|
|
311
|
+
[Console]::Out.Write($payload)
|
|
312
|
+
exit 0
|
|
313
|
+
}
|
|
314
|
+
$bytes = New-Object byte[] $cred.CredentialBlobSize
|
|
315
|
+
[Runtime.InteropServices.Marshal]::Copy($cred.CredentialBlob, $bytes, 0, $cred.CredentialBlobSize)
|
|
316
|
+
$secret = [Text.Encoding]::UTF8.GetString($bytes)
|
|
317
|
+
$payload = @{ secret = $secret } | ConvertTo-Json -Compress
|
|
318
|
+
[Console]::Out.Write($payload)
|
|
319
|
+
exit 0
|
|
320
|
+
} finally {
|
|
321
|
+
[NoloCredNative]::CredFree($ptr)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if ($op -eq 'has') {
|
|
326
|
+
$ptr = [IntPtr]::Zero
|
|
327
|
+
$ok = [NoloCredNative]::CredReadW($target, [NoloCredNative]::CRED_TYPE_GENERIC, 0, [ref]$ptr)
|
|
328
|
+
if (-not $ok) {
|
|
329
|
+
exit [Runtime.InteropServices.Marshal]::GetLastWin32Error()
|
|
330
|
+
}
|
|
331
|
+
[NoloCredNative]::CredFree($ptr)
|
|
332
|
+
exit 0
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if ($op -eq 'put') {
|
|
336
|
+
$secret = [string]$req.secret
|
|
337
|
+
if ([string]::IsNullOrEmpty($secret)) { exit 1 }
|
|
338
|
+
$blob = [Text.Encoding]::UTF8.GetBytes($secret)
|
|
339
|
+
$cred = New-Object NoloCredNative+CREDENTIAL
|
|
340
|
+
$cred.Type = [NoloCredNative]::CRED_TYPE_GENERIC
|
|
341
|
+
$cred.TargetName = $target
|
|
342
|
+
$cred.UserName = 'api-key'
|
|
343
|
+
$cred.Persist = [NoloCredNative]::CRED_PERSIST_LOCAL_MACHINE
|
|
344
|
+
$cred.CredentialBlobSize = $blob.Length
|
|
345
|
+
$cred.CredentialBlob = [Runtime.InteropServices.Marshal]::AllocHGlobal($blob.Length)
|
|
346
|
+
try {
|
|
347
|
+
if ($blob.Length -gt 0) {
|
|
348
|
+
[Runtime.InteropServices.Marshal]::Copy($blob, 0, $cred.CredentialBlob, $blob.Length)
|
|
349
|
+
}
|
|
350
|
+
$ok = [NoloCredNative]::CredWriteW([ref]$cred, 0)
|
|
351
|
+
if (-not $ok) {
|
|
352
|
+
exit [Runtime.InteropServices.Marshal]::GetLastWin32Error()
|
|
353
|
+
}
|
|
354
|
+
exit 0
|
|
355
|
+
} finally {
|
|
356
|
+
[Runtime.InteropServices.Marshal]::FreeHGlobal($cred.CredentialBlob)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if ($op -eq 'delete') {
|
|
361
|
+
$ok = [NoloCredNative]::CredDeleteW($target, [NoloCredNative]::CRED_TYPE_GENERIC, 0)
|
|
362
|
+
if (-not $ok) {
|
|
363
|
+
exit [Runtime.InteropServices.Marshal]::GetLastWin32Error()
|
|
364
|
+
}
|
|
365
|
+
exit 0
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
exit 1
|
|
369
|
+
} catch {
|
|
370
|
+
exit 1
|
|
371
|
+
}
|
|
372
|
+
`.trim();
|
|
373
|
+
|
|
374
|
+
// packages/agent-runtime/agentRecordKeys.ts
|
|
375
|
+
function buildAgentRuntimeAgentLookupKeys(args) {
|
|
376
|
+
if (/^agent(-pub)?-/.test(args.agentRef)) {
|
|
377
|
+
return [args.agentRef];
|
|
378
|
+
}
|
|
379
|
+
return [`agent-${args.userId}-${args.agentRef}`, `agent-pub-${args.agentRef}`];
|
|
380
|
+
}
|
|
381
|
+
function shouldFetchAgentRuntimeRecordRemotely(dbKey) {
|
|
382
|
+
return /^agent(-pub)?-/.test(dbKey);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// packages/agent-runtime/dialogMessageRecord.ts
|
|
386
|
+
function dialogMessageRecordToAgentRuntimeMessage(record) {
|
|
387
|
+
if (!record || typeof record !== "object") return null;
|
|
388
|
+
if (record.role !== "user" && record.role !== "assistant" && record.role !== "tool") return null;
|
|
389
|
+
return {
|
|
390
|
+
role: record.role,
|
|
391
|
+
content: record.content ?? "",
|
|
392
|
+
...typeof record.reasoning_content === "string" ? { reasoning_content: record.reasoning_content } : {},
|
|
393
|
+
...typeof record.toolCallId === "string" ? { tool_call_id: record.toolCallId } : {},
|
|
394
|
+
...Array.isArray(record.tool_calls) ? { tool_calls: record.tool_calls } : {},
|
|
395
|
+
...typeof record.toolName === "string" ? { toolName: record.toolName } : {}
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// packages/agent-runtime/dialogWritePlan.ts
|
|
400
|
+
function extractLastUserText(messages) {
|
|
401
|
+
const lastUser = [...messages].reverse().find((message) => message.role === "user");
|
|
402
|
+
if (typeof lastUser?.content === "string") return lastUser.content;
|
|
403
|
+
if (Array.isArray(lastUser?.content)) {
|
|
404
|
+
return lastUser.content.filter((part) => part.type === "text").map((part) => part.text).join(" ");
|
|
405
|
+
}
|
|
406
|
+
return "";
|
|
407
|
+
}
|
|
408
|
+
function resolveDialogTitle(args) {
|
|
409
|
+
if (typeof args.existingDialog?.title === "string" && args.existingDialog.title.trim()) {
|
|
410
|
+
return args.existingDialog.title;
|
|
411
|
+
}
|
|
412
|
+
const lastUserText = extractLastUserText(args.messages).trim();
|
|
413
|
+
return lastUserText ? lastUserText.slice(0, 80) : "Local agent run";
|
|
414
|
+
}
|
|
415
|
+
function normalizeSubjectRef(ref) {
|
|
416
|
+
if (!ref || typeof ref !== "object") return null;
|
|
417
|
+
const raw = ref;
|
|
418
|
+
const kind = asOptionalTrimmedString(raw.kind);
|
|
419
|
+
const id = asOptionalTrimmedString(raw.id);
|
|
420
|
+
if (!kind || !id) return null;
|
|
421
|
+
const role = asOptionalTrimmedString(raw.role);
|
|
422
|
+
return {
|
|
423
|
+
kind,
|
|
424
|
+
id,
|
|
425
|
+
...role ? { role } : {}
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function mergeSubjectRefs(...groups) {
|
|
429
|
+
const refs = [];
|
|
430
|
+
const seen = /* @__PURE__ */ new Set();
|
|
431
|
+
for (const group of groups) {
|
|
432
|
+
if (!Array.isArray(group)) continue;
|
|
433
|
+
for (const item of group) {
|
|
434
|
+
const ref = normalizeSubjectRef(item);
|
|
435
|
+
if (!ref) continue;
|
|
436
|
+
const key = `${ref.kind}:${ref.id}:${ref.role ?? ""}`;
|
|
437
|
+
if (seen.has(key)) continue;
|
|
438
|
+
seen.add(key);
|
|
439
|
+
refs.push(ref);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return refs.length ? refs : void 0;
|
|
443
|
+
}
|
|
444
|
+
function buildRuntimeSubjectRefs(runtimeContext) {
|
|
445
|
+
return mergeSubjectRefs(runtimeContext?.subjectRefs);
|
|
446
|
+
}
|
|
447
|
+
function buildDialogLineageFields(args) {
|
|
448
|
+
const inheritedFromDialogKey = asOptionalTrimmedString(args.input.inheritedFromDialogKey);
|
|
449
|
+
const parentDialogId = asOptionalTrimmedString(args.input.parentDialogId);
|
|
450
|
+
if (!inheritedFromDialogKey && !parentDialogId) return {};
|
|
451
|
+
const rootDialogId = asOptionalTrimmedString(args.existingDialog?.rootDialogId) ?? asOptionalTrimmedString(args.existingDialog?.parentDialogId) ?? parentDialogId;
|
|
452
|
+
return {
|
|
453
|
+
...inheritedFromDialogKey ? { inheritedFromDialogKey } : {},
|
|
454
|
+
...parentDialogId ? { parentDialogId } : {},
|
|
455
|
+
...rootDialogId ? { rootDialogId } : {}
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
function buildDialogMessageWriteOps(args) {
|
|
459
|
+
return args.input.messages.filter((message) => message.role !== "system").map((message, index) => {
|
|
460
|
+
const id = `${args.now}-${String(index + 1).padStart(3, "0")}`;
|
|
461
|
+
const key = dialogMessageKey(args.dialogId, id);
|
|
462
|
+
const toolName = asOptionalTrimmedString(message.toolName);
|
|
463
|
+
return {
|
|
464
|
+
type: "put",
|
|
465
|
+
key,
|
|
466
|
+
value: {
|
|
467
|
+
id,
|
|
468
|
+
dbKey: key,
|
|
469
|
+
dialogId: args.dialogId,
|
|
470
|
+
role: message.role,
|
|
471
|
+
content: message.content ?? "",
|
|
472
|
+
// 持久化思维链(reasoning):空轮/异常排查的关键证据,
|
|
473
|
+
// 回读由 dialogMessageRecordToAgentRuntimeMessage 还原。
|
|
474
|
+
...typeof message.reasoning_content === "string" ? { reasoning_content: message.reasoning_content } : {},
|
|
475
|
+
...message.role === "user" ? { userId: args.userId } : {},
|
|
476
|
+
...message.role === "assistant" ? {
|
|
477
|
+
agentKey: args.input.agentKey,
|
|
478
|
+
cybotKey: args.input.agentKey
|
|
479
|
+
} : {},
|
|
480
|
+
...message.tool_call_id ? { toolCallId: message.tool_call_id } : {},
|
|
481
|
+
...toolName ? { toolName } : {},
|
|
482
|
+
...Array.isArray(message.tool_calls) ? { tool_calls: message.tool_calls } : {},
|
|
483
|
+
...message.tool_result_metadata ? { metadata: message.tool_result_metadata } : {},
|
|
484
|
+
createdAt: args.nowIso
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
function buildAgentRuntimeDialogWritePlan(args) {
|
|
490
|
+
const dialogId = args.input.continueDialogId || args.createId();
|
|
491
|
+
const nowIso = new Date(args.now).toISOString();
|
|
492
|
+
const dialogKey = `dialog-${args.userId}-${dialogId}`;
|
|
493
|
+
const subjectRefs = buildRuntimeSubjectRefs(args.input.runtimeContext);
|
|
494
|
+
const dialogRecord = {
|
|
495
|
+
...asRecordOrEmpty(args.existingDialog),
|
|
496
|
+
id: dialogId,
|
|
497
|
+
dbKey: dialogKey,
|
|
498
|
+
type: "dialog",
|
|
499
|
+
userId: args.userId,
|
|
500
|
+
cybots: [args.input.agentKey],
|
|
501
|
+
primaryAgentKey: args.input.agentKey,
|
|
502
|
+
title: typeof args.existingDialog?.title === "string" && args.existingDialog.title.trim() ? args.existingDialog.title.trim() : args.titleOverride?.trim() ? args.titleOverride.trim() : resolveDialogTitle({
|
|
503
|
+
existingDialog: args.existingDialog,
|
|
504
|
+
messages: args.input.messages
|
|
505
|
+
}),
|
|
506
|
+
status: args.input.result.error === true ? "failed" : "done",
|
|
507
|
+
triggerType: `${args.runtimeHost}-local`,
|
|
508
|
+
executionMode: args.existingDialog?.executionMode ?? "foreground",
|
|
509
|
+
createdAt: args.existingDialog?.createdAt ?? nowIso,
|
|
510
|
+
updatedAt: nowIso,
|
|
511
|
+
finishedAt: args.now,
|
|
512
|
+
usage: args.input.result.usage,
|
|
513
|
+
...asOptionalTrimmedString(args.input.spaceId) ? { spaceId: asOptionalTrimmedString(args.input.spaceId) } : {},
|
|
514
|
+
...asOptionalTrimmedString(args.input.category) ? { category: asOptionalTrimmedString(args.input.category) } : {},
|
|
515
|
+
...subjectRefs ? { subjectRefs } : {},
|
|
516
|
+
...buildDialogLineageFields({
|
|
517
|
+
input: args.input,
|
|
518
|
+
existingDialog: args.existingDialog
|
|
519
|
+
}),
|
|
520
|
+
...typeof args.input.result.toolCallCount === "number" ? { toolCallCount: args.input.result.toolCallCount } : {},
|
|
521
|
+
localRuntime: {
|
|
522
|
+
host: args.runtimeHost,
|
|
523
|
+
...args.runtimeMetadata ?? {}
|
|
524
|
+
},
|
|
525
|
+
...args.input.result.runtimeToolSurface ? {
|
|
526
|
+
runtimeCheckpoint: {
|
|
527
|
+
...asRecordOrEmpty(args.existingDialog?.runtimeCheckpoint),
|
|
528
|
+
toolSurface: args.input.result.runtimeToolSurface
|
|
529
|
+
}
|
|
530
|
+
} : {}
|
|
531
|
+
};
|
|
532
|
+
return {
|
|
533
|
+
dialogId,
|
|
534
|
+
ops: [
|
|
535
|
+
{
|
|
536
|
+
type: "put",
|
|
537
|
+
key: dialogKey,
|
|
538
|
+
value: dialogRecord
|
|
539
|
+
},
|
|
540
|
+
...buildDialogMessageWriteOps({
|
|
541
|
+
dialogId,
|
|
542
|
+
input: args.input,
|
|
543
|
+
userId: args.userId,
|
|
544
|
+
now: args.now,
|
|
545
|
+
nowIso
|
|
546
|
+
})
|
|
547
|
+
]
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// packages/agent-runtime/hybridRecordStore.ts
|
|
552
|
+
function resolveHybridServers(defaultServer, preferredServerOrigin, fallbackServers = []) {
|
|
553
|
+
const raw = [
|
|
554
|
+
preferredServerOrigin || "",
|
|
555
|
+
defaultServer,
|
|
556
|
+
...fallbackServers
|
|
557
|
+
].filter((value) => value.trim().length > 0);
|
|
558
|
+
return [...new Set(raw.map(normalizeServerOrigin))];
|
|
559
|
+
}
|
|
560
|
+
function normalizeHybridRecord(dbKey, record, serverOrigin) {
|
|
561
|
+
if (!record || typeof record !== "object") return null;
|
|
562
|
+
return {
|
|
563
|
+
...record,
|
|
564
|
+
dbKey: typeof record.dbKey === "string" && record.dbKey ? record.dbKey : dbKey,
|
|
565
|
+
...serverOrigin ? { serverOrigin } : {}
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
function getHybridRecordTimestamp(record) {
|
|
569
|
+
if (!record || typeof record !== "object") return 0;
|
|
570
|
+
const candidates = [
|
|
571
|
+
record.updatedAt,
|
|
572
|
+
record.updated_at,
|
|
573
|
+
record.createdAt,
|
|
574
|
+
record.created,
|
|
575
|
+
record?.meta?.createdAt
|
|
576
|
+
];
|
|
577
|
+
for (const candidate of candidates) {
|
|
578
|
+
const asNumber = asOptionalPositiveFiniteNumber(candidate);
|
|
579
|
+
if (asNumber !== void 0) return asNumber;
|
|
580
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
581
|
+
const parsed = asOptionalPositiveFiniteNumber(Date.parse(candidate));
|
|
582
|
+
if (parsed !== void 0) return parsed;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return 0;
|
|
586
|
+
}
|
|
587
|
+
function isHybridTombstoneRecord(record) {
|
|
588
|
+
if (!record || typeof record !== "object") return false;
|
|
589
|
+
if (typeof record.deletedAt === "string") return record.deletedAt.trim().length > 0;
|
|
590
|
+
return Boolean(record.deletedAt);
|
|
591
|
+
}
|
|
592
|
+
function shouldReplaceWithHybridRecord(nextRecord, currentRecord) {
|
|
593
|
+
const nextTs = getHybridRecordTimestamp(nextRecord);
|
|
594
|
+
const currentTs = getHybridRecordTimestamp(currentRecord);
|
|
595
|
+
if (nextTs !== currentTs) return nextTs > currentTs;
|
|
596
|
+
return isHybridTombstoneRecord(nextRecord) && !isHybridTombstoneRecord(currentRecord);
|
|
597
|
+
}
|
|
598
|
+
function isHybridNotFoundError(error) {
|
|
599
|
+
const message = toErrorMessage(error);
|
|
600
|
+
return /not found|notfound|leveldb: not found|key not found/i.test(message);
|
|
601
|
+
}
|
|
602
|
+
async function readHybridLocalRecord(db, dbKey) {
|
|
603
|
+
try {
|
|
604
|
+
return await db.get(dbKey);
|
|
605
|
+
} catch (error) {
|
|
606
|
+
if (!isHybridNotFoundError(error)) throw error;
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
async function fetchHybridRemoteRecord(args) {
|
|
611
|
+
const response = await args.fetchImpl(
|
|
612
|
+
`${args.server}/api/v1/db/read/${encodeURIComponent(args.dbKey)}`,
|
|
613
|
+
{
|
|
614
|
+
headers: {
|
|
615
|
+
"Content-Type": "application/json",
|
|
616
|
+
...args.authToken ? { Authorization: `Bearer ${args.authToken}` } : {}
|
|
617
|
+
},
|
|
618
|
+
...args.signal ? { signal: args.signal } : {}
|
|
619
|
+
}
|
|
620
|
+
);
|
|
621
|
+
if (!response.ok) return null;
|
|
622
|
+
const payload = await response.json().catch(() => null);
|
|
623
|
+
return normalizeHybridRecord(args.dbKey, payload?.data ?? payload, args.server);
|
|
624
|
+
}
|
|
625
|
+
function shouldCacheHybridRemoteRecord(remoteRecord, localRecord) {
|
|
626
|
+
if (!remoteRecord || typeof remoteRecord !== "object") return false;
|
|
627
|
+
if (!localRecord || typeof localRecord !== "object") return true;
|
|
628
|
+
return shouldReplaceWithHybridRecord(remoteRecord, localRecord);
|
|
629
|
+
}
|
|
630
|
+
function createHybridRecordStore(deps) {
|
|
631
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
632
|
+
return {
|
|
633
|
+
read: async (dbKey, options) => {
|
|
634
|
+
const localRecord = await readHybridLocalRecord(deps.db, dbKey);
|
|
635
|
+
const normalizedLocalRecord = normalizeHybridRecord(dbKey, localRecord);
|
|
636
|
+
if (normalizedLocalRecord && options?.remote !== true) return normalizedLocalRecord;
|
|
637
|
+
if (options?.remote === false) return null;
|
|
638
|
+
for (const server of resolveHybridServers(
|
|
639
|
+
deps.defaultServer,
|
|
640
|
+
options?.preferredServerOrigin,
|
|
641
|
+
deps.fallbackServers
|
|
642
|
+
)) {
|
|
643
|
+
let remoteRecord = null;
|
|
644
|
+
try {
|
|
645
|
+
remoteRecord = await fetchHybridRemoteRecord({
|
|
646
|
+
dbKey,
|
|
647
|
+
server,
|
|
648
|
+
authToken: deps.authToken,
|
|
649
|
+
fetchImpl,
|
|
650
|
+
...deps.requestTimeoutMs ? { signal: AbortSignal.timeout(deps.requestTimeoutMs) } : {}
|
|
651
|
+
});
|
|
652
|
+
} catch {
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
if (!remoteRecord) continue;
|
|
656
|
+
if (shouldCacheHybridRemoteRecord(remoteRecord, localRecord)) {
|
|
657
|
+
await deps.db.put(dbKey, remoteRecord);
|
|
658
|
+
return remoteRecord;
|
|
659
|
+
}
|
|
660
|
+
return normalizedLocalRecord ?? remoteRecord;
|
|
661
|
+
}
|
|
662
|
+
return normalizedLocalRecord ?? null;
|
|
663
|
+
},
|
|
664
|
+
write: async (dbKey, record) => {
|
|
665
|
+
const nextRecord = normalizeHybridRecord(dbKey, record) ?? { dbKey };
|
|
666
|
+
await deps.db.put(dbKey, nextRecord);
|
|
667
|
+
return nextRecord;
|
|
668
|
+
},
|
|
669
|
+
batch: async (ops) => deps.db.batch(ops.map((op) => ({
|
|
670
|
+
...op,
|
|
671
|
+
value: normalizeHybridRecord(op.key, op.value) ?? { dbKey: op.key }
|
|
672
|
+
}))),
|
|
673
|
+
iterator: (options) => deps.db.iterator(options)
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// packages/agent-runtime/shellCommandPolicy.ts
|
|
678
|
+
var DESTRUCTIVE_NEGATION_PATTERNS = [
|
|
679
|
+
"\u522B\u5220",
|
|
680
|
+
"\u522B\u5220\u9664",
|
|
681
|
+
"\u4E0D\u8981\u5220",
|
|
682
|
+
"\u4E0D\u8981\u5220\u9664",
|
|
683
|
+
"\u522B\u4E71\u5220",
|
|
684
|
+
"\u522B\u52A8",
|
|
685
|
+
"\u4E0D\u8981\u52A8",
|
|
686
|
+
"\u4E0D\u8981\u6E05\u7406",
|
|
687
|
+
"\u4E0D\u8981\u6E05\u7A7A",
|
|
688
|
+
"\u4E0D\u8981\u91CD\u7F6E",
|
|
689
|
+
"don't delete",
|
|
690
|
+
"do not delete",
|
|
691
|
+
"don't remove",
|
|
692
|
+
"do not remove",
|
|
693
|
+
"without deleting"
|
|
694
|
+
];
|
|
695
|
+
var EXPLICIT_DESTRUCTIVE_REQUEST_PATTERNS = [
|
|
696
|
+
"\u5220\u9664",
|
|
697
|
+
"\u5220\u6389",
|
|
698
|
+
"\u79FB\u9664",
|
|
699
|
+
"\u6E05\u7406",
|
|
700
|
+
"\u6E05\u7A7A",
|
|
701
|
+
"\u91CD\u7F6E",
|
|
702
|
+
"drop",
|
|
703
|
+
"delete",
|
|
704
|
+
"remove",
|
|
705
|
+
"erase",
|
|
706
|
+
"clean up",
|
|
707
|
+
"reset"
|
|
708
|
+
];
|
|
709
|
+
var SHELL_DESTRUCTIVE_PATTERNS = [
|
|
710
|
+
/\brm\s+(-[^\n\r]*\s+)?/i,
|
|
711
|
+
/\bremove-item\b/i,
|
|
712
|
+
/\bdel\s+\/?[a-z]*\s+/i,
|
|
713
|
+
/\berase\s+/i,
|
|
714
|
+
/\brmdir\s+/i,
|
|
715
|
+
/\brd\s+\/s\b/i,
|
|
716
|
+
/\bgit\s+reset\s+(--hard\b|--merge\b|--keep\b)/i,
|
|
717
|
+
/\bgit\s+clean\s+-[^\n\r]*f/i,
|
|
718
|
+
/\bgit\s+checkout\s+--\b/i,
|
|
719
|
+
/\bgit\s+restore\s+--source=/i
|
|
720
|
+
];
|
|
721
|
+
var SHELL_STRING_EXECUTION_PATTERNS = [
|
|
722
|
+
/\b(?:ba|da|z|k)?sh\b/i,
|
|
723
|
+
/\beval\b/i,
|
|
724
|
+
/\bexec\b/i,
|
|
725
|
+
/\bxargs\b/i,
|
|
726
|
+
/\bsource\s/i,
|
|
727
|
+
/\b(?:node|deno|bun)\s+(?:-\S+\s+)*(?:-e|--eval)\b/i,
|
|
728
|
+
/\bpython[\d.]*\s+(?:-\S+\s+)*-c\b/i,
|
|
729
|
+
/\b(?:perl|ruby)\s+(?:-\S+\s+)*-e\b/i
|
|
730
|
+
];
|
|
731
|
+
var stripQuotedSegments = (text) => {
|
|
732
|
+
let out = "";
|
|
733
|
+
let i = 0;
|
|
734
|
+
while (i < text.length) {
|
|
735
|
+
const ch = text[i];
|
|
736
|
+
if (ch === "\\") {
|
|
737
|
+
out += text.slice(i, i + 2);
|
|
738
|
+
i += 2;
|
|
739
|
+
} else if (ch === "'") {
|
|
740
|
+
const end = text.indexOf("'", i + 1);
|
|
741
|
+
if (end === -1) return out + text.slice(i);
|
|
742
|
+
out += "''";
|
|
743
|
+
i = end + 1;
|
|
744
|
+
} else if (ch === '"') {
|
|
745
|
+
let j = i + 1;
|
|
746
|
+
while (j < text.length && text[j] !== '"') {
|
|
747
|
+
j += text[j] === "\\" ? 2 : 1;
|
|
748
|
+
}
|
|
749
|
+
if (j >= text.length) return out + text.slice(i);
|
|
750
|
+
out += '""';
|
|
751
|
+
i = j + 1;
|
|
752
|
+
} else {
|
|
753
|
+
out += ch;
|
|
754
|
+
i += 1;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return out;
|
|
758
|
+
};
|
|
759
|
+
var normalizeUserInput = (value) => asTrimmedLowercaseString(value);
|
|
760
|
+
var containsAny = (text, patterns) => patterns.some((pattern) => text.includes(pattern));
|
|
761
|
+
var hasExplicitDestructivePermission = (userInput) => {
|
|
762
|
+
const normalized = normalizeUserInput(userInput);
|
|
763
|
+
if (!normalized) return false;
|
|
764
|
+
if (containsAny(normalized, DESTRUCTIVE_NEGATION_PATTERNS)) return false;
|
|
765
|
+
return containsAny(normalized, EXPLICIT_DESTRUCTIVE_REQUEST_PATTERNS);
|
|
766
|
+
};
|
|
767
|
+
var SHELL_COMMAND_ARG_ALIASES = [
|
|
768
|
+
"command",
|
|
769
|
+
"runCommand",
|
|
770
|
+
"run_command",
|
|
771
|
+
"terminalCommand",
|
|
772
|
+
"terminal_command",
|
|
773
|
+
"runInBash",
|
|
774
|
+
"run_in_bash",
|
|
775
|
+
"executeCommand",
|
|
776
|
+
"execute_command",
|
|
777
|
+
"execShell",
|
|
778
|
+
"bash",
|
|
779
|
+
"cmd"
|
|
780
|
+
];
|
|
781
|
+
var resolveShellCommandArg = (args) => {
|
|
782
|
+
if (!args) return void 0;
|
|
783
|
+
for (const key of SHELL_COMMAND_ARG_ALIASES) {
|
|
784
|
+
const value = args[key];
|
|
785
|
+
if (value != null) return value;
|
|
786
|
+
}
|
|
787
|
+
return void 0;
|
|
788
|
+
};
|
|
789
|
+
var buildCombinedShellInput = (args) => {
|
|
790
|
+
const command = resolveShellCommandArg(args);
|
|
791
|
+
return [
|
|
792
|
+
typeof command === "string" ? command : "",
|
|
793
|
+
typeof args.input === "string" ? args.input : ""
|
|
794
|
+
].filter(Boolean).join("\n");
|
|
795
|
+
};
|
|
796
|
+
function isDestructiveShellCommand(args) {
|
|
797
|
+
const combined = buildCombinedShellInput(args);
|
|
798
|
+
if (!combined.trim()) return false;
|
|
799
|
+
const scannable = SHELL_STRING_EXECUTION_PATTERNS.some(
|
|
800
|
+
(pattern) => pattern.test(combined)
|
|
801
|
+
) ? combined : stripQuotedSegments(combined);
|
|
802
|
+
return SHELL_DESTRUCTIVE_PATTERNS.some((pattern) => pattern.test(scannable));
|
|
803
|
+
}
|
|
804
|
+
var LONG_RUNNING_COMMAND_PATTERNS = [
|
|
805
|
+
/\b(run|npm|yarn|pnpm|bun|npx)\s+(run\s+)?(dev|serve|start|watch)\b/i,
|
|
806
|
+
/(^|[\s=&|;])--watch\b/i,
|
|
807
|
+
/\bvite\b.*\b(dev|serve)\b/i,
|
|
808
|
+
/\bwebpack\b.*(?:--watch|serve)\b/i,
|
|
809
|
+
/\bnodemon\b/i,
|
|
810
|
+
/\bgatsby\b.*\b(develop|serve)\b/i,
|
|
811
|
+
/\bnext\b.*\b(dev|start)\b/i,
|
|
812
|
+
/\belectron\b.*\bdev\b/i,
|
|
813
|
+
/\bforever\b/i,
|
|
814
|
+
/\bpm2\b.*\bstart\b/i
|
|
815
|
+
];
|
|
816
|
+
function isLongRunningShellCommand(args) {
|
|
817
|
+
const combined = buildCombinedShellInput(args);
|
|
818
|
+
if (!combined.trim()) return false;
|
|
819
|
+
const scannable = SHELL_STRING_EXECUTION_PATTERNS.some(
|
|
820
|
+
(pattern) => pattern.test(combined)
|
|
821
|
+
) ? combined : stripQuotedSegments(combined);
|
|
822
|
+
return LONG_RUNNING_COMMAND_PATTERNS.some((pattern) => pattern.test(scannable));
|
|
823
|
+
}
|
|
824
|
+
function evaluateShellCommandPolicy(args) {
|
|
825
|
+
if (isDestructiveShellCommand(args) && !hasExplicitDestructivePermission(args.userInput)) {
|
|
826
|
+
return {
|
|
827
|
+
verdict: "forbidden",
|
|
828
|
+
permissionDecision: "ask",
|
|
829
|
+
code: "destructive_action_requires_confirmation",
|
|
830
|
+
reason: "\u5F53\u524D\u8FD0\u884C\u9ED8\u8BA4\u7981\u6B62\u81EA\u52A8\u6267\u884C\u53EF\u80FD\u5220\u9664\u7528\u6237\u5185\u5BB9\u7684 shell \u547D\u4EE4\u3002\u53EA\u6709\u5F53\u7528\u6237\u5728\u5F53\u524D\u8BF7\u6C42\u91CC\u660E\u786E\u8981\u6C42\u5220\u9664/\u6E05\u7406\u65F6\uFF0C\u624D\u80FD\u7EE7\u7EED\uFF1B\u5426\u5219\u8BF7\u505C\u6B62\u5E76\u5148\u8BF4\u660E\u9650\u5236\u3002",
|
|
831
|
+
permissionRequest: {
|
|
832
|
+
id: "permission-shell-destructive-action",
|
|
833
|
+
tool: "execShell",
|
|
834
|
+
action: "destructive_shell_command",
|
|
835
|
+
title: "\u786E\u8BA4\u6267\u884C\u7834\u574F\u6027 shell \u547D\u4EE4",
|
|
836
|
+
body: "\u8BE5\u547D\u4EE4\u53EF\u80FD\u5220\u9664\u6216\u91CD\u7F6E\u7528\u6237\u5185\u5BB9\uFF0C\u9700\u8981\u7528\u6237\u660E\u786E\u786E\u8BA4\u540E\u624D\u80FD\u6267\u884C\u3002",
|
|
837
|
+
suggestedRule: {
|
|
838
|
+
scope: "once",
|
|
839
|
+
pattern: { capability: "destructive_action", target: "shell_command" }
|
|
840
|
+
}
|
|
841
|
+
},
|
|
842
|
+
policy: {
|
|
843
|
+
capability: "destructive_action",
|
|
844
|
+
target: "shell_command",
|
|
845
|
+
detail: "execShell destructive command"
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
const longRunningHint = isLongRunningShellCommand(args);
|
|
850
|
+
return {
|
|
851
|
+
verdict: "allowed",
|
|
852
|
+
permissionDecision: "allow",
|
|
853
|
+
...longRunningHint ? { longRunningHint: true } : {}
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// packages/agent-runtime/localToolPolicy.ts
|
|
858
|
+
var NEVER_LOCAL_TOOLS = /* @__PURE__ */ new Set([
|
|
859
|
+
"deleteSpaces",
|
|
860
|
+
"updateAgent",
|
|
861
|
+
"updateSelf",
|
|
862
|
+
"createAgent"
|
|
863
|
+
]);
|
|
864
|
+
var REMOVED_LOCAL_TOOLS = /* @__PURE__ */ new Set([
|
|
865
|
+
"gitStatus",
|
|
866
|
+
"gitDiff",
|
|
867
|
+
"gitCreateBranch",
|
|
868
|
+
"gitAdd",
|
|
869
|
+
"gitCommit",
|
|
870
|
+
"commitWorkspace"
|
|
871
|
+
]);
|
|
872
|
+
var DEFAULT_LOCAL_TOOLS = /* @__PURE__ */ new Set([
|
|
873
|
+
"listFiles",
|
|
874
|
+
"readFile",
|
|
875
|
+
"writeFile",
|
|
876
|
+
"editFile",
|
|
877
|
+
"globFiles",
|
|
878
|
+
"searchFiles",
|
|
879
|
+
"execShell",
|
|
880
|
+
"launchProcess",
|
|
881
|
+
"listProcesses"
|
|
882
|
+
]);
|
|
883
|
+
function parseToolAllowlist(value) {
|
|
884
|
+
return (value ?? "").split(",").flatMap((item) => {
|
|
885
|
+
const trimmed = item.trim();
|
|
886
|
+
return trimmed ? [trimmed] : [];
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
function normalizeLocalToolName(toolName) {
|
|
890
|
+
return canonicalizeToolName(
|
|
891
|
+
String(toolName ?? "").replace(/^functions\./, "").trim()
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
function parseShellCommandPayload(rawArguments) {
|
|
895
|
+
return parseToolArgumentsJson(rawArguments);
|
|
896
|
+
}
|
|
897
|
+
function toDisplayCommand(command) {
|
|
898
|
+
if (typeof command === "string") {
|
|
899
|
+
const trimmed = command.trim();
|
|
900
|
+
return trimmed ? trimmed : void 0;
|
|
901
|
+
}
|
|
902
|
+
if (Array.isArray(command)) {
|
|
903
|
+
const joined = command.map((item) => typeof item === "string" ? item : String(item)).filter((item) => item.length > 0).join(" ").trim();
|
|
904
|
+
return joined ? joined : void 0;
|
|
905
|
+
}
|
|
906
|
+
return void 0;
|
|
907
|
+
}
|
|
908
|
+
function isRestrictedLocalToolMode(env) {
|
|
909
|
+
return env.NOLO_LOCAL_TOOL_MODE === "restricted";
|
|
910
|
+
}
|
|
911
|
+
function resolveLocalToolPolicy(args) {
|
|
912
|
+
const toolName = normalizeLocalToolName(args.toolName);
|
|
913
|
+
const restrictedMode = isRestrictedLocalToolMode(args.env);
|
|
914
|
+
if (!toolName) {
|
|
915
|
+
return {
|
|
916
|
+
allowed: false,
|
|
917
|
+
toolName,
|
|
918
|
+
reason: "Tool name is required."
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
if (NEVER_LOCAL_TOOLS.has(toolName)) {
|
|
922
|
+
return {
|
|
923
|
+
allowed: false,
|
|
924
|
+
toolName,
|
|
925
|
+
reason: `${toolName} is blocked by the local runtime safety policy.`
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
if (REMOVED_LOCAL_TOOLS.has(toolName)) {
|
|
929
|
+
return {
|
|
930
|
+
allowed: false,
|
|
931
|
+
toolName,
|
|
932
|
+
reason: `${toolName} has been removed from the local runtime tool surface. Use execShell in shell-enabled workspace runs when shell is needed.`
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
const allowedByEnv = new Set(parseToolAllowlist(args.env.NOLO_LOCAL_ALLOWED_TOOLS));
|
|
936
|
+
const agentTools = new Set(
|
|
937
|
+
(args.agentToolNames ?? []).flatMap((item) => {
|
|
938
|
+
const normalized = normalizeLocalToolName(item);
|
|
939
|
+
return normalized ? [normalized] : [];
|
|
940
|
+
})
|
|
941
|
+
);
|
|
942
|
+
if (!restrictedMode && agentTools.has(toolName)) {
|
|
943
|
+
return { allowed: true, toolName };
|
|
944
|
+
}
|
|
945
|
+
if (DEFAULT_LOCAL_TOOLS.has(toolName) && agentTools.has(toolName)) {
|
|
946
|
+
return { allowed: true, toolName };
|
|
947
|
+
}
|
|
948
|
+
if (allowedByEnv.has(toolName) && agentTools.has(toolName)) {
|
|
949
|
+
return { allowed: true, toolName };
|
|
950
|
+
}
|
|
951
|
+
return {
|
|
952
|
+
allowed: false,
|
|
953
|
+
toolName,
|
|
954
|
+
reason: `${toolName} is not enabled for local runtime runs. Set NOLO_LOCAL_ALLOWED_TOOLS and make sure the agent declares the tool before using it locally.`
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
async function executeLocalToolWithPolicy(args) {
|
|
958
|
+
const decision = resolveLocalToolPolicy({
|
|
959
|
+
env: args.env,
|
|
960
|
+
agentToolNames: args.agentToolNames,
|
|
961
|
+
toolName: args.call.name
|
|
962
|
+
});
|
|
963
|
+
if (!decision.allowed) throw new Error(decision.reason);
|
|
964
|
+
const executor = args.executors?.[decision.toolName];
|
|
965
|
+
if (!executor) {
|
|
966
|
+
throw new Error(`${decision.toolName} is allowed by policy but no local executor is registered.`);
|
|
967
|
+
}
|
|
968
|
+
const effectiveExecutor = args.abortSignal && decision.toolName === "execShell" && args.executors ? (call) => args.executors.execShell(call, {
|
|
969
|
+
abortSignal: args.abortSignal,
|
|
970
|
+
detachMs: args.detachMs
|
|
971
|
+
}) : executor;
|
|
972
|
+
const guardEnabled = args.enableDestructiveShellGuard !== false;
|
|
973
|
+
if (decision.toolName === "execShell" && !args.confirmed && guardEnabled) {
|
|
974
|
+
const parsed = parseShellCommandPayload(args.call.arguments);
|
|
975
|
+
const rawCommand = resolveShellCommandArg(parsed);
|
|
976
|
+
if (isDestructiveShellCommand({ command: rawCommand, input: parsed.input })) {
|
|
977
|
+
const command = toDisplayCommand(rawCommand);
|
|
978
|
+
const request = {
|
|
979
|
+
id: "permission-shell-destructive-action",
|
|
980
|
+
tool: "execShell",
|
|
981
|
+
action: "destructive_shell_command",
|
|
982
|
+
title: "\u786E\u8BA4\u6267\u884C\u7834\u574F\u6027 shell \u547D\u4EE4",
|
|
983
|
+
body: "\u8BE5\u547D\u4EE4\u53EF\u80FD\u5220\u9664\u6216\u91CD\u7F6E\u7528\u6237\u5185\u5BB9\uFF0C\u9700\u8981\u7528\u6237\u660E\u786E\u786E\u8BA4\u540E\u624D\u80FD\u6267\u884C\u3002",
|
|
984
|
+
...command ? { command } : {},
|
|
985
|
+
suggestedRule: {
|
|
986
|
+
scope: "once",
|
|
987
|
+
pattern: { capability: "destructive_action", target: "shell_command" }
|
|
988
|
+
}
|
|
989
|
+
};
|
|
990
|
+
const buildBlockedError = (reason) => {
|
|
991
|
+
const error = new Error(reason);
|
|
992
|
+
error.code = "destructive_action_requires_confirmation";
|
|
993
|
+
error.policy = {
|
|
994
|
+
capability: "destructive_action",
|
|
995
|
+
target: "shell_command",
|
|
996
|
+
detail: "execShell destructive command"
|
|
997
|
+
};
|
|
998
|
+
error.permissionRequest = request;
|
|
999
|
+
return error;
|
|
1000
|
+
};
|
|
1001
|
+
if (!args.confirmDestructiveAction) {
|
|
1002
|
+
if (args.blockDestructiveWithoutConfirmation) {
|
|
1003
|
+
throw buildBlockedError(
|
|
1004
|
+
"destructive shell command blocked: no confirmation channel available"
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
return effectiveExecutor({
|
|
1008
|
+
...args.call,
|
|
1009
|
+
name: decision.toolName
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
const confirmed = await args.confirmDestructiveAction(request);
|
|
1013
|
+
if (!confirmed) {
|
|
1014
|
+
throw buildBlockedError(
|
|
1015
|
+
"destructive shell command blocked: user declined confirmation"
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
return effectiveExecutor({
|
|
1021
|
+
...args.call,
|
|
1022
|
+
name: decision.toolName
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// packages/agent-runtime/localWorkspaceTools.ts
|
|
1027
|
+
import { existsSync } from "node:fs";
|
|
1028
|
+
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
1029
|
+
import { dirname, relative, resolve, sep } from "node:path";
|
|
1030
|
+
import { spawnSync, spawn as spawnChildProcess } from "node:child_process";
|
|
1031
|
+
|
|
1032
|
+
// packages/agent-runtime/processRegistry.ts
|
|
1033
|
+
var ProcessRegistry = class {
|
|
1034
|
+
processes = /* @__PURE__ */ new Map();
|
|
1035
|
+
add(proc) {
|
|
1036
|
+
this.processes.set(proc.pid, {
|
|
1037
|
+
pid: proc.pid,
|
|
1038
|
+
pgid: proc.pgid,
|
|
1039
|
+
command: proc.command,
|
|
1040
|
+
label: proc.label,
|
|
1041
|
+
startedAt: Date.now(),
|
|
1042
|
+
status: "running"
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
list() {
|
|
1046
|
+
return Array.from(this.processes.values()).map((proc) => ({ ...proc }));
|
|
1047
|
+
}
|
|
1048
|
+
get(pid) {
|
|
1049
|
+
const item = this.processes.get(pid);
|
|
1050
|
+
return item ? { ...item } : void 0;
|
|
1051
|
+
}
|
|
1052
|
+
kill(pid, signal = "SIGTERM") {
|
|
1053
|
+
const item = this.processes.get(pid);
|
|
1054
|
+
if (!item) return false;
|
|
1055
|
+
if (item.status === "running") {
|
|
1056
|
+
try {
|
|
1057
|
+
process.kill(-item.pgid, signal);
|
|
1058
|
+
} catch {
|
|
1059
|
+
}
|
|
1060
|
+
item.status = "stopped";
|
|
1061
|
+
return true;
|
|
1062
|
+
}
|
|
1063
|
+
return false;
|
|
1064
|
+
}
|
|
1065
|
+
stopAll(signal = "SIGTERM") {
|
|
1066
|
+
for (const item of this.processes.values()) {
|
|
1067
|
+
if (item.status === "running") {
|
|
1068
|
+
try {
|
|
1069
|
+
process.kill(-item.pgid, signal);
|
|
1070
|
+
} catch {
|
|
1071
|
+
}
|
|
1072
|
+
item.status = "stopped";
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
markExited(pid, exitCode) {
|
|
1077
|
+
const item = this.processes.get(pid);
|
|
1078
|
+
if (item && item.status === "running") {
|
|
1079
|
+
item.exitCode = exitCode;
|
|
1080
|
+
item.status = exitCode === 0 ? "exited" : "failed";
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
clear() {
|
|
1084
|
+
this.processes.clear();
|
|
1085
|
+
}
|
|
1086
|
+
};
|
|
1087
|
+
var registry = null;
|
|
1088
|
+
function getProcessRegistry() {
|
|
1089
|
+
if (!registry) {
|
|
1090
|
+
registry = new ProcessRegistry();
|
|
1091
|
+
}
|
|
1092
|
+
return registry;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// packages/agent-runtime/localWorkspaceToolDefs.ts
|
|
1096
|
+
var WORKSPACE_TOOL_NAMES = [
|
|
1097
|
+
"listFiles",
|
|
1098
|
+
"readFile",
|
|
1099
|
+
"writeFile",
|
|
1100
|
+
"editFile",
|
|
1101
|
+
"globFiles",
|
|
1102
|
+
"searchFiles",
|
|
1103
|
+
"captureVisualState",
|
|
1104
|
+
"execShell",
|
|
1105
|
+
"launchProcess",
|
|
1106
|
+
"listProcesses"
|
|
1107
|
+
];
|
|
1108
|
+
var SHELL_TOOL_NAMES = ["execShell", "launchProcess", "listProcesses"];
|
|
1109
|
+
var WORKSPACE_TOOL_NAME_SET = new Set(WORKSPACE_TOOL_NAMES);
|
|
1110
|
+
var REMOVED_WORKSPACE_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
1111
|
+
"gitStatus",
|
|
1112
|
+
"gitDiff",
|
|
1113
|
+
"gitCreateBranch",
|
|
1114
|
+
"gitAdd",
|
|
1115
|
+
"gitCommit",
|
|
1116
|
+
"commitWorkspace"
|
|
1117
|
+
]);
|
|
1118
|
+
function tokenizeShellPrefix(command) {
|
|
1119
|
+
const tokens = [];
|
|
1120
|
+
const pattern = /"([^"]*)"|'([^']*)'|([^\s"'|;&<>]+)/g;
|
|
1121
|
+
let match;
|
|
1122
|
+
while (match = pattern.exec(command)) {
|
|
1123
|
+
tokens.push(match[1] ?? match[2] ?? match[3] ?? "");
|
|
1124
|
+
}
|
|
1125
|
+
return tokens;
|
|
1126
|
+
}
|
|
1127
|
+
function buildWorkspacePathProperty() {
|
|
1128
|
+
return {
|
|
1129
|
+
type: "string",
|
|
1130
|
+
description: "Path relative to the workspace root."
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
function buildListWorkspaceDescription(variant) {
|
|
1134
|
+
if (variant === "brief") {
|
|
1135
|
+
return "List files and directories inside a workspace directory.";
|
|
1136
|
+
}
|
|
1137
|
+
if (variant === "workflow") {
|
|
1138
|
+
return "List a bounded directory overview inside the workspace. Use first when you need to understand nearby structure, choose a subdirectory, or inspect a non-code project. Then use globFiles for path-pattern discovery, searchFiles for text/content discovery, and readFile only for the specific files or line ranges you need.";
|
|
1139
|
+
}
|
|
1140
|
+
if (variant === "antiShell") {
|
|
1141
|
+
return "List a bounded directory overview inside the workspace. Do not use execShell with ls/find/tree for normal directory overviews when listFiles can show the needed depth and limit. Use globFiles for path-pattern discovery and searchFiles for content search.";
|
|
1142
|
+
}
|
|
1143
|
+
return "List a bounded directory overview inside the workspace. Use for scanning nearby structure, not for path-pattern discovery across the repo. Prefer globFiles for finding files by name/extension and searchFiles for text inside files.";
|
|
1144
|
+
}
|
|
1145
|
+
function buildListWorkspaceParameters(variant) {
|
|
1146
|
+
const path = buildWorkspacePathProperty();
|
|
1147
|
+
const maxDepth = {
|
|
1148
|
+
type: "integer",
|
|
1149
|
+
description: "Maximum directory depth to include. Defaults to 1 for the requested directory only."
|
|
1150
|
+
};
|
|
1151
|
+
const maxResults = {
|
|
1152
|
+
type: "integer",
|
|
1153
|
+
description: "Maximum number of entries to return. Use a small value for large directories."
|
|
1154
|
+
};
|
|
1155
|
+
const entryType = {
|
|
1156
|
+
type: "string",
|
|
1157
|
+
enum: ["all", "files", "directories"],
|
|
1158
|
+
description: "Filter returned entries. Defaults to all."
|
|
1159
|
+
};
|
|
1160
|
+
if (variant === "minimal") {
|
|
1161
|
+
return {
|
|
1162
|
+
type: "object",
|
|
1163
|
+
properties: { path }
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
if (variant === "rich") {
|
|
1167
|
+
return {
|
|
1168
|
+
type: "object",
|
|
1169
|
+
properties: {
|
|
1170
|
+
path,
|
|
1171
|
+
maxDepth,
|
|
1172
|
+
maxResults,
|
|
1173
|
+
entryType
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
return {
|
|
1178
|
+
type: "object",
|
|
1179
|
+
properties: {
|
|
1180
|
+
path,
|
|
1181
|
+
maxResults
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
function buildListWorkspaceFilesTool(args) {
|
|
1186
|
+
return {
|
|
1187
|
+
type: "function",
|
|
1188
|
+
function: {
|
|
1189
|
+
name: "listFiles",
|
|
1190
|
+
description: buildListWorkspaceDescription(args?.descriptionVariant),
|
|
1191
|
+
parameters: buildListWorkspaceParameters(args?.parameterVariant)
|
|
1192
|
+
}
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
function buildReadWorkspaceDescription(variant) {
|
|
1196
|
+
if (variant === "brief") {
|
|
1197
|
+
return "Read a UTF-8 text file inside the workspace.";
|
|
1198
|
+
}
|
|
1199
|
+
if (variant === "workflow") {
|
|
1200
|
+
return "Read a UTF-8 text file inside the workspace after listFiles, globFiles, or searchFiles has narrowed the target. Use line ranges from searchFiles matches and tailLines for logs or generated output. Read the whole file only when the task truly needs all content or exact edit context.";
|
|
1201
|
+
}
|
|
1202
|
+
if (variant === "antiShell") {
|
|
1203
|
+
return "Read a UTF-8 text file inside the workspace. Do not use execShell with cat/sed/head/tail for normal text reads when readFile can return the needed file or line range. Use searchFiles first for content search, then readFile only the relevant lines when possible.";
|
|
1204
|
+
}
|
|
1205
|
+
return "Read a UTF-8 text file inside the workspace. Read before editing when you need exact text for editFile. For discovery or classification tasks, avoid batch-reading every candidate; report candidate paths first, or read only one to three representative files with maxLines or focused line ranges. Use line ranges for large code, docs, data, configs, and logs after searchFiles returns line numbers. Read the whole file only when the task truly needs all content or exact edit context.";
|
|
1206
|
+
}
|
|
1207
|
+
function buildReadWorkspaceParameters(variant) {
|
|
1208
|
+
const path = buildWorkspacePathProperty();
|
|
1209
|
+
const startLine = {
|
|
1210
|
+
type: "integer",
|
|
1211
|
+
description: "1-based first line to return. Use with endLine or maxLines after searchFiles gives a line number."
|
|
1212
|
+
};
|
|
1213
|
+
const endLine = {
|
|
1214
|
+
type: "integer",
|
|
1215
|
+
description: "1-based last line to return, inclusive."
|
|
1216
|
+
};
|
|
1217
|
+
const maxLines = {
|
|
1218
|
+
type: "integer",
|
|
1219
|
+
description: "Maximum number of lines to return starting at startLine, or from the start of the file when startLine is omitted. Use for lightweight previews of candidate configs, docs, data, and logs instead of reading many whole files. When the user gives a read/preview budget, each readFile preview consumes one budget slot."
|
|
1220
|
+
};
|
|
1221
|
+
const tailLines = {
|
|
1222
|
+
type: "integer",
|
|
1223
|
+
description: "Return only the last N lines, useful for logs and generated text."
|
|
1224
|
+
};
|
|
1225
|
+
if (variant === "minimal") {
|
|
1226
|
+
return {
|
|
1227
|
+
type: "object",
|
|
1228
|
+
properties: { path },
|
|
1229
|
+
required: ["path"]
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
if (variant === "rich") {
|
|
1233
|
+
return {
|
|
1234
|
+
type: "object",
|
|
1235
|
+
properties: {
|
|
1236
|
+
path,
|
|
1237
|
+
startLine,
|
|
1238
|
+
endLine,
|
|
1239
|
+
maxLines,
|
|
1240
|
+
tailLines
|
|
1241
|
+
},
|
|
1242
|
+
required: ["path"]
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
return {
|
|
1246
|
+
type: "object",
|
|
1247
|
+
properties: {
|
|
1248
|
+
path,
|
|
1249
|
+
startLine,
|
|
1250
|
+
endLine,
|
|
1251
|
+
maxLines
|
|
1252
|
+
},
|
|
1253
|
+
required: ["path"]
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
function buildReadWorkspaceFileTool(args) {
|
|
1257
|
+
return {
|
|
1258
|
+
type: "function",
|
|
1259
|
+
function: {
|
|
1260
|
+
name: "readFile",
|
|
1261
|
+
description: buildReadWorkspaceDescription(args?.descriptionVariant),
|
|
1262
|
+
parameters: buildReadWorkspaceParameters(args?.parameterVariant)
|
|
1263
|
+
}
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
function buildWriteWorkspaceFileTool() {
|
|
1267
|
+
return {
|
|
1268
|
+
type: "function",
|
|
1269
|
+
function: {
|
|
1270
|
+
name: "writeFile",
|
|
1271
|
+
description: "Write full UTF-8 file content inside the workspace. For new files or deliberate whole-file rewrites only. For existing files, prefer editFile for targeted edits. Warn that whole-file rewrites can cause line-ending churn.",
|
|
1272
|
+
parameters: {
|
|
1273
|
+
type: "object",
|
|
1274
|
+
properties: {
|
|
1275
|
+
path: buildWorkspacePathProperty(),
|
|
1276
|
+
content: {
|
|
1277
|
+
type: "string",
|
|
1278
|
+
description: "Full UTF-8 file content to write."
|
|
1279
|
+
}
|
|
1280
|
+
},
|
|
1281
|
+
required: ["path", "content"]
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
function buildReplaceWorkspaceTextTool() {
|
|
1287
|
+
return {
|
|
1288
|
+
type: "function",
|
|
1289
|
+
function: {
|
|
1290
|
+
name: "editFile",
|
|
1291
|
+
description: "Use for small, exact edits in one workspace file without constructing a patch. Read the file first when you need the exact oldText; set expectedReplacements to avoid accidental broad edits. When expected replacement count fails, report a blocker instead of falling back to a full-file rewrite.",
|
|
1292
|
+
parameters: {
|
|
1293
|
+
type: "object",
|
|
1294
|
+
properties: {
|
|
1295
|
+
path: buildWorkspacePathProperty(),
|
|
1296
|
+
oldText: {
|
|
1297
|
+
type: "string",
|
|
1298
|
+
description: "Exact text currently present in the file."
|
|
1299
|
+
},
|
|
1300
|
+
newText: {
|
|
1301
|
+
type: "string",
|
|
1302
|
+
description: "Replacement text to write in place of oldText."
|
|
1303
|
+
},
|
|
1304
|
+
expectedReplacements: {
|
|
1305
|
+
type: "integer",
|
|
1306
|
+
description: "Expected replacement count. Defaults to 1."
|
|
1307
|
+
}
|
|
1308
|
+
},
|
|
1309
|
+
required: ["path", "oldText", "newText"]
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
function buildSearchWorkspaceDescription(variant) {
|
|
1315
|
+
if (variant === "brief") {
|
|
1316
|
+
return "Search workspace file contents.";
|
|
1317
|
+
}
|
|
1318
|
+
if (variant === "workflow") {
|
|
1319
|
+
return "Search file contents inside the current workspace after listFiles or globFiles has scoped the likely area, or directly when the task names text to find. Use for code, docs, data, configs, logs, and other text files. Return line numbers, then use readFile with focused ranges for interpretation or editing.";
|
|
1320
|
+
}
|
|
1321
|
+
if (variant === "antiShell") {
|
|
1322
|
+
return "Search text inside workspace files for code, docs, data, configs, logs, or other text-like assets. Do not use execShell for content search when searchFiles can directly find matching lines. Use globFiles instead for path-only discovery, then readFile for exact surrounding content when needed.";
|
|
1323
|
+
}
|
|
1324
|
+
return "Search file contents inside the current workspace using ripgrep when available. Use for grep-like text or regex search across code, docs, data, configs, logs, and other text files. Use globFiles instead when you only need to find files by name or extension before readFile.";
|
|
1325
|
+
}
|
|
1326
|
+
function buildSearchWorkspaceParameters(variant) {
|
|
1327
|
+
const query = {
|
|
1328
|
+
type: "string",
|
|
1329
|
+
description: "Search query. Treated as a regular expression unless literal is true."
|
|
1330
|
+
};
|
|
1331
|
+
const path = buildWorkspacePathProperty();
|
|
1332
|
+
const includeIgnored = {
|
|
1333
|
+
type: "boolean",
|
|
1334
|
+
description: "When true, search files ignored by .gitignore such as .tmp. Defaults to false; .git and node_modules remain excluded."
|
|
1335
|
+
};
|
|
1336
|
+
const exclude = {
|
|
1337
|
+
type: "array",
|
|
1338
|
+
items: { type: "string" },
|
|
1339
|
+
description: "Glob patterns to exclude from content search, for example ['dist/**', 'build/**', 'exports/**', '*.log']."
|
|
1340
|
+
};
|
|
1341
|
+
const maxResults = {
|
|
1342
|
+
type: "integer",
|
|
1343
|
+
description: "Maximum number of matching output lines to return. Use a small value when you only need candidate files or examples."
|
|
1344
|
+
};
|
|
1345
|
+
const literal = {
|
|
1346
|
+
type: "boolean",
|
|
1347
|
+
description: "When true, search for query as literal text instead of a regular expression."
|
|
1348
|
+
};
|
|
1349
|
+
const caseSensitive = {
|
|
1350
|
+
type: "boolean",
|
|
1351
|
+
description: "When false, search case-insensitively. Defaults to true."
|
|
1352
|
+
};
|
|
1353
|
+
const contextLines = {
|
|
1354
|
+
type: "integer",
|
|
1355
|
+
description: "Number of surrounding context lines to include around each match. Defaults to 0."
|
|
1356
|
+
};
|
|
1357
|
+
if (variant === "minimal") {
|
|
1358
|
+
return {
|
|
1359
|
+
type: "object",
|
|
1360
|
+
properties: { query },
|
|
1361
|
+
required: ["query"]
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
if (variant === "rich") {
|
|
1365
|
+
return {
|
|
1366
|
+
type: "object",
|
|
1367
|
+
properties: {
|
|
1368
|
+
query,
|
|
1369
|
+
path,
|
|
1370
|
+
exclude,
|
|
1371
|
+
includeIgnored,
|
|
1372
|
+
maxResults,
|
|
1373
|
+
literal,
|
|
1374
|
+
caseSensitive,
|
|
1375
|
+
contextLines
|
|
1376
|
+
},
|
|
1377
|
+
required: ["query"]
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
return {
|
|
1381
|
+
type: "object",
|
|
1382
|
+
properties: {
|
|
1383
|
+
query,
|
|
1384
|
+
path,
|
|
1385
|
+
includeIgnored,
|
|
1386
|
+
maxResults
|
|
1387
|
+
},
|
|
1388
|
+
required: ["query"]
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
function buildSearchWorkspaceTool(args) {
|
|
1392
|
+
return {
|
|
1393
|
+
type: "function",
|
|
1394
|
+
function: {
|
|
1395
|
+
name: "searchFiles",
|
|
1396
|
+
description: buildSearchWorkspaceDescription(args?.descriptionVariant),
|
|
1397
|
+
parameters: buildSearchWorkspaceParameters(args?.parameterVariant)
|
|
1398
|
+
}
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
function buildGlobWorkspaceDescription(variant) {
|
|
1402
|
+
if (variant === "brief") {
|
|
1403
|
+
return "Find workspace files by path glob without reading file contents.";
|
|
1404
|
+
}
|
|
1405
|
+
if (variant === "workflow") {
|
|
1406
|
+
return "Find files by glob pattern inside the current workspace without reading file contents. Use after listFiles when you know the likely area, or first when the task names a filename, extension, asset type, config, or document pattern. Then use searchFiles for text inside candidates or readFile for the specific candidate paths.";
|
|
1407
|
+
}
|
|
1408
|
+
if (variant === "antiShell") {
|
|
1409
|
+
return "Find workspace files by path glob without reading file contents. Use for code, docs, data, images, configs, logs, or any file discovery task. Do not use execShell or listFiles for path discovery when globFiles can directly narrow candidate files. Use before opening or reading files, and use searchFiles instead for searching text inside files.";
|
|
1410
|
+
}
|
|
1411
|
+
return "Find files by glob pattern inside the current workspace without reading file contents. Use for code, docs, data, images, configs, logs, or any file discovery task. Use one bounded glob with brace groups before repeated narrow globs when a task names several extensions or config names, for example **/*.{png,jpg,svg}, **/{package.json,bunfig.toml,tsconfig*.json}, or src/**/*.ts. Prefer searchFiles for searching text inside files, and readFile only after candidate paths are narrow enough.";
|
|
1412
|
+
}
|
|
1413
|
+
function buildGlobWorkspaceParameters(variant) {
|
|
1414
|
+
const pattern = {
|
|
1415
|
+
type: "string",
|
|
1416
|
+
description: "Glob pattern for files. For candidate discovery, combine related names or extensions in one brace-group pattern before making repeated narrow calls, for example '**/*.ts', 'packages/**/local*.test.ts', '**/*.{png,jpg,svg}', or '**/{package.json,bunfig.toml,tsconfig*.json}'."
|
|
1417
|
+
};
|
|
1418
|
+
const path = buildWorkspacePathProperty();
|
|
1419
|
+
const includeIgnored = {
|
|
1420
|
+
type: "boolean",
|
|
1421
|
+
description: "When true, include files ignored by .gitignore. Defaults to false; .git and node_modules remain excluded."
|
|
1422
|
+
};
|
|
1423
|
+
const maxResults = {
|
|
1424
|
+
type: "integer",
|
|
1425
|
+
description: "Maximum number of file paths to return. Use a small value when you only need candidate paths before readFile; if results are truncated, report that and narrow the pattern or path before reading."
|
|
1426
|
+
};
|
|
1427
|
+
const exclude = {
|
|
1428
|
+
type: "array",
|
|
1429
|
+
items: { type: "string" },
|
|
1430
|
+
description: "Glob patterns to exclude from results, for example ['dist/**', 'build/**', 'exports/**', '*.tmp']."
|
|
1431
|
+
};
|
|
1432
|
+
if (variant === "minimal") {
|
|
1433
|
+
return {
|
|
1434
|
+
type: "object",
|
|
1435
|
+
properties: { pattern },
|
|
1436
|
+
required: ["pattern"]
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
if (variant === "rich") {
|
|
1440
|
+
return {
|
|
1441
|
+
type: "object",
|
|
1442
|
+
properties: {
|
|
1443
|
+
pattern,
|
|
1444
|
+
path,
|
|
1445
|
+
exclude,
|
|
1446
|
+
includeIgnored,
|
|
1447
|
+
maxResults
|
|
1448
|
+
},
|
|
1449
|
+
required: ["pattern"]
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
return {
|
|
1453
|
+
type: "object",
|
|
1454
|
+
properties: {
|
|
1455
|
+
pattern,
|
|
1456
|
+
glob: {
|
|
1457
|
+
type: "string",
|
|
1458
|
+
description: "Alias for pattern, kept for compatibility with codeSearch-style prompts."
|
|
1459
|
+
},
|
|
1460
|
+
path,
|
|
1461
|
+
exclude,
|
|
1462
|
+
includeIgnored,
|
|
1463
|
+
maxResults
|
|
1464
|
+
}
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
function buildGlobWorkspaceFilesTool(args) {
|
|
1468
|
+
return {
|
|
1469
|
+
type: "function",
|
|
1470
|
+
function: {
|
|
1471
|
+
name: "globFiles",
|
|
1472
|
+
description: buildGlobWorkspaceDescription(args?.descriptionVariant),
|
|
1473
|
+
parameters: buildGlobWorkspaceParameters(args?.parameterVariant)
|
|
1474
|
+
}
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
function buildCaptureVisualStateTool() {
|
|
1478
|
+
return {
|
|
1479
|
+
type: "function",
|
|
1480
|
+
function: {
|
|
1481
|
+
name: "captureVisualState",
|
|
1482
|
+
description: "Capture a real local app screenshot and DOM/computed-style metrics for a selected UI state.",
|
|
1483
|
+
parameters: {
|
|
1484
|
+
type: "object",
|
|
1485
|
+
properties: {
|
|
1486
|
+
baseUrl: {
|
|
1487
|
+
type: "string",
|
|
1488
|
+
description: "Optional local app base URL. Defaults to http://127.0.0.1:38123."
|
|
1489
|
+
},
|
|
1490
|
+
path: {
|
|
1491
|
+
type: "string",
|
|
1492
|
+
description: "App route to open, for example / or /dialog-123. Defaults to /."
|
|
1493
|
+
},
|
|
1494
|
+
waitSelector: {
|
|
1495
|
+
type: "string",
|
|
1496
|
+
description: "CSS selector that must become visible before capture."
|
|
1497
|
+
},
|
|
1498
|
+
scrollSelector: {
|
|
1499
|
+
type: "string",
|
|
1500
|
+
description: "Optional CSS selector to scroll into view before capture."
|
|
1501
|
+
},
|
|
1502
|
+
focusSelector: {
|
|
1503
|
+
type: "string",
|
|
1504
|
+
description: "Optional CSS selector for the target element whose rect/style should be reported."
|
|
1505
|
+
},
|
|
1506
|
+
expectText: {
|
|
1507
|
+
type: "string",
|
|
1508
|
+
description: "Optional visible text expected on the page before capture."
|
|
1509
|
+
},
|
|
1510
|
+
screenshotPath: {
|
|
1511
|
+
type: "string",
|
|
1512
|
+
description: "Workspace-relative screenshot path. Defaults under test-results/frontend-agent/."
|
|
1513
|
+
},
|
|
1514
|
+
metricsPath: {
|
|
1515
|
+
type: "string",
|
|
1516
|
+
description: "Workspace-relative metrics JSON path. Defaults under test-results/frontend-agent/."
|
|
1517
|
+
}
|
|
1518
|
+
},
|
|
1519
|
+
required: ["waitSelector"]
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
function buildExecShellTool(toolName) {
|
|
1525
|
+
return {
|
|
1526
|
+
type: "function",
|
|
1527
|
+
function: {
|
|
1528
|
+
name: toolName,
|
|
1529
|
+
description: "Run a shell command in the local workspace. Commands already execute from the workspace root; do not cd into guessed paths such as /workspace, /repo, /home/user, or /home/user/workspace. Prefer one command that performs a complete verification or related git operation instead of many tiny commands. For branch setup, prefer idempotent commands such as git switch -C <branch> when replacing or recreating a benchmark branch is acceptable. Use portable POSIX commands for macOS/BSD shells; avoid GNU-only flags such as cat -A. Do not use cat -A. Do not use brittle byte-offset commands such as xxd -s -32. For separator lines, use echo or printf '%s\\n'. For text-file content or trailing-newline checks, prefer readFile over shell byte inspection when that tool is available. Do not run repeated git status, git log, git rev-parse, or branch checks after a successful command already returned the needed clean status and commit information; use one final verification command. If a command fails, use the error output to adjust the next command rather than repeating the same shape. For long-running commands that should keep running in the background (dev servers, watchers, REPLs, `--watch`/`serve`/`dev` scripts), use launchProcess instead \u2014 execShell blocks until the command exits and will freeze the conversation.",
|
|
1530
|
+
parameters: {
|
|
1531
|
+
type: "object",
|
|
1532
|
+
properties: {
|
|
1533
|
+
cmd: {
|
|
1534
|
+
type: "string",
|
|
1535
|
+
description: "Shell command to run."
|
|
1536
|
+
},
|
|
1537
|
+
command: {
|
|
1538
|
+
type: "string",
|
|
1539
|
+
description: "Shell command to run."
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
function buildLaunchProcessTool() {
|
|
1547
|
+
return {
|
|
1548
|
+
type: "function",
|
|
1549
|
+
function: {
|
|
1550
|
+
name: "launchProcess",
|
|
1551
|
+
description: "Launch a long-running process (dev server, watcher, REPL) in the background and return immediately without blocking the conversation. Use this instead of execShell for commands that do not exit on their own. Returns {pid, label, status:'running'}. The process keeps running after the tool returns; use listProcesses to check status or the host UI (/procs, /stop) to stop it.",
|
|
1552
|
+
parameters: {
|
|
1553
|
+
type: "object",
|
|
1554
|
+
properties: {
|
|
1555
|
+
command: {
|
|
1556
|
+
type: "string",
|
|
1557
|
+
description: "Shell command to run in background."
|
|
1558
|
+
},
|
|
1559
|
+
label: {
|
|
1560
|
+
type: "string",
|
|
1561
|
+
description: "Optional friendly label for the process."
|
|
1562
|
+
}
|
|
1563
|
+
},
|
|
1564
|
+
required: ["command"]
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
function buildListProcessesTool() {
|
|
1570
|
+
return {
|
|
1571
|
+
type: "function",
|
|
1572
|
+
function: {
|
|
1573
|
+
name: "listProcesses",
|
|
1574
|
+
description: "List currently registered background processes launched via launchProcess. Returns array of {pid, label, command, status, startedAt}.",
|
|
1575
|
+
parameters: {
|
|
1576
|
+
type: "object",
|
|
1577
|
+
properties: {}
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
function wrapPowerShellCommand(command) {
|
|
1583
|
+
return [
|
|
1584
|
+
"[Console]::InputEncoding=[System.Text.Encoding]::UTF8",
|
|
1585
|
+
"[Console]::OutputEncoding=[System.Text.Encoding]::UTF8",
|
|
1586
|
+
"$OutputEncoding=[System.Text.Encoding]::UTF8",
|
|
1587
|
+
"$PSStyle.OutputRendering='PlainText'",
|
|
1588
|
+
command
|
|
1589
|
+
].join("; ");
|
|
1590
|
+
}
|
|
1591
|
+
function findPowerShellExecutable() {
|
|
1592
|
+
return resolveExecutableOnPath("pwsh") || resolveExecutableOnPath("powershell.exe") || resolveExecutableOnPath("powershell");
|
|
1593
|
+
}
|
|
1594
|
+
function buildPowerShellCommand(command) {
|
|
1595
|
+
const executable = findPowerShellExecutable();
|
|
1596
|
+
if (!executable) throw new Error("PowerShell is not available on this machine.");
|
|
1597
|
+
return [
|
|
1598
|
+
executable,
|
|
1599
|
+
"-NoLogo",
|
|
1600
|
+
"-NoProfile",
|
|
1601
|
+
"-NonInteractive",
|
|
1602
|
+
"-ExecutionPolicy",
|
|
1603
|
+
"Bypass",
|
|
1604
|
+
"-Command",
|
|
1605
|
+
wrapPowerShellCommand(command)
|
|
1606
|
+
];
|
|
1607
|
+
}
|
|
1608
|
+
function buildBashCommand(command) {
|
|
1609
|
+
const executable = resolveExecutableOnPath("bash") || resolveExecutableOnPath("sh");
|
|
1610
|
+
if (!executable) throw new Error("bash/sh is not available on this machine.");
|
|
1611
|
+
return [executable, "-lc", command];
|
|
1612
|
+
}
|
|
1613
|
+
function buildWorkspaceShellCommand(args) {
|
|
1614
|
+
if (args.shell === "powershell") return buildPowerShellCommand(args.command);
|
|
1615
|
+
if (args.shell === "bash") return buildBashCommand(args.command);
|
|
1616
|
+
return process.platform === "win32" ? buildPowerShellCommand(args.command) : buildBashCommand(args.command);
|
|
1617
|
+
}
|
|
1618
|
+
function findWorkspaceShellEscapeToken(command) {
|
|
1619
|
+
const tokens = tokenizeShellPrefix(command);
|
|
1620
|
+
for (const token of tokens) {
|
|
1621
|
+
if (token === ".." || token.startsWith("../") || token.startsWith("..\\") || token.includes("/../") || token.includes("\\..\\")) {
|
|
1622
|
+
return token;
|
|
1623
|
+
}
|
|
1624
|
+
if (token === "~" || token.startsWith("~/") || token.startsWith("~\\")) {
|
|
1625
|
+
return token;
|
|
1626
|
+
}
|
|
1627
|
+
if (token === "/" || token.startsWith("/") || /^[A-Za-z]:[\\/]/.test(token) || token.startsWith("\\\\")) {
|
|
1628
|
+
return token;
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
return null;
|
|
1632
|
+
}
|
|
1633
|
+
function buildWorkspaceShellEscapeBlockedResult(args) {
|
|
1634
|
+
return {
|
|
1635
|
+
content: [
|
|
1636
|
+
"workspace_shell_escape_blocked",
|
|
1637
|
+
`blockedToken: ${args.token}`,
|
|
1638
|
+
`command: ${args.command}`,
|
|
1639
|
+
"Use paths relative to the authorized folder only.",
|
|
1640
|
+
"exitCode: 126"
|
|
1641
|
+
].join("\n"),
|
|
1642
|
+
metadata: {
|
|
1643
|
+
exitCode: 126,
|
|
1644
|
+
workspaceShellEscapeBlocked: true,
|
|
1645
|
+
blockedToken: args.token
|
|
1646
|
+
}
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
function buildWorkspaceToolDefinition(toolName, args) {
|
|
1650
|
+
if (toolName === "listFiles") {
|
|
1651
|
+
return buildListWorkspaceFilesTool({
|
|
1652
|
+
descriptionVariant: args?.listFilesDescriptionVariant,
|
|
1653
|
+
parameterVariant: args?.listFilesParameterVariant
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
if (toolName === "readFile") {
|
|
1657
|
+
return buildReadWorkspaceFileTool({
|
|
1658
|
+
descriptionVariant: args?.readFileDescriptionVariant,
|
|
1659
|
+
parameterVariant: args?.readFileParameterVariant
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
if (toolName === "writeFile") {
|
|
1663
|
+
return buildWriteWorkspaceFileTool();
|
|
1664
|
+
}
|
|
1665
|
+
if (toolName === "editFile") {
|
|
1666
|
+
return buildReplaceWorkspaceTextTool();
|
|
1667
|
+
}
|
|
1668
|
+
if (toolName === "globFiles") {
|
|
1669
|
+
return buildGlobWorkspaceFilesTool({
|
|
1670
|
+
descriptionVariant: args?.globFilesDescriptionVariant,
|
|
1671
|
+
parameterVariant: args?.globFilesParameterVariant
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
if (toolName === "searchFiles") {
|
|
1675
|
+
return buildSearchWorkspaceTool({
|
|
1676
|
+
descriptionVariant: args?.searchFilesDescriptionVariant,
|
|
1677
|
+
parameterVariant: args?.searchFilesParameterVariant
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
if (toolName === "captureVisualState") return buildCaptureVisualStateTool();
|
|
1681
|
+
if (toolName === "execShell") return buildExecShellTool(toolName);
|
|
1682
|
+
if (toolName === "launchProcess") return buildLaunchProcessTool();
|
|
1683
|
+
if (toolName === "listProcesses") return buildListProcessesTool();
|
|
1684
|
+
return null;
|
|
1685
|
+
}
|
|
1686
|
+
function filterDeclaredWorkspaceToolNames(args) {
|
|
1687
|
+
return (args.toolNames ?? []).filter(
|
|
1688
|
+
(toolName) => WORKSPACE_TOOL_NAME_SET.has(toolName) && !REMOVED_WORKSPACE_TOOL_NAMES.has(toolName) && (args.exposeShellTools || !SHELL_TOOL_NAMES.includes(toolName))
|
|
1689
|
+
);
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
// packages/agent-runtime/localWorkspaceTools.ts
|
|
1693
|
+
var EXEC_SHELL_TIMEOUT_ENV = "NOLO_EXEC_SHELL_TIMEOUT_MS";
|
|
1694
|
+
var EXEC_SHELL_DETACH_ENV = "NOLO_EXEC_SHELL_DETACH_MS";
|
|
1695
|
+
var DEFAULT_EXEC_SHELL_DETACH_MS = 12e4;
|
|
1696
|
+
var DEFAULT_LOCAL_API_ORIGIN = "http://127.0.0.1:38123";
|
|
1697
|
+
function readTrimmedString(value) {
|
|
1698
|
+
return asOptionalTrimmedString(value);
|
|
1699
|
+
}
|
|
1700
|
+
function resolveExecShellTimeoutMs(override) {
|
|
1701
|
+
const fromOverride = asOptionalPositiveFiniteNumber(override);
|
|
1702
|
+
if (fromOverride !== void 0) return fromOverride;
|
|
1703
|
+
const raw = process.env[EXEC_SHELL_TIMEOUT_ENV];
|
|
1704
|
+
if (raw === void 0) return void 0;
|
|
1705
|
+
return asOptionalPositiveFiniteNumber(Number(raw));
|
|
1706
|
+
}
|
|
1707
|
+
function resolveDetachMs(override) {
|
|
1708
|
+
const fromOverride = asOptionalPositiveFiniteNumber(override);
|
|
1709
|
+
if (fromOverride !== void 0) return fromOverride;
|
|
1710
|
+
const raw = process.env[EXEC_SHELL_DETACH_ENV];
|
|
1711
|
+
if (raw === void 0) return DEFAULT_EXEC_SHELL_DETACH_MS;
|
|
1712
|
+
const parsed = asOptionalPositiveFiniteNumber(Number(raw));
|
|
1713
|
+
return parsed === void 0 ? DEFAULT_EXEC_SHELL_DETACH_MS : parsed;
|
|
1714
|
+
}
|
|
1715
|
+
function extractInteractiveGhAuthCommand(command) {
|
|
1716
|
+
const tokens = tokenizeShellPrefix(command);
|
|
1717
|
+
if (tokens[0] !== "gh" || tokens[1] !== "auth") return null;
|
|
1718
|
+
const subcommand = tokens[2];
|
|
1719
|
+
if (subcommand !== "login" && subcommand !== "refresh") return null;
|
|
1720
|
+
if (tokens.includes("--help")) return null;
|
|
1721
|
+
const result = ["gh", "auth", subcommand];
|
|
1722
|
+
for (let index = 3; index < tokens.length; index += 1) {
|
|
1723
|
+
const token = tokens[index];
|
|
1724
|
+
if (!token) continue;
|
|
1725
|
+
if (token === "-h" || token === "--hostname" || token === "-s" || token === "--scopes" || token === "--remove-scopes" || token === "-r") {
|
|
1726
|
+
const value = tokens[index + 1];
|
|
1727
|
+
if (value) {
|
|
1728
|
+
result.push(token, value);
|
|
1729
|
+
index += 1;
|
|
1730
|
+
}
|
|
1731
|
+
continue;
|
|
1732
|
+
}
|
|
1733
|
+
if (token === "--clipboard" || token === "-c" || token === "--insecure-storage" || token === "--reset-scopes") {
|
|
1734
|
+
result.push(token);
|
|
1735
|
+
continue;
|
|
1736
|
+
}
|
|
1737
|
+
if (token.startsWith("--hostname=") || token.startsWith("--scopes=") || token.startsWith("--remove-scopes=")) {
|
|
1738
|
+
result.push(token);
|
|
1739
|
+
continue;
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
return result.join(" ");
|
|
1743
|
+
}
|
|
1744
|
+
function splitShellWords(command) {
|
|
1745
|
+
return tokenizeShellPrefix(command);
|
|
1746
|
+
}
|
|
1747
|
+
function buildInteractiveCommandBlockedResult(command) {
|
|
1748
|
+
const argv = splitShellWords(command);
|
|
1749
|
+
return {
|
|
1750
|
+
content: [
|
|
1751
|
+
"action_gate: handoff",
|
|
1752
|
+
`command: ${command}`,
|
|
1753
|
+
"Run this in the current TUI terminal, then resume the agent turn.",
|
|
1754
|
+
"exitCode: 130"
|
|
1755
|
+
].join("\n"),
|
|
1756
|
+
metadata: {
|
|
1757
|
+
exitCode: 130,
|
|
1758
|
+
timedOut: false,
|
|
1759
|
+
actionGate: {
|
|
1760
|
+
id: `gate-${Date.now().toString(36)}-terminal-handoff`,
|
|
1761
|
+
kind: "handoff",
|
|
1762
|
+
title: "This command requires an interactive terminal.",
|
|
1763
|
+
body: "Complete it in the terminal, then nolo will continue.",
|
|
1764
|
+
payload: {
|
|
1765
|
+
command: argv,
|
|
1766
|
+
displayCommand: command
|
|
1767
|
+
}
|
|
1768
|
+
},
|
|
1769
|
+
reason: "interactive-command-requires-terminal"
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1773
|
+
function extractActivityRefs(rawRefs) {
|
|
1774
|
+
if (!Array.isArray(rawRefs)) return void 0;
|
|
1775
|
+
const refs = rawRefs.flatMap((entry) => {
|
|
1776
|
+
if (!isRecord(entry)) return [];
|
|
1777
|
+
if (entry.type === "file") {
|
|
1778
|
+
const path = readTrimmedString(entry.path);
|
|
1779
|
+
return path ? [{ type: "file", path }] : [];
|
|
1780
|
+
}
|
|
1781
|
+
if (entry.type === "terminal") {
|
|
1782
|
+
const id = readTrimmedString(entry.id);
|
|
1783
|
+
const label = readTrimmedString(entry.label);
|
|
1784
|
+
return id || label ? [{ type: "terminal", ...id ? { id } : {}, ...label ? { label } : {} }] : [];
|
|
1785
|
+
}
|
|
1786
|
+
if (entry.type === "url") {
|
|
1787
|
+
const url = readTrimmedString(entry.url);
|
|
1788
|
+
const label = readTrimmedString(entry.label);
|
|
1789
|
+
return url ? [{ type: "url", url, ...label ? { label } : {} }] : [];
|
|
1790
|
+
}
|
|
1791
|
+
return [];
|
|
1792
|
+
});
|
|
1793
|
+
return refs.length ? refs : void 0;
|
|
1794
|
+
}
|
|
1795
|
+
function extractActivityAction(rawAction) {
|
|
1796
|
+
if (!isRecord(rawAction)) return void 0;
|
|
1797
|
+
const title = readTrimmedString(rawAction.title);
|
|
1798
|
+
if (!title) return void 0;
|
|
1799
|
+
const kind = readTrimmedString(rawAction.kind);
|
|
1800
|
+
const detail = readTrimmedString(rawAction.detail);
|
|
1801
|
+
const refs = extractActivityRefs(rawAction.refs);
|
|
1802
|
+
return {
|
|
1803
|
+
title,
|
|
1804
|
+
...kind ? { kind } : {},
|
|
1805
|
+
...detail ? { detail } : {},
|
|
1806
|
+
...refs ? { refs } : {}
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
function extractActivityPhase(rawPhase) {
|
|
1810
|
+
if (!isRecord(rawPhase)) return void 0;
|
|
1811
|
+
const title = readTrimmedString(rawPhase.title);
|
|
1812
|
+
if (!title) return void 0;
|
|
1813
|
+
const id = readTrimmedString(rawPhase.id) || title.toLowerCase().replace(/\s+/g, "-");
|
|
1814
|
+
const index = asOptionalFiniteNumber(rawPhase.index);
|
|
1815
|
+
const total = asOptionalFiniteNumber(rawPhase.total);
|
|
1816
|
+
const status = rawPhase.status === "pending" || rawPhase.status === "running" || rawPhase.status === "success" || rawPhase.status === "failed" ? rawPhase.status : void 0;
|
|
1817
|
+
return {
|
|
1818
|
+
id,
|
|
1819
|
+
title,
|
|
1820
|
+
...index !== void 0 ? { index } : {},
|
|
1821
|
+
...total !== void 0 ? { total } : {},
|
|
1822
|
+
...status ? { status } : {}
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
function extractActivityPlan(rawPlan) {
|
|
1826
|
+
if (!isRecord(rawPlan)) return void 0;
|
|
1827
|
+
if (!Array.isArray(rawPlan.phases)) return void 0;
|
|
1828
|
+
const phases = rawPlan.phases.flatMap((entry, index) => {
|
|
1829
|
+
const phase = extractActivityPhase(entry);
|
|
1830
|
+
if (!phase) return [];
|
|
1831
|
+
return [{
|
|
1832
|
+
id: phase.id,
|
|
1833
|
+
title: phase.title,
|
|
1834
|
+
index: phase.index ?? index + 1,
|
|
1835
|
+
...phase.status ? { status: phase.status } : {}
|
|
1836
|
+
}];
|
|
1837
|
+
});
|
|
1838
|
+
if (phases.length === 0) return void 0;
|
|
1839
|
+
const title = readTrimmedString(rawPlan.title);
|
|
1840
|
+
return {
|
|
1841
|
+
...title ? { title } : {},
|
|
1842
|
+
phases
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
function extractActivity(parsed) {
|
|
1846
|
+
const raw = parsed._activity;
|
|
1847
|
+
if (!isRecord(raw)) return void 0;
|
|
1848
|
+
const nestedAction = extractActivityAction(raw.action);
|
|
1849
|
+
const legacyAction = extractActivityAction(raw);
|
|
1850
|
+
const action = nestedAction || legacyAction;
|
|
1851
|
+
const phase = extractActivityPhase(raw.phase);
|
|
1852
|
+
const plan = extractActivityPlan(raw.plan);
|
|
1853
|
+
if (!action && !plan) return void 0;
|
|
1854
|
+
return {
|
|
1855
|
+
...action ? action : {},
|
|
1856
|
+
...phase ? { phase } : {},
|
|
1857
|
+
...nestedAction ? { action: nestedAction } : {},
|
|
1858
|
+
...plan ? { plan } : {}
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
function buildLocalWorkspaceToolset(args) {
|
|
1862
|
+
const exposeShellTools = args.exposeShellTools === true;
|
|
1863
|
+
if (args.useDeclaredToolNamesOnly) {
|
|
1864
|
+
const toolNames2 = new Set(filterDeclaredWorkspaceToolNames({
|
|
1865
|
+
toolNames: args.declaredToolNames,
|
|
1866
|
+
exposeShellTools
|
|
1867
|
+
}));
|
|
1868
|
+
return {
|
|
1869
|
+
toolNames: [...toolNames2],
|
|
1870
|
+
exposeShellTools
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1873
|
+
const toolNames = /* @__PURE__ */ new Set([
|
|
1874
|
+
...exposeShellTools ? SHELL_TOOL_NAMES : [],
|
|
1875
|
+
...filterDeclaredWorkspaceToolNames({
|
|
1876
|
+
toolNames: args.declaredToolNames,
|
|
1877
|
+
exposeShellTools
|
|
1878
|
+
})
|
|
1879
|
+
]);
|
|
1880
|
+
return {
|
|
1881
|
+
toolNames: [...toolNames],
|
|
1882
|
+
exposeShellTools
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
function buildLocalWorkspacePolicyToolNames(args) {
|
|
1886
|
+
if (args.useDeclaredToolNamesOnly) {
|
|
1887
|
+
return filterDeclaredWorkspaceToolNames({
|
|
1888
|
+
toolNames: args.declaredToolNames,
|
|
1889
|
+
exposeShellTools: args.exposeShellTools === true
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
return [.../* @__PURE__ */ new Set([
|
|
1893
|
+
...buildLocalWorkspaceToolset({
|
|
1894
|
+
exposeShellTools: args.exposeShellTools
|
|
1895
|
+
}).toolNames,
|
|
1896
|
+
...filterDeclaredWorkspaceToolNames({
|
|
1897
|
+
toolNames: args.declaredToolNames,
|
|
1898
|
+
exposeShellTools: args.exposeShellTools === true
|
|
1899
|
+
})
|
|
1900
|
+
])].filter((toolName) => !REMOVED_WORKSPACE_TOOL_NAMES.has(toolName));
|
|
1901
|
+
}
|
|
1902
|
+
function buildLocalWorkspaceOpenAiTools(args) {
|
|
1903
|
+
const declaredTools = new Set(args.toolNames ?? []);
|
|
1904
|
+
return WORKSPACE_TOOL_NAMES.filter((toolName) => {
|
|
1905
|
+
if (!declaredTools.has(toolName)) return false;
|
|
1906
|
+
if (!args.exposeShellTools && SHELL_TOOL_NAMES.includes(toolName)) {
|
|
1907
|
+
return false;
|
|
1908
|
+
}
|
|
1909
|
+
return true;
|
|
1910
|
+
}).map((toolName) => buildWorkspaceToolDefinition(toolName, {
|
|
1911
|
+
listFilesDescriptionVariant: args.listFilesDescriptionVariant,
|
|
1912
|
+
listFilesParameterVariant: args.listFilesParameterVariant,
|
|
1913
|
+
readFileDescriptionVariant: args.readFileDescriptionVariant,
|
|
1914
|
+
readFileParameterVariant: args.readFileParameterVariant,
|
|
1915
|
+
globFilesDescriptionVariant: args.globFilesDescriptionVariant,
|
|
1916
|
+
globFilesParameterVariant: args.globFilesParameterVariant,
|
|
1917
|
+
searchFilesDescriptionVariant: args.searchFilesDescriptionVariant,
|
|
1918
|
+
searchFilesParameterVariant: args.searchFilesParameterVariant
|
|
1919
|
+
})).filter((tool) => Boolean(tool));
|
|
1920
|
+
}
|
|
1921
|
+
function parseWorkspaceToolArguments(raw) {
|
|
1922
|
+
try {
|
|
1923
|
+
return asRecordOrEmpty(JSON.parse(raw || "{}"));
|
|
1924
|
+
} catch {
|
|
1925
|
+
return {};
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
function normalizeWorkspaceRelativePath(args) {
|
|
1929
|
+
const relativePath = relative(args.workspaceRoot, args.targetPath);
|
|
1930
|
+
return relativePath || ".";
|
|
1931
|
+
}
|
|
1932
|
+
function isPathInsideWorkspace(args) {
|
|
1933
|
+
const relativePath = relative(args.workspaceRoot, args.targetPath);
|
|
1934
|
+
return relativePath === "" || !relativePath.startsWith("..") && !relativePath.includes(`..${sep}`);
|
|
1935
|
+
}
|
|
1936
|
+
function requireWorkspaceToolPath(args) {
|
|
1937
|
+
const requestedPath = readWorkspacePathAlias(args) ?? "";
|
|
1938
|
+
if (!requestedPath) throw new Error("Workspace tool requires a non-empty path.");
|
|
1939
|
+
return requestedPath;
|
|
1940
|
+
}
|
|
1941
|
+
function readWorkspacePathAlias(args) {
|
|
1942
|
+
const path = args.path ?? args.file_path ?? args.filePath ?? args.filename ?? args.file;
|
|
1943
|
+
return asOptionalTrimmedString(path);
|
|
1944
|
+
}
|
|
1945
|
+
function requireWorkspaceFileContent(args) {
|
|
1946
|
+
if (typeof args.content !== "string") {
|
|
1947
|
+
throw new Error("writeFile requires string content.");
|
|
1948
|
+
}
|
|
1949
|
+
return args.content;
|
|
1950
|
+
}
|
|
1951
|
+
function requireWorkspaceOldText(args) {
|
|
1952
|
+
if (typeof args.oldText !== "string" || !args.oldText) {
|
|
1953
|
+
throw new Error("editFile requires non-empty oldText.");
|
|
1954
|
+
}
|
|
1955
|
+
return args.oldText;
|
|
1956
|
+
}
|
|
1957
|
+
function requireWorkspaceNewText(args) {
|
|
1958
|
+
if (typeof args.newText !== "string") {
|
|
1959
|
+
throw new Error("editFile requires string newText.");
|
|
1960
|
+
}
|
|
1961
|
+
return args.newText;
|
|
1962
|
+
}
|
|
1963
|
+
function readExpectedReplacementCount(args) {
|
|
1964
|
+
if (args.expectedReplacements === void 0) return 1;
|
|
1965
|
+
const value = Number(args.expectedReplacements);
|
|
1966
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
1967
|
+
throw new Error("editFile expectedReplacements must be a positive integer.");
|
|
1968
|
+
}
|
|
1969
|
+
return value;
|
|
1970
|
+
}
|
|
1971
|
+
function readPositiveIntegerArg(args) {
|
|
1972
|
+
if (args.value === void 0) return void 0;
|
|
1973
|
+
const value = Number(args.value);
|
|
1974
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
1975
|
+
throw new Error(`${args.name} must be a positive integer.`);
|
|
1976
|
+
}
|
|
1977
|
+
return args.max ? Math.min(value, args.max) : value;
|
|
1978
|
+
}
|
|
1979
|
+
function readFileSliceArgs(args) {
|
|
1980
|
+
const startLine = readPositiveIntegerArg({ value: args.startLine, name: "startLine" });
|
|
1981
|
+
const endLine = readPositiveIntegerArg({ value: args.endLine, name: "endLine" });
|
|
1982
|
+
const maxLines = readPositiveIntegerArg({ value: args.maxLines, name: "maxLines", max: 2e3 });
|
|
1983
|
+
const tailLines = readPositiveIntegerArg({ value: args.tailLines, name: "tailLines", max: 2e3 });
|
|
1984
|
+
if (tailLines !== void 0 && (startLine !== void 0 || endLine !== void 0)) {
|
|
1985
|
+
throw new Error("tailLines cannot be combined with startLine or endLine.");
|
|
1986
|
+
}
|
|
1987
|
+
if (endLine !== void 0 && startLine !== void 0 && endLine < startLine) {
|
|
1988
|
+
throw new Error("endLine must be greater than or equal to startLine.");
|
|
1989
|
+
}
|
|
1990
|
+
return {
|
|
1991
|
+
startLine,
|
|
1992
|
+
endLine,
|
|
1993
|
+
maxLines,
|
|
1994
|
+
tailLines
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
function splitTextLines(content) {
|
|
1998
|
+
const lines = content.split(/\r?\n/);
|
|
1999
|
+
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
2000
|
+
return lines;
|
|
2001
|
+
}
|
|
2002
|
+
function sliceReadFileContent(args) {
|
|
2003
|
+
const lines = splitTextLines(args.content);
|
|
2004
|
+
const totalLines = lines.length;
|
|
2005
|
+
if (args.startLine === void 0 && args.endLine === void 0 && args.maxLines === void 0 && args.tailLines === void 0) {
|
|
2006
|
+
return {
|
|
2007
|
+
content: args.content,
|
|
2008
|
+
startLine: 1,
|
|
2009
|
+
endLine: totalLines,
|
|
2010
|
+
totalLines,
|
|
2011
|
+
truncated: false
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
2014
|
+
if (args.tailLines !== void 0) {
|
|
2015
|
+
const startIndex2 = Math.max(0, totalLines - args.tailLines);
|
|
2016
|
+
const selected2 = lines.slice(startIndex2);
|
|
2017
|
+
return {
|
|
2018
|
+
content: selected2.join("\n"),
|
|
2019
|
+
startLine: startIndex2 + 1,
|
|
2020
|
+
endLine: startIndex2 + selected2.length,
|
|
2021
|
+
totalLines,
|
|
2022
|
+
truncated: startIndex2 > 0
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
const startLine = args.startLine ?? 1;
|
|
2026
|
+
let endLine = args.endLine ?? totalLines;
|
|
2027
|
+
if (args.maxLines !== void 0) {
|
|
2028
|
+
endLine = Math.min(endLine, startLine + args.maxLines - 1);
|
|
2029
|
+
}
|
|
2030
|
+
const startIndex = Math.min(Math.max(startLine - 1, 0), totalLines);
|
|
2031
|
+
const endIndex = Math.min(Math.max(endLine, 0), totalLines);
|
|
2032
|
+
const selected = lines.slice(startIndex, endIndex);
|
|
2033
|
+
const effectiveEndLine = selected.length ? startIndex + selected.length : startIndex;
|
|
2034
|
+
return {
|
|
2035
|
+
content: selected.join("\n"),
|
|
2036
|
+
startLine,
|
|
2037
|
+
endLine: effectiveEndLine,
|
|
2038
|
+
totalLines,
|
|
2039
|
+
truncated: startLine > 1 || effectiveEndLine < totalLines
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
function countExactTextOccurrences(args) {
|
|
2043
|
+
return args.content.split(args.oldText).length - 1;
|
|
2044
|
+
}
|
|
2045
|
+
function pluralizeReplacement(count) {
|
|
2046
|
+
return count === 1 ? "replacement" : "replacements";
|
|
2047
|
+
}
|
|
2048
|
+
var EDIT_SNIPPET_MAX_LINES = 5;
|
|
2049
|
+
var EDIT_SNIPPET_WIDTH = 96;
|
|
2050
|
+
function clipEditSnippet(text) {
|
|
2051
|
+
const lines = text.split(/\r?\n/).filter((line) => line.length > 0);
|
|
2052
|
+
return lines.slice(0, EDIT_SNIPPET_MAX_LINES).map((line) => line.length > EDIT_SNIPPET_WIDTH ? `${line.slice(0, EDIT_SNIPPET_WIDTH)}\u2026` : line).join("\n");
|
|
2053
|
+
}
|
|
2054
|
+
function requireWorkspaceSearchQuery(args) {
|
|
2055
|
+
const query = asTrimmedString(args.query);
|
|
2056
|
+
if (!query) throw new Error("searchFiles requires a non-empty query.");
|
|
2057
|
+
return query;
|
|
2058
|
+
}
|
|
2059
|
+
function requireWorkspaceGlobPattern(args) {
|
|
2060
|
+
const pattern = asOptionalTrimmedString(args.pattern) ?? asOptionalTrimmedString(args.glob) ?? "";
|
|
2061
|
+
if (!pattern) throw new Error("globFiles requires a non-empty pattern.");
|
|
2062
|
+
return pattern;
|
|
2063
|
+
}
|
|
2064
|
+
function readWorkspaceMaxResults(args) {
|
|
2065
|
+
if (args.maxResults === void 0) return void 0;
|
|
2066
|
+
const value = Number(args.maxResults);
|
|
2067
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
2068
|
+
throw new Error("maxResults must be a positive integer.");
|
|
2069
|
+
}
|
|
2070
|
+
return Math.min(value, 500);
|
|
2071
|
+
}
|
|
2072
|
+
function readWorkspaceMaxDepth(args) {
|
|
2073
|
+
if (args.maxDepth === void 0) return 1;
|
|
2074
|
+
const value = Number(args.maxDepth);
|
|
2075
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
2076
|
+
throw new Error("maxDepth must be a positive integer.");
|
|
2077
|
+
}
|
|
2078
|
+
return Math.min(value, 10);
|
|
2079
|
+
}
|
|
2080
|
+
function readWorkspaceEntryType(args) {
|
|
2081
|
+
if (args.entryType === void 0) return "all";
|
|
2082
|
+
if (args.entryType === "all" || args.entryType === "files" || args.entryType === "directories") {
|
|
2083
|
+
return args.entryType;
|
|
2084
|
+
}
|
|
2085
|
+
throw new Error("entryType must be one of all, files, or directories.");
|
|
2086
|
+
}
|
|
2087
|
+
function readWorkspaceContextLines(args) {
|
|
2088
|
+
if (args.contextLines === void 0) return void 0;
|
|
2089
|
+
const value = Number(args.contextLines);
|
|
2090
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
2091
|
+
throw new Error("contextLines must be a non-negative integer.");
|
|
2092
|
+
}
|
|
2093
|
+
return Math.min(value, 20);
|
|
2094
|
+
}
|
|
2095
|
+
function readWorkspaceExcludeGlobs(args) {
|
|
2096
|
+
if (args.exclude === void 0) return [];
|
|
2097
|
+
const singleExclude = asOptionalTrimmedString(args.exclude);
|
|
2098
|
+
if (singleExclude) {
|
|
2099
|
+
return [singleExclude];
|
|
2100
|
+
}
|
|
2101
|
+
if (!Array.isArray(args.exclude)) {
|
|
2102
|
+
throw new Error("exclude must be a glob string or an array of glob strings.");
|
|
2103
|
+
}
|
|
2104
|
+
return asTrimmedNonEmptyStringArray(args.exclude);
|
|
2105
|
+
}
|
|
2106
|
+
function requireShellCommand(args, toolName) {
|
|
2107
|
+
const command = asTrimmedString(args.cmd) || asTrimmedString(args.command);
|
|
2108
|
+
if (!command) throw new Error(`${toolName} requires a non-empty command.`);
|
|
2109
|
+
return command;
|
|
2110
|
+
}
|
|
2111
|
+
function requireVisualWaitSelector(args) {
|
|
2112
|
+
const selector = asTrimmedString(args.waitSelector);
|
|
2113
|
+
if (!selector) throw new Error("captureVisualState requires a non-empty waitSelector.");
|
|
2114
|
+
return selector;
|
|
2115
|
+
}
|
|
2116
|
+
function readVisualStateCaptureArgs(args) {
|
|
2117
|
+
const baseUrl = asOptionalTrimmedString(args.baseUrl) ?? asOptionalTrimmedString(args.base);
|
|
2118
|
+
const path = asOptionalTrimmedString(args.path) ?? "/";
|
|
2119
|
+
const waitSelector = requireVisualWaitSelector(args);
|
|
2120
|
+
const scrollSelector = asOptionalTrimmedString(args.scrollSelector);
|
|
2121
|
+
const focusSelector = asOptionalTrimmedString(args.focusSelector);
|
|
2122
|
+
const expectText = asOptionalTrimmedString(args.expectText);
|
|
2123
|
+
const screenshotPath = asOptionalTrimmedString(args.screenshotPath) ?? "test-results/frontend-agent/visual-state.png";
|
|
2124
|
+
const metricsPath = asOptionalTrimmedString(args.metricsPath) ?? "test-results/frontend-agent/visual-state-metrics.json";
|
|
2125
|
+
return {
|
|
2126
|
+
baseUrl,
|
|
2127
|
+
path,
|
|
2128
|
+
waitSelector,
|
|
2129
|
+
scrollSelector,
|
|
2130
|
+
focusSelector,
|
|
2131
|
+
expectText,
|
|
2132
|
+
screenshotPath,
|
|
2133
|
+
metricsPath
|
|
2134
|
+
};
|
|
2135
|
+
}
|
|
2136
|
+
async function readWorkspacePackageScripts(workspaceRoot) {
|
|
2137
|
+
try {
|
|
2138
|
+
const raw = await readFile(resolve(workspaceRoot, "package.json"), "utf8");
|
|
2139
|
+
const parsed = JSON.parse(raw);
|
|
2140
|
+
const scripts = parsed && typeof parsed === "object" && parsed.scripts && typeof parsed.scripts === "object" ? Object.keys(parsed.scripts).sort() : [];
|
|
2141
|
+
return { scripts };
|
|
2142
|
+
} catch (error) {
|
|
2143
|
+
return {
|
|
2144
|
+
scripts: [],
|
|
2145
|
+
error: toErrorMessage(error)
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
function formatAvailableScripts(scripts) {
|
|
2150
|
+
return scripts.length ? scripts.join(", ") : "(none)";
|
|
2151
|
+
}
|
|
2152
|
+
async function runWorkspacePackageScript(args) {
|
|
2153
|
+
const script = args.script.trim();
|
|
2154
|
+
if (!script) throw new Error("package script name is required.");
|
|
2155
|
+
const extraArgs = args.extraArgs ?? [];
|
|
2156
|
+
const packageScripts = await readWorkspacePackageScripts(args.workspaceRoot);
|
|
2157
|
+
if (!packageScripts.scripts.includes(script)) {
|
|
2158
|
+
return {
|
|
2159
|
+
content: [
|
|
2160
|
+
`script not found: ${script}`,
|
|
2161
|
+
`available scripts: ${formatAvailableScripts(packageScripts.scripts)}`,
|
|
2162
|
+
...packageScripts.error ? [`package.json read error: ${packageScripts.error}`] : []
|
|
2163
|
+
].join("\n"),
|
|
2164
|
+
metadata: {
|
|
2165
|
+
script,
|
|
2166
|
+
exitCode: 1,
|
|
2167
|
+
reason: "script-not-found",
|
|
2168
|
+
availableScripts: packageScripts.scripts,
|
|
2169
|
+
...packageScripts.error ? { packageJsonError: packageScripts.error } : {}
|
|
2170
|
+
}
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
const result = await runWorkspaceCommand({
|
|
2174
|
+
workspaceRoot: args.workspaceRoot,
|
|
2175
|
+
command: ["bun", "run", script, ...extraArgs.length ? ["--", ...extraArgs] : []],
|
|
2176
|
+
timeoutMs: args.commandTimeoutMs
|
|
2177
|
+
});
|
|
2178
|
+
return {
|
|
2179
|
+
content: result.content,
|
|
2180
|
+
metadata: {
|
|
2181
|
+
script,
|
|
2182
|
+
args: extraArgs,
|
|
2183
|
+
exitCode: result.exitCode,
|
|
2184
|
+
reason: result.exitCode === 0 ? "ok" : "script-failed",
|
|
2185
|
+
timedOut: result.timedOut,
|
|
2186
|
+
stdoutTail: result.stdout.trim().slice(-4e3),
|
|
2187
|
+
stderrTail: result.stderr.trim().slice(-4e3)
|
|
2188
|
+
}
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
function truncateToolOutput(value, limit = 2e4) {
|
|
2192
|
+
if (value.length <= limit) return value;
|
|
2193
|
+
const approxMarkerLen = 40;
|
|
2194
|
+
if (limit <= approxMarkerLen) return value.slice(0, limit);
|
|
2195
|
+
const remaining = limit - approxMarkerLen;
|
|
2196
|
+
const headSize = Math.floor(remaining * 0.3);
|
|
2197
|
+
const tailSize = remaining - headSize;
|
|
2198
|
+
const head = value.slice(0, headSize);
|
|
2199
|
+
const tail = value.slice(-tailSize);
|
|
2200
|
+
const actualRemoved = value.length - headSize - tailSize;
|
|
2201
|
+
return `${head}
|
|
2202
|
+
|
|
2203
|
+
[... truncated ${actualRemoved} chars ...]
|
|
2204
|
+
|
|
2205
|
+
${tail}`;
|
|
2206
|
+
}
|
|
2207
|
+
function parseLastJsonObject(text) {
|
|
2208
|
+
const trimmed = text.trim();
|
|
2209
|
+
for (let index = trimmed.lastIndexOf("{"); index >= 0; index = trimmed.lastIndexOf("{", index - 1)) {
|
|
2210
|
+
try {
|
|
2211
|
+
const parsed = JSON.parse(trimmed.slice(index));
|
|
2212
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
2213
|
+
} catch {
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
return null;
|
|
2217
|
+
}
|
|
2218
|
+
function readNodeStream(stream) {
|
|
2219
|
+
if (!stream) return Promise.resolve("");
|
|
2220
|
+
return new Promise((resolveRead, rejectRead) => {
|
|
2221
|
+
const chunks = [];
|
|
2222
|
+
stream.on("data", (chunk) => {
|
|
2223
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
2224
|
+
});
|
|
2225
|
+
stream.on("error", rejectRead);
|
|
2226
|
+
stream.on("end", () => resolveRead(Buffer.concat(chunks).toString("utf8")));
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
function waitForNodeProcessExit(proc) {
|
|
2230
|
+
return new Promise((resolveExit, rejectExit) => {
|
|
2231
|
+
proc.once("error", rejectExit);
|
|
2232
|
+
proc.once("close", (code, signal) => {
|
|
2233
|
+
if (typeof code === "number") {
|
|
2234
|
+
resolveExit(code);
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
2237
|
+
resolveExit(signal ? 1 : 0);
|
|
2238
|
+
});
|
|
2239
|
+
});
|
|
2240
|
+
}
|
|
2241
|
+
function resolveRipgrepBinary(env = process.env) {
|
|
2242
|
+
const fromEnv = asOptionalTrimmedString(env.NOLO_BUNDLED_RG);
|
|
2243
|
+
if (fromEnv) {
|
|
2244
|
+
try {
|
|
2245
|
+
if (existsSync(fromEnv)) return fromEnv;
|
|
2246
|
+
} catch {
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
return resolveExecutableOnPath("rg");
|
|
2250
|
+
}
|
|
2251
|
+
async function runWorkspaceCommand(args) {
|
|
2252
|
+
const timeoutMs = asOptionalPositiveFiniteNumber(args.timeoutMs);
|
|
2253
|
+
const detached = process.platform !== "win32";
|
|
2254
|
+
const command = [
|
|
2255
|
+
...args.commandPrefix ?? [],
|
|
2256
|
+
...args.command
|
|
2257
|
+
];
|
|
2258
|
+
const proc = spawnChildProcess(command[0] ?? "", command.slice(1), {
|
|
2259
|
+
cwd: resolve(args.workspaceRoot),
|
|
2260
|
+
stdio: [
|
|
2261
|
+
args.stdin === void 0 ? "ignore" : "pipe",
|
|
2262
|
+
"pipe",
|
|
2263
|
+
"pipe"
|
|
2264
|
+
],
|
|
2265
|
+
detached
|
|
2266
|
+
});
|
|
2267
|
+
if (args.stdin !== void 0 && proc.stdin) {
|
|
2268
|
+
proc.stdin.write(args.stdin);
|
|
2269
|
+
proc.stdin.end();
|
|
2270
|
+
}
|
|
2271
|
+
const exitPromise = waitForNodeProcessExit(proc);
|
|
2272
|
+
const cleanupChildOnHostSignal = (signal) => {
|
|
2273
|
+
if (typeof proc.pid === "number") {
|
|
2274
|
+
try {
|
|
2275
|
+
process.kill(-proc.pid, signal);
|
|
2276
|
+
} catch {
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
};
|
|
2280
|
+
if (detached) {
|
|
2281
|
+
process.once("SIGHUP", cleanupChildOnHostSignal);
|
|
2282
|
+
process.once("SIGTERM", cleanupChildOnHostSignal);
|
|
2283
|
+
process.once("SIGINT", cleanupChildOnHostSignal);
|
|
2284
|
+
}
|
|
2285
|
+
const detachSignalCleanup = () => {
|
|
2286
|
+
if (!detached) return;
|
|
2287
|
+
process.removeListener("SIGHUP", cleanupChildOnHostSignal);
|
|
2288
|
+
process.removeListener("SIGTERM", cleanupChildOnHostSignal);
|
|
2289
|
+
process.removeListener("SIGINT", cleanupChildOnHostSignal);
|
|
2290
|
+
};
|
|
2291
|
+
exitPromise.then(detachSignalCleanup, detachSignalCleanup);
|
|
2292
|
+
const stdoutPromise = readNodeStream(proc.stdout);
|
|
2293
|
+
const stderrPromise = readNodeStream(proc.stderr);
|
|
2294
|
+
const killChild = (signal) => {
|
|
2295
|
+
if (detached && typeof proc.pid === "number") {
|
|
2296
|
+
try {
|
|
2297
|
+
process.kill(-proc.pid, signal);
|
|
2298
|
+
return;
|
|
2299
|
+
} catch {
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
try {
|
|
2303
|
+
proc.kill(signal);
|
|
2304
|
+
} catch {
|
|
2305
|
+
}
|
|
2306
|
+
};
|
|
2307
|
+
let timeout;
|
|
2308
|
+
let detachTimer;
|
|
2309
|
+
const timeoutResult = Symbol("timeout");
|
|
2310
|
+
const abortResult = Symbol("abort");
|
|
2311
|
+
const detachResult = Symbol("detach");
|
|
2312
|
+
let abortListener;
|
|
2313
|
+
const abortPromise = args.abortSignal ? new Promise((resolveAbort) => {
|
|
2314
|
+
if (args.abortSignal.aborted) {
|
|
2315
|
+
resolveAbort(abortResult);
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
abortListener = () => resolveAbort(abortResult);
|
|
2319
|
+
args.abortSignal.addEventListener("abort", abortListener, { once: true });
|
|
2320
|
+
}) : null;
|
|
2321
|
+
const effectiveDetachMs = resolveDetachMs(args.detachMs);
|
|
2322
|
+
const detachPromise = effectiveDetachMs ? new Promise((r) => {
|
|
2323
|
+
detachTimer = setTimeout(() => r(detachResult), effectiveDetachMs);
|
|
2324
|
+
}) : null;
|
|
2325
|
+
const racers = [exitPromise];
|
|
2326
|
+
if (timeoutMs) {
|
|
2327
|
+
racers.push(
|
|
2328
|
+
new Promise((resolveTimeout) => {
|
|
2329
|
+
timeout = setTimeout(() => resolveTimeout(timeoutResult), timeoutMs);
|
|
2330
|
+
})
|
|
2331
|
+
);
|
|
2332
|
+
}
|
|
2333
|
+
if (abortPromise) racers.push(abortPromise);
|
|
2334
|
+
if (detachPromise) racers.push(detachPromise);
|
|
2335
|
+
const raceWinner = await Promise.race(racers);
|
|
2336
|
+
if (timeout) clearTimeout(timeout);
|
|
2337
|
+
if (detachTimer) clearTimeout(detachTimer);
|
|
2338
|
+
if (abortListener && args.abortSignal) {
|
|
2339
|
+
args.abortSignal.removeEventListener("abort", abortListener);
|
|
2340
|
+
}
|
|
2341
|
+
const timedOut = raceWinner === timeoutResult;
|
|
2342
|
+
const aborted = raceWinner === abortResult;
|
|
2343
|
+
const didDetach = raceWinner === detachResult;
|
|
2344
|
+
if (timedOut || aborted) {
|
|
2345
|
+
killChild("SIGTERM");
|
|
2346
|
+
await Promise.race([
|
|
2347
|
+
exitPromise.catch(() => 124),
|
|
2348
|
+
new Promise((resolveKill) => setTimeout(resolveKill, 500))
|
|
2349
|
+
]);
|
|
2350
|
+
killChild("SIGKILL");
|
|
2351
|
+
}
|
|
2352
|
+
if (didDetach) {
|
|
2353
|
+
detachSignalCleanup();
|
|
2354
|
+
const pid = typeof proc.pid === "number" ? proc.pid : 0;
|
|
2355
|
+
const label = deriveLabel(args.command.join(" "));
|
|
2356
|
+
const pgid = pid;
|
|
2357
|
+
const registry2 = getProcessRegistry();
|
|
2358
|
+
registry2.add({ pid, pgid, command: args.command.join(" "), label });
|
|
2359
|
+
proc.on("close", (code) => {
|
|
2360
|
+
registry2.markExited(pid, code ?? 1);
|
|
2361
|
+
});
|
|
2362
|
+
return {
|
|
2363
|
+
stdout: "",
|
|
2364
|
+
stderr: `command detached to background after ${effectiveDetachMs}ms (pid=${pid}, label=${label})
|
|
2365
|
+
Use listProcesses / processRegistry to inspect.`,
|
|
2366
|
+
exitCode: 0,
|
|
2367
|
+
timedOut: false,
|
|
2368
|
+
detached: true,
|
|
2369
|
+
pid,
|
|
2370
|
+
label,
|
|
2371
|
+
content: JSON.stringify({ detached: true, pid, label, status: "running" })
|
|
2372
|
+
};
|
|
2373
|
+
}
|
|
2374
|
+
const [stdout, rawStderr] = await Promise.all([
|
|
2375
|
+
stdoutPromise,
|
|
2376
|
+
stderrPromise
|
|
2377
|
+
]);
|
|
2378
|
+
const exitCode = aborted ? 130 : timedOut ? 124 : Number(raceWinner);
|
|
2379
|
+
const stderr = aborted ? `${rawStderr.trim() ? `${rawStderr.trim()}
|
|
2380
|
+
` : ""}command aborted by signal
|
|
2381
|
+
` : timedOut ? `${rawStderr.trim() ? `${rawStderr.trim()}
|
|
2382
|
+
` : ""}command timed out after ${timeoutMs ?? "unknown"}ms
|
|
2383
|
+
` : rawStderr;
|
|
2384
|
+
return {
|
|
2385
|
+
stdout,
|
|
2386
|
+
stderr,
|
|
2387
|
+
exitCode,
|
|
2388
|
+
timedOut,
|
|
2389
|
+
aborted,
|
|
2390
|
+
content: truncateToolOutput([
|
|
2391
|
+
stdout.trim() ? `stdout:
|
|
2392
|
+
${stdout.trim()}` : "",
|
|
2393
|
+
stderr.trim() ? `stderr:
|
|
2394
|
+
${stderr.trim()}` : "",
|
|
2395
|
+
`exitCode: ${exitCode}`
|
|
2396
|
+
].filter(Boolean).join("\n\n"), args.outputLimit)
|
|
2397
|
+
};
|
|
2398
|
+
}
|
|
2399
|
+
async function runWorkspaceCommandLimitedLines(args) {
|
|
2400
|
+
const command = [
|
|
2401
|
+
...args.commandPrefix ?? [],
|
|
2402
|
+
...args.command
|
|
2403
|
+
];
|
|
2404
|
+
const proc = spawnChildProcess(command[0] ?? "", command.slice(1), {
|
|
2405
|
+
cwd: resolve(args.workspaceRoot),
|
|
2406
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2407
|
+
detached: false
|
|
2408
|
+
});
|
|
2409
|
+
const exitSettled = waitForNodeProcessExit(proc).then(
|
|
2410
|
+
(code) => ({ ok: true, code }),
|
|
2411
|
+
(error) => ({ ok: false, error })
|
|
2412
|
+
);
|
|
2413
|
+
const stderrPromise = readNodeStream(proc.stderr);
|
|
2414
|
+
const lines = [];
|
|
2415
|
+
let pending = "";
|
|
2416
|
+
let limitedByMaxResults = false;
|
|
2417
|
+
try {
|
|
2418
|
+
await Promise.race([
|
|
2419
|
+
new Promise((resolveRead, rejectRead) => {
|
|
2420
|
+
proc.stdout?.on("data", (chunk) => {
|
|
2421
|
+
pending += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
|
|
2422
|
+
const parts = pending.split(/\r?\n/);
|
|
2423
|
+
pending = parts.pop() ?? "";
|
|
2424
|
+
for (const part of parts) {
|
|
2425
|
+
if (!part) continue;
|
|
2426
|
+
lines.push(part);
|
|
2427
|
+
if (lines.length >= args.maxLines) {
|
|
2428
|
+
limitedByMaxResults = true;
|
|
2429
|
+
try {
|
|
2430
|
+
proc.kill("SIGTERM");
|
|
2431
|
+
} catch {
|
|
2432
|
+
}
|
|
2433
|
+
break;
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
});
|
|
2437
|
+
proc.stdout?.on("error", rejectRead);
|
|
2438
|
+
proc.stdout?.on("end", () => resolveRead());
|
|
2439
|
+
if (!proc.stdout) {
|
|
2440
|
+
void exitSettled.then((result) => {
|
|
2441
|
+
if (!result.ok) rejectRead(result.error);
|
|
2442
|
+
else resolveRead();
|
|
2443
|
+
});
|
|
2444
|
+
}
|
|
2445
|
+
}),
|
|
2446
|
+
exitSettled.then((result) => {
|
|
2447
|
+
if (!result.ok) throw result.error;
|
|
2448
|
+
})
|
|
2449
|
+
]);
|
|
2450
|
+
} catch (error) {
|
|
2451
|
+
const settled2 = await exitSettled;
|
|
2452
|
+
if (!settled2.ok) throw settled2.error;
|
|
2453
|
+
throw error;
|
|
2454
|
+
}
|
|
2455
|
+
if (!limitedByMaxResults && pending.trim()) lines.push(pending);
|
|
2456
|
+
const settled = await exitSettled;
|
|
2457
|
+
if (!settled.ok) throw settled.error;
|
|
2458
|
+
const exitCode = limitedByMaxResults ? 0 : settled.code;
|
|
2459
|
+
const stderr = await stderrPromise;
|
|
2460
|
+
return {
|
|
2461
|
+
stdout: lines.slice(0, args.maxLines).join("\n"),
|
|
2462
|
+
stderr,
|
|
2463
|
+
exitCode,
|
|
2464
|
+
limitedByMaxResults
|
|
2465
|
+
};
|
|
2466
|
+
}
|
|
2467
|
+
async function resolveLocalWorkspaceToolPath(args) {
|
|
2468
|
+
const workspaceRoot = resolve(args.workspaceRoot);
|
|
2469
|
+
const targetPath = resolve(workspaceRoot, args.requestedPath);
|
|
2470
|
+
if (!isPathInsideWorkspace({ workspaceRoot, targetPath })) {
|
|
2471
|
+
if (args.confirmExternalFileAccess) {
|
|
2472
|
+
const request = {
|
|
2473
|
+
id: "permission-file-external-access",
|
|
2474
|
+
tool: "fileTool",
|
|
2475
|
+
action: "external_file_access",
|
|
2476
|
+
title: "\u786E\u8BA4\u8BFB\u53D6\u5DE5\u4F5C\u533A\u5916\u90E8\u6587\u4EF6",
|
|
2477
|
+
body: "\u8BE5\u8DEF\u5F84\u4F4D\u4E8E\u5F53\u524D\u5DE5\u4F5C\u533A\u4E4B\u5916\u3002\u786E\u8BA4\u540E\u672C\u6B21\u8BBF\u95EE\u653E\u884C\uFF0C\u5426\u5219\u62D2\u7EDD\u3002",
|
|
2478
|
+
command: args.requestedPath,
|
|
2479
|
+
suggestedRule: {
|
|
2480
|
+
scope: "once",
|
|
2481
|
+
pattern: { capability: "external_file_access", target: args.requestedPath }
|
|
2482
|
+
}
|
|
2483
|
+
};
|
|
2484
|
+
const confirmed = await args.confirmExternalFileAccess(request);
|
|
2485
|
+
if (!confirmed) {
|
|
2486
|
+
const error = new Error(
|
|
2487
|
+
`external file access blocked: user declined confirmation for ${args.requestedPath}`
|
|
2488
|
+
);
|
|
2489
|
+
error.code = "external_file_access_requires_confirmation";
|
|
2490
|
+
error.permissionRequest = request;
|
|
2491
|
+
throw error;
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
return targetPath;
|
|
2496
|
+
}
|
|
2497
|
+
async function readFileTool(args) {
|
|
2498
|
+
const parsed = parseWorkspaceToolArguments(args.call.arguments);
|
|
2499
|
+
const requestedPath = requireWorkspaceToolPath(parsed);
|
|
2500
|
+
const sliceArgs = readFileSliceArgs(parsed);
|
|
2501
|
+
const absolutePath = await resolveLocalWorkspaceToolPath({
|
|
2502
|
+
workspaceRoot: args.workspaceRoot,
|
|
2503
|
+
requestedPath,
|
|
2504
|
+
...args.confirmExternalFileAccess ? { confirmExternalFileAccess: args.confirmExternalFileAccess } : {}
|
|
2505
|
+
});
|
|
2506
|
+
const content = await readFile(absolutePath, "utf8");
|
|
2507
|
+
const activity = extractActivity(parsed);
|
|
2508
|
+
const sliced = sliceReadFileContent({
|
|
2509
|
+
content,
|
|
2510
|
+
...sliceArgs
|
|
2511
|
+
});
|
|
2512
|
+
return {
|
|
2513
|
+
content: sliced.content,
|
|
2514
|
+
metadata: {
|
|
2515
|
+
path: normalizeWorkspaceRelativePath({
|
|
2516
|
+
workspaceRoot: resolve(args.workspaceRoot),
|
|
2517
|
+
targetPath: absolutePath
|
|
2518
|
+
}),
|
|
2519
|
+
bytes: Buffer.byteLength(sliced.content),
|
|
2520
|
+
totalBytes: Buffer.byteLength(content),
|
|
2521
|
+
startLine: sliced.startLine,
|
|
2522
|
+
endLine: sliced.endLine,
|
|
2523
|
+
totalLines: sliced.totalLines,
|
|
2524
|
+
truncated: sliced.truncated,
|
|
2525
|
+
...sliceArgs.maxLines ? { maxLines: sliceArgs.maxLines } : {},
|
|
2526
|
+
...sliceArgs.tailLines ? { tailLines: sliceArgs.tailLines } : {},
|
|
2527
|
+
...activity ? { activity } : {}
|
|
2528
|
+
}
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
async function writeFileTool(args) {
|
|
2532
|
+
const parsed = parseWorkspaceToolArguments(args.call.arguments);
|
|
2533
|
+
const requestedPath = requireWorkspaceToolPath(parsed);
|
|
2534
|
+
const content = requireWorkspaceFileContent(parsed);
|
|
2535
|
+
const absolutePath = await resolveLocalWorkspaceToolPath({
|
|
2536
|
+
workspaceRoot: args.workspaceRoot,
|
|
2537
|
+
requestedPath,
|
|
2538
|
+
...args.confirmExternalFileAccess ? { confirmExternalFileAccess: args.confirmExternalFileAccess } : {}
|
|
2539
|
+
});
|
|
2540
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
2541
|
+
await writeFile(absolutePath, content, "utf8");
|
|
2542
|
+
const relativePath = normalizeWorkspaceRelativePath({
|
|
2543
|
+
workspaceRoot: resolve(args.workspaceRoot),
|
|
2544
|
+
targetPath: absolutePath
|
|
2545
|
+
});
|
|
2546
|
+
const activity = extractActivity(parsed);
|
|
2547
|
+
let diffStat;
|
|
2548
|
+
try {
|
|
2549
|
+
const stats = [];
|
|
2550
|
+
for (const ref of ["", "--cached"]) {
|
|
2551
|
+
const result = spawnSync("git", ["diff", ref, "--stat", "--", relativePath], {
|
|
2552
|
+
cwd: args.workspaceRoot,
|
|
2553
|
+
timeout: 3e3,
|
|
2554
|
+
encoding: "utf-8",
|
|
2555
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2556
|
+
});
|
|
2557
|
+
const out = (result.stdout ?? "").trim();
|
|
2558
|
+
if (out) stats.push(out);
|
|
2559
|
+
}
|
|
2560
|
+
if (stats.length > 0) diffStat = stats.join("; ");
|
|
2561
|
+
} catch (err) {
|
|
2562
|
+
diffStat = `[git diff unavailable: ${toErrorMessage(err)}]`;
|
|
2563
|
+
}
|
|
2564
|
+
return {
|
|
2565
|
+
content: `wrote ${relativePath}`,
|
|
2566
|
+
metadata: {
|
|
2567
|
+
path: relativePath,
|
|
2568
|
+
bytes: Buffer.byteLength(content),
|
|
2569
|
+
...diffStat ? { diffStat } : {},
|
|
2570
|
+
...activity ? { activity } : {}
|
|
2571
|
+
}
|
|
2572
|
+
};
|
|
2573
|
+
}
|
|
2574
|
+
async function editFileTool(args) {
|
|
2575
|
+
const parsed = parseWorkspaceToolArguments(args.call.arguments);
|
|
2576
|
+
const requestedPath = requireWorkspaceToolPath(parsed);
|
|
2577
|
+
const oldText = requireWorkspaceOldText(parsed);
|
|
2578
|
+
const newText = requireWorkspaceNewText(parsed);
|
|
2579
|
+
const expectedReplacements = readExpectedReplacementCount(parsed);
|
|
2580
|
+
const absolutePath = await resolveLocalWorkspaceToolPath({
|
|
2581
|
+
workspaceRoot: args.workspaceRoot,
|
|
2582
|
+
requestedPath,
|
|
2583
|
+
...args.confirmExternalFileAccess ? { confirmExternalFileAccess: args.confirmExternalFileAccess } : {}
|
|
2584
|
+
});
|
|
2585
|
+
const content = await readFile(absolutePath, "utf8");
|
|
2586
|
+
const replacementCount = countExactTextOccurrences({ content, oldText });
|
|
2587
|
+
if (replacementCount !== expectedReplacements) {
|
|
2588
|
+
throw new Error(
|
|
2589
|
+
`editFile expected ${expectedReplacements} ${pluralizeReplacement(expectedReplacements)} but found ${replacementCount} in ${requestedPath}.`
|
|
2590
|
+
);
|
|
2591
|
+
}
|
|
2592
|
+
const nextContent = content.split(oldText).join(newText);
|
|
2593
|
+
await writeFile(absolutePath, nextContent, "utf8");
|
|
2594
|
+
const relativePath = normalizeWorkspaceRelativePath({
|
|
2595
|
+
workspaceRoot: resolve(args.workspaceRoot),
|
|
2596
|
+
targetPath: absolutePath
|
|
2597
|
+
});
|
|
2598
|
+
const activity = extractActivity(parsed);
|
|
2599
|
+
return {
|
|
2600
|
+
content: `replaced ${replacementCount} occurrence${replacementCount === 1 ? "" : "s"} in ${relativePath}`,
|
|
2601
|
+
metadata: {
|
|
2602
|
+
path: relativePath,
|
|
2603
|
+
replacements: replacementCount,
|
|
2604
|
+
bytes: Buffer.byteLength(nextContent),
|
|
2605
|
+
oldSnippet: clipEditSnippet(oldText),
|
|
2606
|
+
newSnippet: clipEditSnippet(newText),
|
|
2607
|
+
...activity ? { activity } : {}
|
|
2608
|
+
}
|
|
2609
|
+
};
|
|
2610
|
+
}
|
|
2611
|
+
async function listWorkspaceEntries(args) {
|
|
2612
|
+
const entries = [];
|
|
2613
|
+
let truncated = false;
|
|
2614
|
+
let visitedEntries = 0;
|
|
2615
|
+
let limitedByMaxDepth = false;
|
|
2616
|
+
const visit = async (dirPath, depth) => {
|
|
2617
|
+
if (truncated) return;
|
|
2618
|
+
const names = (await readdir(dirPath)).sort((left, right) => left.localeCompare(right));
|
|
2619
|
+
for (const name of names) {
|
|
2620
|
+
if (truncated) break;
|
|
2621
|
+
const absolutePath = resolve(dirPath, name);
|
|
2622
|
+
const info = await stat(absolutePath);
|
|
2623
|
+
visitedEntries += 1;
|
|
2624
|
+
const relativePath = normalizeWorkspaceRelativePath({
|
|
2625
|
+
workspaceRoot: resolve(args.workspaceRoot),
|
|
2626
|
+
targetPath: absolutePath
|
|
2627
|
+
});
|
|
2628
|
+
const isDirectory = info.isDirectory();
|
|
2629
|
+
if (args.entryType === "all" || args.entryType === "files" && !isDirectory || args.entryType === "directories" && isDirectory) {
|
|
2630
|
+
entries.push(isDirectory ? `${relativePath}/` : relativePath);
|
|
2631
|
+
if (args.maxResults && entries.length >= args.maxResults) {
|
|
2632
|
+
truncated = true;
|
|
2633
|
+
break;
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
if (isDirectory && depth < args.maxDepth) {
|
|
2637
|
+
await visit(absolutePath, depth + 1);
|
|
2638
|
+
} else if (isDirectory && depth >= args.maxDepth && !limitedByMaxDepth) {
|
|
2639
|
+
const childNames = await readdir(absolutePath);
|
|
2640
|
+
limitedByMaxDepth = childNames.length > 0;
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
};
|
|
2644
|
+
await visit(args.dirPath, 1);
|
|
2645
|
+
return {
|
|
2646
|
+
entries,
|
|
2647
|
+
truncated,
|
|
2648
|
+
visitedEntries,
|
|
2649
|
+
limitedByMaxDepth
|
|
2650
|
+
};
|
|
2651
|
+
}
|
|
2652
|
+
async function listFilesTool(args) {
|
|
2653
|
+
const parsed = parseWorkspaceToolArguments(args.call.arguments);
|
|
2654
|
+
const requestedPath = readWorkspacePathAlias(parsed) ?? ".";
|
|
2655
|
+
const maxDepth = readWorkspaceMaxDepth(parsed);
|
|
2656
|
+
const maxResults = readWorkspaceMaxResults(parsed);
|
|
2657
|
+
const entryType = readWorkspaceEntryType(parsed);
|
|
2658
|
+
const dirPath = await resolveLocalWorkspaceToolPath({
|
|
2659
|
+
workspaceRoot: args.workspaceRoot,
|
|
2660
|
+
requestedPath,
|
|
2661
|
+
...args.confirmExternalFileAccess ? { confirmExternalFileAccess: args.confirmExternalFileAccess } : {}
|
|
2662
|
+
});
|
|
2663
|
+
const listed = await listWorkspaceEntries({
|
|
2664
|
+
workspaceRoot: args.workspaceRoot,
|
|
2665
|
+
dirPath,
|
|
2666
|
+
maxDepth,
|
|
2667
|
+
maxResults,
|
|
2668
|
+
entryType
|
|
2669
|
+
});
|
|
2670
|
+
return {
|
|
2671
|
+
content: listed.entries.join("\n"),
|
|
2672
|
+
metadata: {
|
|
2673
|
+
path: requestedPath,
|
|
2674
|
+
count: listed.entries.length,
|
|
2675
|
+
maxDepth,
|
|
2676
|
+
entryType,
|
|
2677
|
+
truncated: listed.truncated,
|
|
2678
|
+
limitedByMaxResults: listed.truncated,
|
|
2679
|
+
limitedByMaxDepth: listed.limitedByMaxDepth,
|
|
2680
|
+
visitedEntries: listed.visitedEntries,
|
|
2681
|
+
...maxResults ? { maxResults } : {}
|
|
2682
|
+
}
|
|
2683
|
+
};
|
|
2684
|
+
}
|
|
2685
|
+
async function searchFilesTool(args) {
|
|
2686
|
+
const parsed = parseWorkspaceToolArguments(args.call.arguments);
|
|
2687
|
+
const query = requireWorkspaceSearchQuery(parsed);
|
|
2688
|
+
const exclude = readWorkspaceExcludeGlobs(parsed);
|
|
2689
|
+
const maxResults = readWorkspaceMaxResults(parsed);
|
|
2690
|
+
const contextLines = readWorkspaceContextLines(parsed);
|
|
2691
|
+
const literal = parsed.literal === true;
|
|
2692
|
+
const caseSensitive = parsed.caseSensitive === false ? false : true;
|
|
2693
|
+
const requestedPath = readWorkspacePathAlias(parsed) ?? ".";
|
|
2694
|
+
const includeIgnored = parsed.includeIgnored === true;
|
|
2695
|
+
const searchPath = await resolveLocalWorkspaceToolPath({
|
|
2696
|
+
workspaceRoot: args.workspaceRoot,
|
|
2697
|
+
requestedPath,
|
|
2698
|
+
...args.confirmExternalFileAccess ? { confirmExternalFileAccess: args.confirmExternalFileAccess } : {}
|
|
2699
|
+
});
|
|
2700
|
+
const relativeSearchPath = normalizeWorkspaceRelativePath({
|
|
2701
|
+
workspaceRoot: resolve(args.workspaceRoot),
|
|
2702
|
+
targetPath: searchPath
|
|
2703
|
+
});
|
|
2704
|
+
const rgBinary = resolveRipgrepBinary();
|
|
2705
|
+
const grepBinary = resolveExecutableOnPath("grep") || "grep";
|
|
2706
|
+
const rgCommand = rgBinary ? [
|
|
2707
|
+
rgBinary,
|
|
2708
|
+
"--line-number",
|
|
2709
|
+
"--no-heading",
|
|
2710
|
+
"--hidden",
|
|
2711
|
+
...literal ? ["--fixed-strings"] : [],
|
|
2712
|
+
...caseSensitive ? [] : ["--ignore-case"],
|
|
2713
|
+
...contextLines !== void 0 ? ["--context", String(contextLines)] : [],
|
|
2714
|
+
...includeIgnored ? ["--no-ignore"] : [],
|
|
2715
|
+
"--glob",
|
|
2716
|
+
"!node_modules",
|
|
2717
|
+
"--glob",
|
|
2718
|
+
"!.git",
|
|
2719
|
+
...exclude.flatMap((excludePattern) => ["--glob", `!${excludePattern}`]),
|
|
2720
|
+
"--",
|
|
2721
|
+
query,
|
|
2722
|
+
relativeSearchPath
|
|
2723
|
+
] : null;
|
|
2724
|
+
const grepCommand = [
|
|
2725
|
+
grepBinary,
|
|
2726
|
+
"-R",
|
|
2727
|
+
"-n",
|
|
2728
|
+
"-I",
|
|
2729
|
+
...literal ? ["-F"] : [],
|
|
2730
|
+
...caseSensitive ? [] : ["-i"],
|
|
2731
|
+
...contextLines !== void 0 ? ["-C", String(contextLines)] : [],
|
|
2732
|
+
"--exclude-dir=node_modules",
|
|
2733
|
+
"--exclude-dir=.git",
|
|
2734
|
+
...exclude.flatMap((excludePattern) => ["--exclude", excludePattern]),
|
|
2735
|
+
query,
|
|
2736
|
+
relativeSearchPath
|
|
2737
|
+
];
|
|
2738
|
+
const result = await (async () => {
|
|
2739
|
+
if (rgCommand) {
|
|
2740
|
+
try {
|
|
2741
|
+
return maxResults && !contextLines ? await runWorkspaceCommandLimitedLines({
|
|
2742
|
+
workspaceRoot: args.workspaceRoot,
|
|
2743
|
+
command: rgCommand,
|
|
2744
|
+
maxLines: maxResults
|
|
2745
|
+
}) : await runWorkspaceCommand({
|
|
2746
|
+
workspaceRoot: args.workspaceRoot,
|
|
2747
|
+
command: rgCommand
|
|
2748
|
+
});
|
|
2749
|
+
} catch {
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
try {
|
|
2753
|
+
return maxResults && !contextLines ? await runWorkspaceCommandLimitedLines({
|
|
2754
|
+
workspaceRoot: args.workspaceRoot,
|
|
2755
|
+
command: grepCommand,
|
|
2756
|
+
maxLines: maxResults
|
|
2757
|
+
}) : await runWorkspaceCommand({
|
|
2758
|
+
workspaceRoot: args.workspaceRoot,
|
|
2759
|
+
command: grepCommand
|
|
2760
|
+
});
|
|
2761
|
+
} catch {
|
|
2762
|
+
return {
|
|
2763
|
+
stdout: "",
|
|
2764
|
+
stderr: "",
|
|
2765
|
+
exitCode: 0,
|
|
2766
|
+
limitedByMaxResults: false
|
|
2767
|
+
};
|
|
2768
|
+
}
|
|
2769
|
+
})();
|
|
2770
|
+
let outputLines = result.stdout.split(/\r?\n/).map((line) => line.trimEnd().replace(/^\.\//, "")).filter(Boolean);
|
|
2771
|
+
let limitedByMaxResults = "limitedByMaxResults" in result && result.limitedByMaxResults === true;
|
|
2772
|
+
if (outputLines.length === 0 && result.exitCode === 0 && (includeIgnored || !await hasRootGitignore(args.workspaceRoot))) {
|
|
2773
|
+
const fallback = await scanWorkspaceTextMatches({
|
|
2774
|
+
workspaceRoot: args.workspaceRoot,
|
|
2775
|
+
relativeSearchPath,
|
|
2776
|
+
query,
|
|
2777
|
+
exclude,
|
|
2778
|
+
maxResults,
|
|
2779
|
+
literal,
|
|
2780
|
+
caseSensitive,
|
|
2781
|
+
contextLines
|
|
2782
|
+
});
|
|
2783
|
+
outputLines = fallback.lines;
|
|
2784
|
+
limitedByMaxResults = fallback.limitedByMaxResults;
|
|
2785
|
+
}
|
|
2786
|
+
if (contextLines && outputLines.length > 0) {
|
|
2787
|
+
const limited = limitSearchOutputByMatches(outputLines, maxResults);
|
|
2788
|
+
outputLines = limited.lines;
|
|
2789
|
+
limitedByMaxResults = limitedByMaxResults || limited.truncated;
|
|
2790
|
+
}
|
|
2791
|
+
const matchLines = outputLines.filter((line) => /:\d+:/.test(line));
|
|
2792
|
+
const matchedFiles = Array.from(new Set(matchLines.flatMap((line) => {
|
|
2793
|
+
const match = line.match(/^(.*?):\d+:/);
|
|
2794
|
+
return match?.[1] ? [match[1]] : [];
|
|
2795
|
+
})));
|
|
2796
|
+
return {
|
|
2797
|
+
content: outputLines.join("\n"),
|
|
2798
|
+
metadata: {
|
|
2799
|
+
query,
|
|
2800
|
+
path: requestedPath,
|
|
2801
|
+
searchedPath: relativeSearchPath,
|
|
2802
|
+
exclude,
|
|
2803
|
+
includeIgnored,
|
|
2804
|
+
literal,
|
|
2805
|
+
caseSensitive,
|
|
2806
|
+
...contextLines === void 0 ? {} : { contextLines },
|
|
2807
|
+
count: outputLines.length,
|
|
2808
|
+
matchCount: matchLines.length,
|
|
2809
|
+
matchedFiles,
|
|
2810
|
+
truncated: limitedByMaxResults,
|
|
2811
|
+
limitedByMaxResults,
|
|
2812
|
+
...maxResults ? { maxResults } : {},
|
|
2813
|
+
exitCode: result.exitCode,
|
|
2814
|
+
...extractActivity(parsed) ? { activity: extractActivity(parsed) } : {}
|
|
2815
|
+
}
|
|
2816
|
+
};
|
|
2817
|
+
}
|
|
2818
|
+
async function hasRootGitignore(workspaceRoot) {
|
|
2819
|
+
try {
|
|
2820
|
+
await stat(resolve(workspaceRoot, ".gitignore"));
|
|
2821
|
+
return true;
|
|
2822
|
+
} catch {
|
|
2823
|
+
return false;
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
async function readRootGitignorePatterns(workspaceRoot) {
|
|
2827
|
+
try {
|
|
2828
|
+
const content = await readFile(resolve(workspaceRoot, ".gitignore"), "utf8");
|
|
2829
|
+
return content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#") && !line.startsWith("!")).map((line) => {
|
|
2830
|
+
const unanchored = line.replace(/^\//, "");
|
|
2831
|
+
return unanchored.endsWith("/") ? `${unanchored}**` : unanchored;
|
|
2832
|
+
});
|
|
2833
|
+
} catch {
|
|
2834
|
+
return [];
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
async function filterRootGitignoredFiles(args) {
|
|
2838
|
+
if (args.includeIgnored) return args.files;
|
|
2839
|
+
const patterns = await readRootGitignorePatterns(args.workspaceRoot);
|
|
2840
|
+
if (patterns.length === 0) return args.files;
|
|
2841
|
+
const globs = patterns.map((pattern) => createGlob(pattern));
|
|
2842
|
+
return args.files.filter((file) => !globs.some((glob) => glob.match(file)));
|
|
2843
|
+
}
|
|
2844
|
+
function scanWorkspaceGlobFiles(args) {
|
|
2845
|
+
const pathPrefix = args.relativeSearchPath === "." ? "" : `${args.relativeSearchPath.replace(/\/+$/, "")}/`;
|
|
2846
|
+
return Array.from(createGlob(args.pattern).scanSync({
|
|
2847
|
+
cwd: args.workspaceRoot,
|
|
2848
|
+
dot: true,
|
|
2849
|
+
onlyFiles: true
|
|
2850
|
+
})).map((line) => line.trim().replace(/^\.\//, "")).filter(Boolean).filter((line) => !line.startsWith(".git/") && !line.startsWith("node_modules/")).filter((line) => !pathPrefix || line === args.relativeSearchPath || line.startsWith(pathPrefix)).filter((line) => !args.exclude.some((pattern) => createGlob(pattern).match(line))).sort((left, right) => left.localeCompare(right));
|
|
2851
|
+
}
|
|
2852
|
+
function limitSearchOutputByMatches(lines, maxResults) {
|
|
2853
|
+
if (!maxResults) return { lines, truncated: false };
|
|
2854
|
+
const limited = [];
|
|
2855
|
+
let matches = 0;
|
|
2856
|
+
for (const line of lines) {
|
|
2857
|
+
if (/:\d+:/.test(line)) {
|
|
2858
|
+
if (matches >= maxResults) {
|
|
2859
|
+
return { lines: limited, truncated: true };
|
|
2860
|
+
}
|
|
2861
|
+
matches += 1;
|
|
2862
|
+
}
|
|
2863
|
+
limited.push(line);
|
|
2864
|
+
}
|
|
2865
|
+
return { lines: limited, truncated: false };
|
|
2866
|
+
}
|
|
2867
|
+
async function scanWorkspaceTextMatches(args) {
|
|
2868
|
+
const files = scanWorkspaceGlobFiles({
|
|
2869
|
+
workspaceRoot: args.workspaceRoot,
|
|
2870
|
+
pattern: "**/*",
|
|
2871
|
+
relativeSearchPath: args.relativeSearchPath,
|
|
2872
|
+
exclude: args.exclude
|
|
2873
|
+
});
|
|
2874
|
+
const results = [];
|
|
2875
|
+
const seenContext = /* @__PURE__ */ new Set();
|
|
2876
|
+
let matchCount = 0;
|
|
2877
|
+
const literalQuery = args.caseSensitive ? args.query : args.query.toLowerCase();
|
|
2878
|
+
const regex = args.literal ? void 0 : new RegExp(args.query, args.caseSensitive ? "" : "i");
|
|
2879
|
+
const pushLine = (file, lineNumber, text, isMatch) => {
|
|
2880
|
+
const separator = isMatch ? ":" : "-";
|
|
2881
|
+
const key = `${file}:${lineNumber}:${separator}`;
|
|
2882
|
+
if (seenContext.has(key)) return;
|
|
2883
|
+
seenContext.add(key);
|
|
2884
|
+
results.push(`${file}${separator}${lineNumber}${separator}${text}`);
|
|
2885
|
+
};
|
|
2886
|
+
for (const file of files) {
|
|
2887
|
+
let content = "";
|
|
2888
|
+
try {
|
|
2889
|
+
content = await readFile(resolve(args.workspaceRoot, file), "utf8");
|
|
2890
|
+
} catch {
|
|
2891
|
+
continue;
|
|
2892
|
+
}
|
|
2893
|
+
if (content.includes("\0")) continue;
|
|
2894
|
+
const lines = content.split(/\r?\n/);
|
|
2895
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
2896
|
+
const line = lines[index] ?? "";
|
|
2897
|
+
const haystack = args.caseSensitive ? line : line.toLowerCase();
|
|
2898
|
+
const matched = args.literal ? haystack.includes(literalQuery) : regex?.test(line) === true;
|
|
2899
|
+
if (!matched) continue;
|
|
2900
|
+
matchCount += 1;
|
|
2901
|
+
const contextLines = args.contextLines ?? 0;
|
|
2902
|
+
const start = Math.max(0, index - contextLines);
|
|
2903
|
+
const end = Math.min(lines.length - 1, index + contextLines);
|
|
2904
|
+
for (let contextIndex = start; contextIndex <= end; contextIndex += 1) {
|
|
2905
|
+
pushLine(file, contextIndex + 1, lines[contextIndex] ?? "", contextIndex === index);
|
|
2906
|
+
}
|
|
2907
|
+
if (args.maxResults && matchCount >= args.maxResults) {
|
|
2908
|
+
return {
|
|
2909
|
+
lines: results,
|
|
2910
|
+
limitedByMaxResults: true
|
|
2911
|
+
};
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
return {
|
|
2916
|
+
lines: results,
|
|
2917
|
+
limitedByMaxResults: false
|
|
2918
|
+
};
|
|
2919
|
+
}
|
|
2920
|
+
async function globFilesTool(args) {
|
|
2921
|
+
const parsed = parseWorkspaceToolArguments(args.call.arguments);
|
|
2922
|
+
const pattern = requireWorkspaceGlobPattern(parsed);
|
|
2923
|
+
const exclude = readWorkspaceExcludeGlobs(parsed);
|
|
2924
|
+
const maxResults = readWorkspaceMaxResults(parsed);
|
|
2925
|
+
const requestedPath = readWorkspacePathAlias(parsed) ?? ".";
|
|
2926
|
+
const includeIgnored = parsed.includeIgnored === true;
|
|
2927
|
+
const searchPath = await resolveLocalWorkspaceToolPath({
|
|
2928
|
+
workspaceRoot: args.workspaceRoot,
|
|
2929
|
+
requestedPath,
|
|
2930
|
+
...args.confirmExternalFileAccess ? { confirmExternalFileAccess: args.confirmExternalFileAccess } : {}
|
|
2931
|
+
});
|
|
2932
|
+
const relativeSearchPath = normalizeWorkspaceRelativePath({
|
|
2933
|
+
workspaceRoot: resolve(args.workspaceRoot),
|
|
2934
|
+
targetPath: searchPath
|
|
2935
|
+
});
|
|
2936
|
+
const command = [
|
|
2937
|
+
"rg",
|
|
2938
|
+
"--files",
|
|
2939
|
+
"--hidden",
|
|
2940
|
+
...includeIgnored ? ["--no-ignore"] : [],
|
|
2941
|
+
"--glob",
|
|
2942
|
+
pattern,
|
|
2943
|
+
"--glob",
|
|
2944
|
+
"!node_modules",
|
|
2945
|
+
"--glob",
|
|
2946
|
+
"!.git",
|
|
2947
|
+
...exclude.flatMap((excludePattern) => ["--glob", `!${excludePattern}`]),
|
|
2948
|
+
...relativeSearchPath === "." ? [] : [relativeSearchPath]
|
|
2949
|
+
];
|
|
2950
|
+
const result = await runWorkspaceCommand({
|
|
2951
|
+
workspaceRoot: args.workspaceRoot,
|
|
2952
|
+
command
|
|
2953
|
+
});
|
|
2954
|
+
const pathPrefix = relativeSearchPath === "." ? "" : `${relativeSearchPath.replace(/\/+$/, "")}/`;
|
|
2955
|
+
let files = result.stdout.split(/\r?\n/).map((line) => line.trim().replace(/^\.\//, "")).filter(Boolean).filter((line) => !pathPrefix || line === relativeSearchPath || line.startsWith(pathPrefix)).sort((left, right) => left.localeCompare(right));
|
|
2956
|
+
files = await filterRootGitignoredFiles({
|
|
2957
|
+
workspaceRoot: args.workspaceRoot,
|
|
2958
|
+
files,
|
|
2959
|
+
includeIgnored
|
|
2960
|
+
});
|
|
2961
|
+
if (files.length === 0 && (includeIgnored || !await hasRootGitignore(args.workspaceRoot))) {
|
|
2962
|
+
files = scanWorkspaceGlobFiles({
|
|
2963
|
+
workspaceRoot: args.workspaceRoot,
|
|
2964
|
+
pattern,
|
|
2965
|
+
relativeSearchPath,
|
|
2966
|
+
exclude
|
|
2967
|
+
});
|
|
2968
|
+
}
|
|
2969
|
+
const commandLimitedByMaxResults = false;
|
|
2970
|
+
const totalCount = commandLimitedByMaxResults ? void 0 : files.length;
|
|
2971
|
+
const limitedFiles = maxResults ? files.slice(0, maxResults) : files;
|
|
2972
|
+
const limitedByMaxResults = commandLimitedByMaxResults || limitedFiles.length < (totalCount ?? limitedFiles.length);
|
|
2973
|
+
const activity = extractActivity(parsed);
|
|
2974
|
+
return {
|
|
2975
|
+
content: limitedFiles.join("\n"),
|
|
2976
|
+
metadata: {
|
|
2977
|
+
pattern,
|
|
2978
|
+
effectivePattern: pathPrefix ? `${pathPrefix}${pattern}` : pattern,
|
|
2979
|
+
path: requestedPath,
|
|
2980
|
+
searchedPath: relativeSearchPath,
|
|
2981
|
+
exclude,
|
|
2982
|
+
includeIgnored,
|
|
2983
|
+
count: limitedFiles.length,
|
|
2984
|
+
...totalCount === void 0 ? {} : { totalCount },
|
|
2985
|
+
truncated: limitedByMaxResults,
|
|
2986
|
+
limitedByMaxResults,
|
|
2987
|
+
...maxResults ? { maxResults } : {},
|
|
2988
|
+
exitCode: result.exitCode,
|
|
2989
|
+
...activity ? { activity } : {}
|
|
2990
|
+
}
|
|
2991
|
+
};
|
|
2992
|
+
}
|
|
2993
|
+
async function resolveVisualStateBaseUrl(args) {
|
|
2994
|
+
if (args.explicitBaseUrl) return args.explicitBaseUrl;
|
|
2995
|
+
return DEFAULT_LOCAL_API_ORIGIN;
|
|
2996
|
+
}
|
|
2997
|
+
async function captureVisualStateTool(args) {
|
|
2998
|
+
const parsed = parseWorkspaceToolArguments(args.call.arguments);
|
|
2999
|
+
const captureArgs = readVisualStateCaptureArgs(parsed);
|
|
3000
|
+
const baseUrl = await resolveVisualStateBaseUrl({
|
|
3001
|
+
explicitBaseUrl: captureArgs.baseUrl
|
|
3002
|
+
});
|
|
3003
|
+
const extraArgs = [
|
|
3004
|
+
"--base",
|
|
3005
|
+
baseUrl,
|
|
3006
|
+
"--path",
|
|
3007
|
+
captureArgs.path,
|
|
3008
|
+
"--wait-selector",
|
|
3009
|
+
captureArgs.waitSelector,
|
|
3010
|
+
...captureArgs.scrollSelector ? ["--scroll-selector", captureArgs.scrollSelector] : [],
|
|
3011
|
+
...captureArgs.focusSelector ? ["--focus-selector", captureArgs.focusSelector] : [],
|
|
3012
|
+
...captureArgs.expectText ? ["--expect-text", captureArgs.expectText] : [],
|
|
3013
|
+
"--screenshot",
|
|
3014
|
+
captureArgs.screenshotPath,
|
|
3015
|
+
"--metrics",
|
|
3016
|
+
captureArgs.metricsPath
|
|
3017
|
+
];
|
|
3018
|
+
const result = await runWorkspacePackageScript({
|
|
3019
|
+
workspaceRoot: args.workspaceRoot,
|
|
3020
|
+
script: "probe:visual-review",
|
|
3021
|
+
extraArgs,
|
|
3022
|
+
commandTimeoutMs: args.commandTimeoutMs
|
|
3023
|
+
});
|
|
3024
|
+
const metrics = parseLastJsonObject(String(result.metadata?.stdoutTail ?? result.content));
|
|
3025
|
+
return {
|
|
3026
|
+
content: [
|
|
3027
|
+
`pageUrl: ${typeof metrics?.pageUrl === "string" ? metrics.pageUrl : ""}`,
|
|
3028
|
+
`screenshotPath: ${captureArgs.screenshotPath}`,
|
|
3029
|
+
`metricsPath: ${captureArgs.metricsPath}`,
|
|
3030
|
+
`waitSelector: ${captureArgs.waitSelector}`,
|
|
3031
|
+
...captureArgs.focusSelector ? [`focusSelector: ${captureArgs.focusSelector}`] : [],
|
|
3032
|
+
...captureArgs.expectText ? [`expectText: ${captureArgs.expectText}`] : [],
|
|
3033
|
+
"",
|
|
3034
|
+
result.content
|
|
3035
|
+
].join("\n").trim(),
|
|
3036
|
+
metadata: {
|
|
3037
|
+
...result.metadata,
|
|
3038
|
+
script: "probe:visual-review",
|
|
3039
|
+
args: extraArgs,
|
|
3040
|
+
baseUrl,
|
|
3041
|
+
path: captureArgs.path,
|
|
3042
|
+
waitSelector: captureArgs.waitSelector,
|
|
3043
|
+
scrollSelector: captureArgs.scrollSelector,
|
|
3044
|
+
focusSelector: captureArgs.focusSelector,
|
|
3045
|
+
expectText: captureArgs.expectText,
|
|
3046
|
+
screenshotPath: captureArgs.screenshotPath,
|
|
3047
|
+
metricsPath: captureArgs.metricsPath,
|
|
3048
|
+
...metrics ?? {}
|
|
3049
|
+
}
|
|
3050
|
+
};
|
|
3051
|
+
}
|
|
3052
|
+
async function execShellTool(args) {
|
|
3053
|
+
const parsed = parseWorkspaceToolArguments(args.call.arguments);
|
|
3054
|
+
const command = requireShellCommand(parsed, args.call.name);
|
|
3055
|
+
if (args.restrictToWorkspace) {
|
|
3056
|
+
const escapeToken = findWorkspaceShellEscapeToken(command);
|
|
3057
|
+
if (escapeToken) {
|
|
3058
|
+
return buildWorkspaceShellEscapeBlockedResult({ command, token: escapeToken });
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
const interactiveAuthCommand = extractInteractiveGhAuthCommand(command);
|
|
3062
|
+
if (interactiveAuthCommand) {
|
|
3063
|
+
const activity2 = extractActivity(parsed);
|
|
3064
|
+
const blocked = buildInteractiveCommandBlockedResult(interactiveAuthCommand);
|
|
3065
|
+
return {
|
|
3066
|
+
...blocked,
|
|
3067
|
+
metadata: {
|
|
3068
|
+
...blocked.metadata,
|
|
3069
|
+
...activity2 ? { activity: activity2 } : {}
|
|
3070
|
+
}
|
|
3071
|
+
};
|
|
3072
|
+
}
|
|
3073
|
+
const result = await runWorkspaceCommand({
|
|
3074
|
+
workspaceRoot: args.workspaceRoot,
|
|
3075
|
+
command: buildWorkspaceShellCommand({
|
|
3076
|
+
toolName: args.call.name,
|
|
3077
|
+
command,
|
|
3078
|
+
shell: parsed.shell
|
|
3079
|
+
}),
|
|
3080
|
+
timeoutMs: resolveExecShellTimeoutMs(args.commandTimeoutMs),
|
|
3081
|
+
outputLimit: args.commandOutputLimit,
|
|
3082
|
+
commandPrefix: args.commandPrefix,
|
|
3083
|
+
abortSignal: args.abortSignal,
|
|
3084
|
+
detachMs: args.detachMs
|
|
3085
|
+
});
|
|
3086
|
+
const activity = extractActivity(parsed);
|
|
3087
|
+
return {
|
|
3088
|
+
content: result.content,
|
|
3089
|
+
metadata: {
|
|
3090
|
+
command,
|
|
3091
|
+
exitCode: result.exitCode,
|
|
3092
|
+
timedOut: result.timedOut,
|
|
3093
|
+
...result.aborted ? { aborted: true } : {},
|
|
3094
|
+
...result.detached ? { detached: true, pid: result.pid, label: result.label, status: "running" } : {},
|
|
3095
|
+
...activity ? { activity } : {}
|
|
3096
|
+
}
|
|
3097
|
+
};
|
|
3098
|
+
}
|
|
3099
|
+
function deriveLabel(command) {
|
|
3100
|
+
const trimmed = command.trim();
|
|
3101
|
+
if (!trimmed) return "process";
|
|
3102
|
+
const tokens = trimmed.split(/\s+/);
|
|
3103
|
+
const lastToken = tokens[tokens.length - 1] ?? trimmed;
|
|
3104
|
+
const labelCandidate = lastToken.split(/[/\\]/).pop() || lastToken;
|
|
3105
|
+
return labelCandidate || trimmed;
|
|
3106
|
+
}
|
|
3107
|
+
async function launchProcessTool(args) {
|
|
3108
|
+
const parsed = parseWorkspaceToolArguments(args.call.arguments);
|
|
3109
|
+
const command = requireShellCommand(parsed, args.call.name);
|
|
3110
|
+
if (args.restrictToWorkspace) {
|
|
3111
|
+
const escapeToken = findWorkspaceShellEscapeToken(command);
|
|
3112
|
+
if (escapeToken) {
|
|
3113
|
+
return buildWorkspaceShellEscapeBlockedResult({ command, token: escapeToken });
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
const labelArg = readTrimmedString(parsed.label);
|
|
3117
|
+
const label = labelArg || deriveLabel(command);
|
|
3118
|
+
const fullCommand = buildWorkspaceShellCommand({
|
|
3119
|
+
toolName: args.call.name,
|
|
3120
|
+
command,
|
|
3121
|
+
shell: parsed.shell
|
|
3122
|
+
});
|
|
3123
|
+
const cmdArgs = [
|
|
3124
|
+
...args.commandPrefix ?? [],
|
|
3125
|
+
...fullCommand
|
|
3126
|
+
];
|
|
3127
|
+
const detached = process.platform !== "win32";
|
|
3128
|
+
const proc = spawnChildProcess(cmdArgs[0] ?? "", cmdArgs.slice(1), {
|
|
3129
|
+
cwd: resolve(args.workspaceRoot),
|
|
3130
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3131
|
+
detached
|
|
3132
|
+
});
|
|
3133
|
+
const pid = proc.pid;
|
|
3134
|
+
if (typeof pid !== "number") {
|
|
3135
|
+
return {
|
|
3136
|
+
content: JSON.stringify({ error: "Failed to spawn child process" }),
|
|
3137
|
+
metadata: { command, status: "failed" }
|
|
3138
|
+
};
|
|
3139
|
+
}
|
|
3140
|
+
const pgid = detached ? pid : pid;
|
|
3141
|
+
const registry2 = getProcessRegistry();
|
|
3142
|
+
registry2.add({ pid, pgid, command, label });
|
|
3143
|
+
const cleanupChildOnHostSignal = (signal) => {
|
|
3144
|
+
try {
|
|
3145
|
+
if (detached) {
|
|
3146
|
+
process.kill(-pid, signal);
|
|
3147
|
+
} else {
|
|
3148
|
+
process.kill(pid, signal);
|
|
3149
|
+
}
|
|
3150
|
+
} catch {
|
|
3151
|
+
}
|
|
3152
|
+
};
|
|
3153
|
+
if (detached) {
|
|
3154
|
+
process.once("SIGHUP", cleanupChildOnHostSignal);
|
|
3155
|
+
process.once("SIGTERM", cleanupChildOnHostSignal);
|
|
3156
|
+
process.once("SIGINT", cleanupChildOnHostSignal);
|
|
3157
|
+
}
|
|
3158
|
+
proc.on("close", (code) => {
|
|
3159
|
+
if (detached) {
|
|
3160
|
+
process.removeListener("SIGHUP", cleanupChildOnHostSignal);
|
|
3161
|
+
process.removeListener("SIGTERM", cleanupChildOnHostSignal);
|
|
3162
|
+
process.removeListener("SIGINT", cleanupChildOnHostSignal);
|
|
3163
|
+
}
|
|
3164
|
+
registry2.markExited(pid, code ?? 1);
|
|
3165
|
+
});
|
|
3166
|
+
const activity = extractActivity(parsed);
|
|
3167
|
+
const resultData = { pid, label, status: "running" };
|
|
3168
|
+
return {
|
|
3169
|
+
content: JSON.stringify(resultData),
|
|
3170
|
+
metadata: {
|
|
3171
|
+
command,
|
|
3172
|
+
pid,
|
|
3173
|
+
label,
|
|
3174
|
+
status: "running",
|
|
3175
|
+
processLaunch: resultData,
|
|
3176
|
+
...activity ? { activity } : {}
|
|
3177
|
+
}
|
|
3178
|
+
};
|
|
3179
|
+
}
|
|
3180
|
+
async function listProcessesTool(args) {
|
|
3181
|
+
const list = getProcessRegistry().list();
|
|
3182
|
+
return {
|
|
3183
|
+
content: JSON.stringify(list),
|
|
3184
|
+
metadata: {
|
|
3185
|
+
count: list.length,
|
|
3186
|
+
processes: list
|
|
3187
|
+
}
|
|
3188
|
+
};
|
|
3189
|
+
}
|
|
3190
|
+
function createLocalWorkspaceToolExecutors(args) {
|
|
3191
|
+
const fileAccess = args.confirmExternalFileAccess ? { confirmExternalFileAccess: args.confirmExternalFileAccess } : {};
|
|
3192
|
+
return {
|
|
3193
|
+
editFile: (call) => editFileTool({
|
|
3194
|
+
call,
|
|
3195
|
+
workspaceRoot: args.workspaceRoot,
|
|
3196
|
+
...fileAccess
|
|
3197
|
+
}),
|
|
3198
|
+
globFiles: (call) => globFilesTool({
|
|
3199
|
+
call,
|
|
3200
|
+
workspaceRoot: args.workspaceRoot,
|
|
3201
|
+
...fileAccess
|
|
3202
|
+
}),
|
|
3203
|
+
listFiles: (call) => listFilesTool({
|
|
3204
|
+
call,
|
|
3205
|
+
workspaceRoot: args.workspaceRoot,
|
|
3206
|
+
...fileAccess
|
|
3207
|
+
}),
|
|
3208
|
+
readFile: (call) => readFileTool({
|
|
3209
|
+
call,
|
|
3210
|
+
workspaceRoot: args.workspaceRoot,
|
|
3211
|
+
...fileAccess
|
|
3212
|
+
}),
|
|
3213
|
+
writeFile: (call) => writeFileTool({
|
|
3214
|
+
call,
|
|
3215
|
+
workspaceRoot: args.workspaceRoot,
|
|
3216
|
+
...fileAccess
|
|
3217
|
+
}),
|
|
3218
|
+
searchFiles: (call) => searchFilesTool({
|
|
3219
|
+
call,
|
|
3220
|
+
workspaceRoot: args.workspaceRoot,
|
|
3221
|
+
...fileAccess
|
|
3222
|
+
}),
|
|
3223
|
+
captureVisualState: (call) => captureVisualStateTool({
|
|
3224
|
+
call,
|
|
3225
|
+
workspaceRoot: args.workspaceRoot,
|
|
3226
|
+
commandTimeoutMs: args.commandTimeoutMs
|
|
3227
|
+
}),
|
|
3228
|
+
execShell: (call, opts) => execShellTool({
|
|
3229
|
+
call,
|
|
3230
|
+
workspaceRoot: args.workspaceRoot,
|
|
3231
|
+
commandTimeoutMs: args.commandTimeoutMs,
|
|
3232
|
+
commandOutputLimit: args.commandOutputLimit,
|
|
3233
|
+
commandPrefix: args.commandPrefix,
|
|
3234
|
+
restrictToWorkspace: args.restrictShellToWorkspace,
|
|
3235
|
+
abortSignal: opts?.abortSignal ?? args.abortSignal,
|
|
3236
|
+
detachMs: opts?.detachMs ?? args.detachMs
|
|
3237
|
+
}),
|
|
3238
|
+
launchProcess: (call) => launchProcessTool({
|
|
3239
|
+
call,
|
|
3240
|
+
workspaceRoot: args.workspaceRoot,
|
|
3241
|
+
commandPrefix: args.commandPrefix,
|
|
3242
|
+
restrictToWorkspace: args.restrictShellToWorkspace
|
|
3243
|
+
}),
|
|
3244
|
+
listProcesses: (call) => listProcessesTool({
|
|
3245
|
+
call
|
|
3246
|
+
})
|
|
3247
|
+
};
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
export {
|
|
3251
|
+
parseSseDataLineJson,
|
|
3252
|
+
buildOpenAiCompatibleChatCompletionRequest,
|
|
3253
|
+
parseOpenAiCompatibleChatCompletionResponse,
|
|
3254
|
+
readOpenAiCompatibleSseCompletion,
|
|
3255
|
+
executeOpenAiCompatibleChatCompletion,
|
|
3256
|
+
resolveOpenAiCompatibleProviderConfig,
|
|
3257
|
+
buildAgentRuntimeAgentLookupKeys,
|
|
3258
|
+
shouldFetchAgentRuntimeRecordRemotely,
|
|
3259
|
+
dialogMessageRecordToAgentRuntimeMessage,
|
|
3260
|
+
buildAgentRuntimeDialogWritePlan,
|
|
3261
|
+
shouldCacheHybridRemoteRecord,
|
|
3262
|
+
createHybridRecordStore,
|
|
3263
|
+
isDestructiveShellCommand,
|
|
3264
|
+
evaluateShellCommandPolicy,
|
|
3265
|
+
resolveLocalToolPolicy,
|
|
3266
|
+
executeLocalToolWithPolicy,
|
|
3267
|
+
getProcessRegistry,
|
|
3268
|
+
buildLocalWorkspaceToolset,
|
|
3269
|
+
buildLocalWorkspacePolicyToolNames,
|
|
3270
|
+
buildLocalWorkspaceOpenAiTools,
|
|
3271
|
+
createLocalWorkspaceToolExecutors
|
|
3272
|
+
};
|