@geteigen/cli 0.0.0-stage → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +154 -2
- package/dist/agent.d.ts +32 -0
- package/dist/agent.js +246 -0
- package/dist/auth.d.ts +5 -0
- package/dist/auth.js +33 -0
- package/dist/cli/src/agent.d.ts +24 -0
- package/dist/cli/src/agent.js +263 -0
- package/dist/cli/src/agentTools.d.ts +6 -0
- package/dist/cli/src/agentTools.js +104 -0
- package/dist/cli/src/auth.d.ts +5 -0
- package/dist/cli/src/auth.js +33 -0
- package/dist/cli/src/cliEnv.d.ts +6 -0
- package/dist/cli/src/cliEnv.js +26 -0
- package/dist/cli/src/config.d.ts +11 -0
- package/dist/cli/src/config.js +51 -0
- package/dist/cli/src/cursorAuth.d.ts +10 -0
- package/dist/cli/src/cursorAuth.js +92 -0
- package/dist/cli/src/gitAuthEnv.d.ts +5 -0
- package/dist/cli/src/gitAuthEnv.js +35 -0
- package/dist/cli/src/index.d.ts +2 -0
- package/dist/cli/src/index.js +244 -0
- package/dist/cli/src/listModels.d.ts +14 -0
- package/dist/cli/src/listModels.js +196 -0
- package/dist/cli/src/listSessions.d.ts +9 -0
- package/dist/cli/src/listSessions.js +37 -0
- package/dist/cli/src/machineConfig.d.ts +20 -0
- package/dist/cli/src/machineConfig.js +66 -0
- package/dist/cli/src/pickFolder.d.ts +5 -0
- package/dist/cli/src/pickFolder.js +65 -0
- package/dist/cli/src/proc.d.ts +31 -0
- package/dist/cli/src/proc.js +97 -0
- package/dist/cli/src/service.d.ts +17 -0
- package/dist/cli/src/service.js +255 -0
- package/dist/cli/src/workRoots.d.ts +30 -0
- package/dist/cli/src/workRoots.js +228 -0
- package/dist/cli/src/worker.d.ts +8 -0
- package/dist/cli/src/worker.js +51 -0
- package/dist/cli/src/workerCore.d.ts +47 -0
- package/dist/cli/src/workerCore.js +721 -0
- package/dist/cli/src/workspace.d.ts +10 -0
- package/dist/cli/src/workspace.js +329 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.js +51 -0
- package/dist/cursorAuth.d.ts +10 -0
- package/dist/cursorAuth.js +95 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +108 -0
- package/dist/listModels.d.ts +6 -0
- package/dist/listModels.js +62 -0
- package/dist/listSessions.d.ts +9 -0
- package/dist/listSessions.js +37 -0
- package/dist/pickFolder.d.ts +5 -0
- package/dist/pickFolder.js +65 -0
- package/dist/shared/actionDrafts.d.ts +83 -0
- package/dist/shared/actionDrafts.js +169 -0
- package/dist/shared/agentArtifacts.d.ts +49 -0
- package/dist/shared/agentArtifacts.js +169 -0
- package/dist/shared/agentLabel.d.ts +18 -0
- package/dist/shared/agentLabel.js +22 -0
- package/dist/shared/agentMentions.d.ts +38 -0
- package/dist/shared/agentMentions.js +110 -0
- package/dist/shared/agentRepos.d.ts +28 -0
- package/dist/shared/agentRepos.js +60 -0
- package/dist/shared/agentRunStatus.d.ts +12 -0
- package/dist/shared/agentRunStatus.js +17 -0
- package/dist/shared/agentUsage.d.ts +24 -0
- package/dist/shared/agentUsage.js +139 -0
- package/dist/shared/approvals.d.ts +44 -0
- package/dist/shared/approvals.js +97 -0
- package/dist/shared/askEigen.d.ts +190 -0
- package/dist/shared/askEigen.js +76 -0
- package/dist/shared/attribution.d.ts +25 -0
- package/dist/shared/attribution.js +63 -0
- package/dist/shared/automations.d.ts +135 -0
- package/dist/shared/automations.js +553 -0
- package/dist/shared/backends/claudeCode.d.ts +44 -0
- package/dist/shared/backends/claudeCode.js +548 -0
- package/dist/shared/backends/codex.d.ts +15 -0
- package/dist/shared/backends/codex.js +287 -0
- package/dist/shared/backends/cursor.d.ts +111 -0
- package/dist/shared/backends/cursor.js +1216 -0
- package/dist/shared/backends/index.d.ts +18 -0
- package/dist/shared/backends/index.js +58 -0
- package/dist/shared/backends/opencode.d.ts +43 -0
- package/dist/shared/backends/opencode.js +415 -0
- package/dist/shared/backends/types.d.ts +171 -0
- package/dist/shared/backends/types.js +19 -0
- package/dist/shared/backends/usage.d.ts +8 -0
- package/dist/shared/backends/usage.js +71 -0
- package/dist/shared/billingPlans.d.ts +60 -0
- package/dist/shared/billingPlans.js +262 -0
- package/dist/shared/brain.d.ts +353 -0
- package/dist/shared/brain.js +487 -0
- package/dist/shared/clarifyingQuestions.d.ts +13 -0
- package/dist/shared/clarifyingQuestions.js +207 -0
- package/dist/shared/claudeModels.d.ts +10 -0
- package/dist/shared/claudeModels.js +52 -0
- package/dist/shared/cloudBackends.d.ts +21 -0
- package/dist/shared/cloudBackends.js +29 -0
- package/dist/shared/codexModels.d.ts +10 -0
- package/dist/shared/codexModels.js +28 -0
- package/dist/shared/contextWindows.d.ts +2 -0
- package/dist/shared/contextWindows.js +30 -0
- package/dist/shared/cursorMcpConfig.d.ts +14 -0
- package/dist/shared/cursorMcpConfig.js +43 -0
- package/dist/shared/cursorProjectMcp.d.ts +10 -0
- package/dist/shared/cursorProjectMcp.js +42 -0
- package/dist/shared/docs.d.ts +59 -0
- package/dist/shared/docs.js +87 -0
- package/dist/shared/events.d.ts +932 -0
- package/dist/shared/events.js +57 -0
- package/dist/shared/integrations.d.ts +266 -0
- package/dist/shared/integrations.js +449 -0
- package/dist/shared/issueTranscript.d.ts +64 -0
- package/dist/shared/issueTranscript.js +387 -0
- package/dist/shared/issues.d.ts +139 -0
- package/dist/shared/issues.js +217 -0
- package/dist/shared/machines.d.ts +119 -0
- package/dist/shared/machines.js +58 -0
- package/dist/shared/markdownMedia.d.ts +13 -0
- package/dist/shared/markdownMedia.js +41 -0
- package/dist/shared/notificationText.d.ts +6 -0
- package/dist/shared/notificationText.js +38 -0
- package/dist/shared/opencodeModels.d.ts +19 -0
- package/dist/shared/opencodeModels.js +62 -0
- package/dist/shared/opencodeSession.d.ts +7 -0
- package/dist/shared/opencodeSession.js +30 -0
- package/dist/shared/orchestration.d.ts +58 -0
- package/dist/shared/orchestration.js +12 -0
- package/dist/shared/orgs.d.ts +47 -0
- package/dist/shared/orgs.js +49 -0
- package/dist/shared/personalOrg.d.ts +5 -0
- package/dist/shared/personalOrg.js +12 -0
- package/dist/shared/plans.d.ts +3 -0
- package/dist/shared/plans.js +19 -0
- package/dist/shared/providerModels.d.ts +40 -0
- package/dist/shared/providerModels.js +503 -0
- package/dist/shared/roomAttention.d.ts +17 -0
- package/dist/shared/roomAttention.js +36 -0
- package/dist/shared/roomContext.d.ts +120 -0
- package/dist/shared/roomContext.js +133 -0
- package/dist/shared/roomPermissions.d.ts +56 -0
- package/dist/shared/roomPermissions.js +133 -0
- package/dist/shared/sessionSeats.d.ts +29 -0
- package/dist/shared/sessionSeats.js +57 -0
- package/dist/shared/slashCommands.d.ts +39 -0
- package/dist/shared/slashCommands.js +166 -0
- package/dist/shared/space.d.ts +33 -0
- package/dist/shared/space.js +54 -0
- package/dist/shared/spaceFeed.d.ts +78 -0
- package/dist/shared/spaceFeed.js +194 -0
- package/dist/shared/stringifyUnknown.d.ts +1 -0
- package/dist/shared/stringifyUnknown.js +1 -0
- package/dist/shared/swarm.d.ts +320 -0
- package/dist/shared/swarm.js +328 -0
- package/dist/shared/swarmVisual.d.ts +79 -0
- package/dist/shared/swarmVisual.js +212 -0
- package/dist/shared/terminalFilter.d.ts +13 -0
- package/dist/shared/terminalFilter.js +46 -0
- package/dist/shared/toolIntegration.d.ts +56 -0
- package/dist/shared/toolIntegration.js +180 -0
- package/dist/shared/typing.d.ts +9 -0
- package/dist/shared/typing.js +37 -0
- package/dist/shared/uploads.d.ts +31 -0
- package/dist/shared/uploads.js +65 -0
- package/dist/shared/voiceApprovals.d.ts +36 -0
- package/dist/shared/voiceApprovals.js +229 -0
- package/dist/shared/workspace.d.ts +49 -0
- package/dist/shared/workspace.js +53 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +266 -0
- package/package.json +51 -4
|
@@ -0,0 +1,1216 @@
|
|
|
1
|
+
const EMPTY_UNKNOWN = new Set(["", "[object Object]", "undefined", "null"]);
|
|
2
|
+
function isEmptyUnknown(text) {
|
|
3
|
+
return EMPTY_UNKNOWN.has(text.trim());
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Turn unknown thrown values / CLI payloads into a user-visible string.
|
|
7
|
+
* Never returns "[object Object]". Lives here so Next (extensionless) and
|
|
8
|
+
* Node ESM (`*.js`) both resolve it without a new shared module.
|
|
9
|
+
*/
|
|
10
|
+
export function stringifyUnknown(value, fallback = "", depth = 0) {
|
|
11
|
+
if (value == null)
|
|
12
|
+
return fallback;
|
|
13
|
+
if (depth > 4)
|
|
14
|
+
return fallback;
|
|
15
|
+
if (typeof value === "string") {
|
|
16
|
+
return isEmptyUnknown(value) ? fallback : value;
|
|
17
|
+
}
|
|
18
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
19
|
+
return String(value);
|
|
20
|
+
}
|
|
21
|
+
if (typeof value === "bigint")
|
|
22
|
+
return value.toString();
|
|
23
|
+
if (value instanceof Error) {
|
|
24
|
+
const msg = value.message?.trim();
|
|
25
|
+
if (msg && !isEmptyUnknown(msg))
|
|
26
|
+
return msg;
|
|
27
|
+
const cause = value.cause;
|
|
28
|
+
if (cause !== undefined) {
|
|
29
|
+
const nested = stringifyUnknown(cause, "", depth + 1);
|
|
30
|
+
if (nested)
|
|
31
|
+
return nested;
|
|
32
|
+
}
|
|
33
|
+
return value.name || fallback;
|
|
34
|
+
}
|
|
35
|
+
if (Array.isArray(value)) {
|
|
36
|
+
const parts = value
|
|
37
|
+
.map((item) => stringifyUnknown(item, "", depth + 1))
|
|
38
|
+
.filter((part) => part && !isEmptyUnknown(part));
|
|
39
|
+
return parts.length ? parts.join("\n") : fallback;
|
|
40
|
+
}
|
|
41
|
+
if (typeof value === "object") {
|
|
42
|
+
const rec = value;
|
|
43
|
+
for (const key of [
|
|
44
|
+
"message",
|
|
45
|
+
"error",
|
|
46
|
+
"detail",
|
|
47
|
+
"description",
|
|
48
|
+
"reason",
|
|
49
|
+
"result",
|
|
50
|
+
"stderr",
|
|
51
|
+
"stdout",
|
|
52
|
+
"text",
|
|
53
|
+
"content",
|
|
54
|
+
]) {
|
|
55
|
+
if (!(key in rec))
|
|
56
|
+
continue;
|
|
57
|
+
const inner = stringifyUnknown(rec[key], "", depth + 1);
|
|
58
|
+
if (inner && !isEmptyUnknown(inner))
|
|
59
|
+
return inner;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const json = JSON.stringify(value);
|
|
63
|
+
if (json && json !== "{}" && json !== "[]" && !isEmptyUnknown(json)) {
|
|
64
|
+
return json.length > 800 ? `${json.slice(0, 800)}…` : json;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// circular / bigint
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return fallback;
|
|
72
|
+
}
|
|
73
|
+
export function errorMessage(err, fallback = "Unknown error") {
|
|
74
|
+
const text = stringifyUnknown(err, "");
|
|
75
|
+
return text && !isEmptyUnknown(text) ? text : fallback;
|
|
76
|
+
}
|
|
77
|
+
const QUESTION_TOOL_RE = /^(askuserquestion|askfollowupquestion|askquestion|ask_user|ask_followup_question|ask_user_question|clarify|question|userinput|getinput)/i;
|
|
78
|
+
/** Whether a tool name represents an agent asking the user clarifying questions. */
|
|
79
|
+
export function isQuestionTool(name) {
|
|
80
|
+
return QUESTION_TOOL_RE.test(name.replace(/ToolCall$/i, ""));
|
|
81
|
+
}
|
|
82
|
+
function parseQuestionOption(opt) {
|
|
83
|
+
if (typeof opt === "string" && opt.trim())
|
|
84
|
+
return { label: opt.trim() };
|
|
85
|
+
if (!opt || typeof opt !== "object")
|
|
86
|
+
return null;
|
|
87
|
+
const o = opt;
|
|
88
|
+
const label = String(o.label ?? o.text ?? o.title ?? "").trim();
|
|
89
|
+
const description = typeof o.description === "string" ? o.description.trim() : undefined;
|
|
90
|
+
return label ? { label, description } : null;
|
|
91
|
+
}
|
|
92
|
+
/** Parse structured clarifying questions from a tool's args (Claude AskUserQuestion, etc). */
|
|
93
|
+
export function parseQuestionToolArgs(args) {
|
|
94
|
+
if (!args || typeof args !== "object")
|
|
95
|
+
return [];
|
|
96
|
+
const raw = args;
|
|
97
|
+
if (Array.isArray(raw.questions)) {
|
|
98
|
+
const list = [];
|
|
99
|
+
for (let i = 0; i < raw.questions.length; i++) {
|
|
100
|
+
const q = raw.questions[i];
|
|
101
|
+
if (typeof q === "string" && q.trim()) {
|
|
102
|
+
list.push({ id: `q-${i + 1}`, question: q.trim() });
|
|
103
|
+
}
|
|
104
|
+
else if (q && typeof q === "object") {
|
|
105
|
+
const item = q;
|
|
106
|
+
const questionText = String(item.question ?? item.prompt ?? item.text ?? item.title ?? "").trim();
|
|
107
|
+
if (!questionText)
|
|
108
|
+
continue;
|
|
109
|
+
const options = Array.isArray(item.options)
|
|
110
|
+
? item.options.map(parseQuestionOption).filter((o) => o !== null)
|
|
111
|
+
: undefined;
|
|
112
|
+
list.push({
|
|
113
|
+
id: String(item.id || `q-${i + 1}`),
|
|
114
|
+
question: questionText,
|
|
115
|
+
header: typeof item.header === "string" ? item.header.trim() : undefined,
|
|
116
|
+
options: options && options.length ? options : undefined,
|
|
117
|
+
multiSelect: Boolean(item.multiSelect),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (list.length)
|
|
122
|
+
return list;
|
|
123
|
+
}
|
|
124
|
+
const singleText = String(raw.question ?? raw.prompt ?? raw.query ?? raw.message ?? "").trim();
|
|
125
|
+
if (singleText) {
|
|
126
|
+
const options = Array.isArray(raw.options)
|
|
127
|
+
? raw.options.map(parseQuestionOption).filter((o) => o !== null)
|
|
128
|
+
: undefined;
|
|
129
|
+
return [
|
|
130
|
+
{
|
|
131
|
+
id: "q-1",
|
|
132
|
+
question: singleText,
|
|
133
|
+
header: typeof raw.header === "string" ? raw.header.trim() : undefined,
|
|
134
|
+
options: options && options.length ? options : undefined,
|
|
135
|
+
multiSelect: Boolean(raw.multiSelect),
|
|
136
|
+
},
|
|
137
|
+
];
|
|
138
|
+
}
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Merge a new assistant payload into the accumulated stream.
|
|
143
|
+
* Cursor `--stream-partial-output` may send either token deltas or a growing
|
|
144
|
+
* snapshot of the same message. Blind concatenation duplicates the paragraph;
|
|
145
|
+
* blindly replacing with a shorter payload truncates it.
|
|
146
|
+
*/
|
|
147
|
+
export function mergeAssistantText(prev, incoming) {
|
|
148
|
+
if (!incoming)
|
|
149
|
+
return prev;
|
|
150
|
+
if (!prev)
|
|
151
|
+
return incoming;
|
|
152
|
+
if (incoming === prev)
|
|
153
|
+
return prev;
|
|
154
|
+
if (incoming.startsWith(prev))
|
|
155
|
+
return incoming;
|
|
156
|
+
if (prev.startsWith(incoming))
|
|
157
|
+
return prev;
|
|
158
|
+
return prev + incoming;
|
|
159
|
+
}
|
|
160
|
+
const THINKING_PART_TYPES = new Set([
|
|
161
|
+
"thinking",
|
|
162
|
+
"reasoning",
|
|
163
|
+
"redacted_thinking",
|
|
164
|
+
"thinking_delta",
|
|
165
|
+
"reasoning_delta",
|
|
166
|
+
]);
|
|
167
|
+
function isThinkingPartType(type) {
|
|
168
|
+
return THINKING_PART_TYPES.has(type) || type.endsWith("thinking") || type.endsWith("reasoning");
|
|
169
|
+
}
|
|
170
|
+
/** Collapse hard wraps so thinking reads as prose, not one phrase per line. */
|
|
171
|
+
export function normalizeThinkingText(text) {
|
|
172
|
+
return text
|
|
173
|
+
.replace(/\r\n/g, "\n")
|
|
174
|
+
.replace(/\s*\n+\s*/g, " ")
|
|
175
|
+
.replace(/[ \t]{2,}/g, " ");
|
|
176
|
+
}
|
|
177
|
+
/** Merge streamed thinking: snapshots replace, deltas append with a space. */
|
|
178
|
+
export function mergeThinkingText(prev, incoming) {
|
|
179
|
+
if (!incoming)
|
|
180
|
+
return prev;
|
|
181
|
+
const chunk = normalizeThinkingText(incoming).trim();
|
|
182
|
+
if (!chunk)
|
|
183
|
+
return prev;
|
|
184
|
+
const base = normalizeThinkingText(prev).trim();
|
|
185
|
+
if (!base)
|
|
186
|
+
return chunk;
|
|
187
|
+
if (chunk === base)
|
|
188
|
+
return base;
|
|
189
|
+
if (chunk.startsWith(base))
|
|
190
|
+
return chunk;
|
|
191
|
+
if (base.startsWith(chunk))
|
|
192
|
+
return base;
|
|
193
|
+
return `${base} ${chunk}`;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Cursor CLI streams thinking as phrase-sized JSON lines that are not prefixes
|
|
197
|
+
* of each other. Always concatenate; a new thought starts only after tools
|
|
198
|
+
* (`afterTools` in RoomManager).
|
|
199
|
+
*/
|
|
200
|
+
export function advanceThinking(prev, incoming) {
|
|
201
|
+
return { current: mergeThinkingText(prev, incoming) };
|
|
202
|
+
}
|
|
203
|
+
function thinkingFromRecord(rec) {
|
|
204
|
+
for (const key of ["thinking", "reasoning", "text", "content"]) {
|
|
205
|
+
const value = rec[key];
|
|
206
|
+
if (typeof value === "string" && value.trim())
|
|
207
|
+
return value;
|
|
208
|
+
}
|
|
209
|
+
const summary = rec.summary;
|
|
210
|
+
if (Array.isArray(summary)) {
|
|
211
|
+
const parts = summary
|
|
212
|
+
.map((row) => {
|
|
213
|
+
if (typeof row === "string")
|
|
214
|
+
return row;
|
|
215
|
+
if (row && typeof row === "object") {
|
|
216
|
+
const item = row;
|
|
217
|
+
return typeof item.text === "string"
|
|
218
|
+
? item.text
|
|
219
|
+
: typeof item.summary === "string"
|
|
220
|
+
? item.summary
|
|
221
|
+
: "";
|
|
222
|
+
}
|
|
223
|
+
return "";
|
|
224
|
+
})
|
|
225
|
+
.filter((part) => part.trim());
|
|
226
|
+
if (parts.length)
|
|
227
|
+
return parts.join(" ");
|
|
228
|
+
}
|
|
229
|
+
return "";
|
|
230
|
+
}
|
|
231
|
+
/** Pull visible model thinking / reasoning out of a CLI or SDK payload. */
|
|
232
|
+
export function extractThinkingText(source) {
|
|
233
|
+
if (!source || typeof source !== "object")
|
|
234
|
+
return "";
|
|
235
|
+
if (Array.isArray(source)) {
|
|
236
|
+
return source
|
|
237
|
+
.map((part) => extractThinkingText(part))
|
|
238
|
+
.filter(Boolean)
|
|
239
|
+
.join(" ");
|
|
240
|
+
}
|
|
241
|
+
const rec = source;
|
|
242
|
+
const type = String(rec.type ?? rec.kind ?? "").trim();
|
|
243
|
+
if (type === "text" || type === "output_text" || type === "tool_use" || type === "tool_result") {
|
|
244
|
+
return "";
|
|
245
|
+
}
|
|
246
|
+
if (rec.message !== undefined) {
|
|
247
|
+
const nested = extractThinkingText(rec.message);
|
|
248
|
+
if (nested)
|
|
249
|
+
return nested;
|
|
250
|
+
}
|
|
251
|
+
if (Array.isArray(rec.content)) {
|
|
252
|
+
const fromContent = extractThinkingText(rec.content);
|
|
253
|
+
if (fromContent)
|
|
254
|
+
return fromContent;
|
|
255
|
+
}
|
|
256
|
+
if (isThinkingPartType(type) || "thinking" in rec || "reasoning" in rec) {
|
|
257
|
+
return thinkingFromRecord(rec);
|
|
258
|
+
}
|
|
259
|
+
return "";
|
|
260
|
+
}
|
|
261
|
+
function extractText(message) {
|
|
262
|
+
if (!message || typeof message !== "object")
|
|
263
|
+
return "";
|
|
264
|
+
const content = message.content;
|
|
265
|
+
if (typeof content === "string")
|
|
266
|
+
return content;
|
|
267
|
+
if (!Array.isArray(content))
|
|
268
|
+
return "";
|
|
269
|
+
return content
|
|
270
|
+
.map((part) => {
|
|
271
|
+
if (!part || typeof part !== "object")
|
|
272
|
+
return "";
|
|
273
|
+
const rec = part;
|
|
274
|
+
const type = String(rec.type ?? "");
|
|
275
|
+
if (isThinkingPartType(type) || "thinking" in rec)
|
|
276
|
+
return "";
|
|
277
|
+
if (type && type !== "text" && type !== "output_text")
|
|
278
|
+
return "";
|
|
279
|
+
if ("text" in rec)
|
|
280
|
+
return String(rec.text ?? "");
|
|
281
|
+
return "";
|
|
282
|
+
})
|
|
283
|
+
.join("");
|
|
284
|
+
}
|
|
285
|
+
function errorText(value) {
|
|
286
|
+
if (typeof value === "string")
|
|
287
|
+
return value.trim();
|
|
288
|
+
if (value && typeof value === "object") {
|
|
289
|
+
const rec = value;
|
|
290
|
+
for (const k of ["message", "reason", "error", "stderr"]) {
|
|
291
|
+
if (typeof rec[k] === "string" && String(rec[k]).trim())
|
|
292
|
+
return String(rec[k]).trim();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return "";
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Failure message for a finished tool call, or null when it succeeded.
|
|
299
|
+
* Covers SDK `{status:"error", error}` results / call status, CLI
|
|
300
|
+
* `{failure}` / `{error}` / `{rejected}` results and MCP `isError` replies.
|
|
301
|
+
*/
|
|
302
|
+
export function toolResultError(result, callStatus) {
|
|
303
|
+
const r = result && typeof result === "object" ? result : null;
|
|
304
|
+
if (r) {
|
|
305
|
+
if (r.status === "error")
|
|
306
|
+
return errorText(r.error) || "Tool failed";
|
|
307
|
+
if (r.failure !== undefined)
|
|
308
|
+
return errorText(r.failure) || "Tool failed";
|
|
309
|
+
if (r.rejected !== undefined)
|
|
310
|
+
return errorText(r.rejected) || "Rejected";
|
|
311
|
+
if (r.error !== undefined && r.success === undefined)
|
|
312
|
+
return errorText(r.error) || "Tool failed";
|
|
313
|
+
const payload = unwrapToolResultPayload(r);
|
|
314
|
+
if (payload?.isError === true) {
|
|
315
|
+
return formatToolResultDetail("mcp", undefined, r, "Tool returned an error");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (callStatus === "error")
|
|
319
|
+
return errorText(r?.error) || "Tool failed";
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
/** Readable start-of-call detail for tools whose args aren't a path/command. */
|
|
323
|
+
export function structuredToolDetail(name, args) {
|
|
324
|
+
const tool = name.replace(/ToolCall$/i, "").toLowerCase();
|
|
325
|
+
if (tool === "mcp") {
|
|
326
|
+
// Card shows the call's own inputs (valid JSON → rendered as inputs).
|
|
327
|
+
const inner = coerceArgsObject(args.args) ?? {};
|
|
328
|
+
const payload = args.toolName === "call_tool" && inner.arguments && typeof inner.arguments === "object"
|
|
329
|
+
? inner.arguments
|
|
330
|
+
: inner;
|
|
331
|
+
try {
|
|
332
|
+
const json = JSON.stringify(payload);
|
|
333
|
+
return json.length <= 4000 ? json : `${json.slice(0, 4000)}…`;
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
return undefined;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (tool === "readlints" && Array.isArray(args.paths)) {
|
|
340
|
+
const paths = args.paths.filter((p) => typeof p === "string");
|
|
341
|
+
return paths.length ? `Lints · ${paths.join(", ")}`.slice(0, 160) : "Lints · workspace";
|
|
342
|
+
}
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
/** Max chars of tool result text we persist into chat content. */
|
|
346
|
+
export const TOOL_RESULT_DETAIL_LIMIT = 100_000;
|
|
347
|
+
/** `$ command` plus its full stdout/stderr for the tool-call card. */
|
|
348
|
+
export function formatToolTranscript(command, output) {
|
|
349
|
+
const cmd = (command || "").trim();
|
|
350
|
+
const out = (output || "").trim();
|
|
351
|
+
if (cmd && out && out !== cmd) {
|
|
352
|
+
if (out.startsWith(`$ ${cmd}`))
|
|
353
|
+
return out;
|
|
354
|
+
return `$ ${cmd}\n\n${out}`;
|
|
355
|
+
}
|
|
356
|
+
return out || cmd;
|
|
357
|
+
}
|
|
358
|
+
function commandFromArgs(args) {
|
|
359
|
+
if (!args)
|
|
360
|
+
return undefined;
|
|
361
|
+
if (typeof args.command === "string" && args.command.trim()) {
|
|
362
|
+
return args.command.trim();
|
|
363
|
+
}
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
function withCommandPrefix(args, body) {
|
|
367
|
+
return formatToolTranscript(commandFromArgs(args), body);
|
|
368
|
+
}
|
|
369
|
+
function finishResultDetail(args, body) {
|
|
370
|
+
return truncateDetail(withCommandPrefix(args, body));
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Keep the start-time command/args when a tool_done only has the result,
|
|
374
|
+
* so the chat card can show input and output together.
|
|
375
|
+
*/
|
|
376
|
+
export function mergeToolStartAndResult(name, start, result) {
|
|
377
|
+
const startText = (start || "").trim();
|
|
378
|
+
const resultText = (result || "").trim();
|
|
379
|
+
if (!resultText)
|
|
380
|
+
return truncateDetail(startText);
|
|
381
|
+
if (!startText || resultText === startText)
|
|
382
|
+
return truncateDetail(resultText);
|
|
383
|
+
if (resultText.startsWith("$ "))
|
|
384
|
+
return truncateDetail(resultText);
|
|
385
|
+
try {
|
|
386
|
+
const parsed = JSON.parse(resultText);
|
|
387
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
388
|
+
const rec = parsed;
|
|
389
|
+
if (rec.input != null ||
|
|
390
|
+
rec.command != null ||
|
|
391
|
+
rec.output != null ||
|
|
392
|
+
rec.stdout != null) {
|
|
393
|
+
return truncateDetail(resultText);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
/* result is not JSON */
|
|
399
|
+
}
|
|
400
|
+
if (/bash|shell|exec|terminal/i.test(name)) {
|
|
401
|
+
return truncateDetail(formatToolTranscript(startText, resultText));
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
const parsed = JSON.parse(startText);
|
|
405
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
406
|
+
return truncateDetail(JSON.stringify({ ...parsed, output: resultText }));
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
/* start is not JSON */
|
|
411
|
+
}
|
|
412
|
+
return truncateDetail(JSON.stringify({ input: startText, output: resultText }));
|
|
413
|
+
}
|
|
414
|
+
function toolInfo(toolCall) {
|
|
415
|
+
if (!toolCall)
|
|
416
|
+
return { name: "tool", detail: "" };
|
|
417
|
+
// Prefer explicit *ToolCall keys — Cursor sometimes also attaches metadata
|
|
418
|
+
// objects that would otherwise win Object.keys().find().
|
|
419
|
+
const toolCallKey = Object.keys(toolCall).find((k) => k.endsWith("ToolCall"));
|
|
420
|
+
const key = toolCallKey ||
|
|
421
|
+
Object.keys(toolCall).find((k) => ![
|
|
422
|
+
"hookAdditionalContexts",
|
|
423
|
+
"toolCallId",
|
|
424
|
+
"startedAtMs",
|
|
425
|
+
"completedAtMs",
|
|
426
|
+
"parentToolCallId",
|
|
427
|
+
"parent_tool_call_id",
|
|
428
|
+
].includes(k) && typeof toolCall[k] === "object");
|
|
429
|
+
const name = key ? key.replace(/ToolCall$/, "") : "tool";
|
|
430
|
+
const body = key ? toolCall[key] : undefined;
|
|
431
|
+
const rawArgs = body?.args ?? body?.input ?? body?.parameters;
|
|
432
|
+
const args = coerceArgsObject(rawArgs);
|
|
433
|
+
const result = body?.result;
|
|
434
|
+
let detail = "";
|
|
435
|
+
let path;
|
|
436
|
+
if (args) {
|
|
437
|
+
for (const k of [
|
|
438
|
+
"path",
|
|
439
|
+
"filePath",
|
|
440
|
+
"file_path",
|
|
441
|
+
"target_file",
|
|
442
|
+
"targetFile",
|
|
443
|
+
]) {
|
|
444
|
+
if (typeof args[k] === "string" && String(args[k]).trim()) {
|
|
445
|
+
path = String(args[k]).trim();
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
// editToolCall.result.success.path is often the authoritative absolute path
|
|
450
|
+
const resultPath = pathFromToolResult(result);
|
|
451
|
+
if (!path && resultPath)
|
|
452
|
+
path = resultPath;
|
|
453
|
+
const todos = todosFromToolArgs(args);
|
|
454
|
+
if (isTodoTool(name) || todos.length > 0) {
|
|
455
|
+
detail = todos.length
|
|
456
|
+
? `${todos.length} todo${todos.length === 1 ? "" : "s"} · ${todoStatusSummary(todos)}`
|
|
457
|
+
: "Updating todos";
|
|
458
|
+
}
|
|
459
|
+
else if (structuredToolDetail(name, args)) {
|
|
460
|
+
detail = structuredToolDetail(name, args);
|
|
461
|
+
}
|
|
462
|
+
else if (typeof args.description === "string" &&
|
|
463
|
+
String(args.description).trim()) {
|
|
464
|
+
detail = String(args.description).trim().slice(0, 160);
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
detail =
|
|
468
|
+
String(args.command ??
|
|
469
|
+
args.pattern ??
|
|
470
|
+
args.globPattern ??
|
|
471
|
+
args.glob_pattern ??
|
|
472
|
+
args.query ??
|
|
473
|
+
path ??
|
|
474
|
+
args.targetDirectory ??
|
|
475
|
+
args.prompt ??
|
|
476
|
+
"") || JSON.stringify(args).slice(0, 120);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
const resultPath = pathFromToolResult(result);
|
|
481
|
+
if (resultPath)
|
|
482
|
+
path = resultPath;
|
|
483
|
+
}
|
|
484
|
+
const parentCallId = typeof toolCall.parentToolCallId === "string"
|
|
485
|
+
? toolCall.parentToolCallId
|
|
486
|
+
: typeof toolCall.parent_tool_call_id === "string"
|
|
487
|
+
? toolCall.parent_tool_call_id
|
|
488
|
+
: undefined;
|
|
489
|
+
return { name, detail, path, args, result, parentCallId };
|
|
490
|
+
}
|
|
491
|
+
/** Unwrap CLI `{success:{…}}` / SDK `{status,value}` / bare object result shapes. */
|
|
492
|
+
export function unwrapToolResultPayload(result) {
|
|
493
|
+
if (!result || typeof result !== "object")
|
|
494
|
+
return null;
|
|
495
|
+
const r = result;
|
|
496
|
+
if (r.success && typeof r.success === "object") {
|
|
497
|
+
return r.success;
|
|
498
|
+
}
|
|
499
|
+
if (r.failure && typeof r.failure === "object") {
|
|
500
|
+
return r.failure;
|
|
501
|
+
}
|
|
502
|
+
if (r.error && typeof r.error === "object") {
|
|
503
|
+
return r.error;
|
|
504
|
+
}
|
|
505
|
+
if ((r.status === "success" || r.status === "error" || r.status === "failure") &&
|
|
506
|
+
r.value &&
|
|
507
|
+
typeof r.value === "object") {
|
|
508
|
+
return r.value;
|
|
509
|
+
}
|
|
510
|
+
return r;
|
|
511
|
+
}
|
|
512
|
+
function pathFromToolResult(result) {
|
|
513
|
+
const payload = unwrapToolResultPayload(result);
|
|
514
|
+
if (!payload)
|
|
515
|
+
return undefined;
|
|
516
|
+
for (const k of ["path", "filePath", "file_path", "target_file"]) {
|
|
517
|
+
if (typeof payload[k] === "string" && String(payload[k]).trim()) {
|
|
518
|
+
return String(payload[k]).trim();
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return undefined;
|
|
522
|
+
}
|
|
523
|
+
function truncateDetail(text, limit = TOOL_RESULT_DETAIL_LIMIT) {
|
|
524
|
+
if (text.length <= limit)
|
|
525
|
+
return text;
|
|
526
|
+
return `${text.slice(0, limit)}\n…(truncated)`;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Human-readable tool output for chat. Prefers completed `result` payloads
|
|
530
|
+
* (stdout, file content, diff summary) over the start-time args summary.
|
|
531
|
+
*/
|
|
532
|
+
export function formatToolResultDetail(name, args, result, fallback = "") {
|
|
533
|
+
const payload = unwrapToolResultPayload(result);
|
|
534
|
+
if (payload) {
|
|
535
|
+
if (typeof payload.stdout === "string" || typeof payload.stderr === "string") {
|
|
536
|
+
const stdout = typeof payload.stdout === "string" ? payload.stdout : "";
|
|
537
|
+
const stderr = typeof payload.stderr === "string" ? payload.stderr : "";
|
|
538
|
+
const exit = typeof payload.exitCode === "number"
|
|
539
|
+
? `exit ${payload.exitCode}`
|
|
540
|
+
: typeof payload.exit_code === "number"
|
|
541
|
+
? `exit ${payload.exit_code}`
|
|
542
|
+
: "";
|
|
543
|
+
const parts = [stdout, stderr].filter((s) => s.trim().length > 0);
|
|
544
|
+
const body = parts.join(parts.length === 2 ? "\n--- stderr ---\n" : "");
|
|
545
|
+
if (body.trim()) {
|
|
546
|
+
return finishResultDetail(args, exit ? `${exit}\n${body}` : body);
|
|
547
|
+
}
|
|
548
|
+
if (exit)
|
|
549
|
+
return finishResultDetail(args, exit);
|
|
550
|
+
}
|
|
551
|
+
if (typeof payload.content === "string" && payload.content.length > 0) {
|
|
552
|
+
return finishResultDetail(args, payload.content);
|
|
553
|
+
}
|
|
554
|
+
if (typeof payload.resultForModel === "string" && payload.resultForModel.trim()) {
|
|
555
|
+
const linesAdded = typeof payload.linesAdded === "number" ? payload.linesAdded : undefined;
|
|
556
|
+
const linesRemoved = typeof payload.linesRemoved === "number" ? payload.linesRemoved : undefined;
|
|
557
|
+
const stats = linesAdded != null || linesRemoved != null
|
|
558
|
+
? ` (+${linesAdded ?? 0}/-${linesRemoved ?? 0})`
|
|
559
|
+
: "";
|
|
560
|
+
return finishResultDetail(args, `${payload.resultForModel}${stats}`);
|
|
561
|
+
}
|
|
562
|
+
if (typeof payload.diffString === "string" && payload.diffString.trim()) {
|
|
563
|
+
const path = (typeof payload.path === "string" && payload.path) ||
|
|
564
|
+
(args && typeof args.path === "string" && args.path) ||
|
|
565
|
+
"file";
|
|
566
|
+
const linesAdded = typeof payload.linesAdded === "number" ? payload.linesAdded : undefined;
|
|
567
|
+
const linesRemoved = typeof payload.linesRemoved === "number" ? payload.linesRemoved : undefined;
|
|
568
|
+
if (linesAdded != null || linesRemoved != null) {
|
|
569
|
+
return `${path} (+${linesAdded ?? 0}/-${linesRemoved ?? 0})`;
|
|
570
|
+
}
|
|
571
|
+
return path;
|
|
572
|
+
}
|
|
573
|
+
if (typeof payload.errorMessage === "string" && payload.errorMessage.trim()) {
|
|
574
|
+
return finishResultDetail(args, payload.errorMessage);
|
|
575
|
+
}
|
|
576
|
+
if (Array.isArray(payload.content)) {
|
|
577
|
+
const text = payload.content
|
|
578
|
+
.map((item) => {
|
|
579
|
+
if (typeof item === "string")
|
|
580
|
+
return item;
|
|
581
|
+
if (!item || typeof item !== "object")
|
|
582
|
+
return "";
|
|
583
|
+
const o = item;
|
|
584
|
+
if (typeof o.text === "string")
|
|
585
|
+
return o.text;
|
|
586
|
+
if (o.text && typeof o.text === "object") {
|
|
587
|
+
const inner = o.text.text;
|
|
588
|
+
if (typeof inner === "string")
|
|
589
|
+
return inner;
|
|
590
|
+
}
|
|
591
|
+
if (typeof o.content === "string")
|
|
592
|
+
return o.content;
|
|
593
|
+
return "";
|
|
594
|
+
})
|
|
595
|
+
.filter(Boolean)
|
|
596
|
+
.join("\n");
|
|
597
|
+
if (text.trim())
|
|
598
|
+
return finishResultDetail(args, text);
|
|
599
|
+
}
|
|
600
|
+
if (Array.isArray(payload.entries)) {
|
|
601
|
+
return finishResultDetail(args, payload.entries
|
|
602
|
+
.map((e) => (typeof e === "string" ? e : JSON.stringify(e)))
|
|
603
|
+
.join("\n"));
|
|
604
|
+
}
|
|
605
|
+
if (Array.isArray(payload.files)) {
|
|
606
|
+
return finishResultDetail(args, payload.files
|
|
607
|
+
.map((e) => (typeof e === "string" ? e : JSON.stringify(e)))
|
|
608
|
+
.join("\n"));
|
|
609
|
+
}
|
|
610
|
+
if (Array.isArray(payload.matches)) {
|
|
611
|
+
try {
|
|
612
|
+
return finishResultDetail(args, JSON.stringify(payload.matches, null, 2));
|
|
613
|
+
}
|
|
614
|
+
catch {
|
|
615
|
+
/* fall through */
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (typeof payload.linesCreated === "number") {
|
|
619
|
+
const path = (typeof payload.path === "string" && payload.path) ||
|
|
620
|
+
(args && typeof args.path === "string" && args.path) ||
|
|
621
|
+
"file";
|
|
622
|
+
return `${path} · ${payload.linesCreated} lines`;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (typeof result === "string" && result.trim()) {
|
|
626
|
+
return finishResultDetail(args, result);
|
|
627
|
+
}
|
|
628
|
+
// Fall back to args summary (start-time detail) when result has no text.
|
|
629
|
+
if (args) {
|
|
630
|
+
const todos = todosFromToolArgs(args);
|
|
631
|
+
if (isTodoTool(name) || todos.length > 0) {
|
|
632
|
+
return todos.length
|
|
633
|
+
? `${todos.length} todo${todos.length === 1 ? "" : "s"} · ${todoStatusSummary(todos)}`
|
|
634
|
+
: "Updating todos";
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return fallback || "Done";
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Pull a unified diff out of a completed tool `result` payload.
|
|
641
|
+
* Cursor editToolCall puts the real patch in `result.success.diffString`.
|
|
642
|
+
*/
|
|
643
|
+
export function diffFromToolResult(result, pathHint) {
|
|
644
|
+
const payload = unwrapToolResultPayload(result);
|
|
645
|
+
if (!payload)
|
|
646
|
+
return "";
|
|
647
|
+
const ready = (typeof payload.diffString === "string" && payload.diffString) ||
|
|
648
|
+
(typeof payload.diff === "string" && payload.diff) ||
|
|
649
|
+
(typeof payload.unifiedDiff === "string" && payload.unifiedDiff) ||
|
|
650
|
+
(typeof payload.unified_diff === "string" && payload.unified_diff) ||
|
|
651
|
+
(typeof payload.patch === "string" && payload.patch) ||
|
|
652
|
+
"";
|
|
653
|
+
if (ready && /diff --git|@@ /.test(ready)) {
|
|
654
|
+
return ready.trim();
|
|
655
|
+
}
|
|
656
|
+
// editToolCall often emits a short hunk without git headers — wrap it.
|
|
657
|
+
if (typeof payload.diffString === "string" && payload.diffString.trim()) {
|
|
658
|
+
const path = (typeof payload.path === "string" && payload.path) ||
|
|
659
|
+
pathHint ||
|
|
660
|
+
"file";
|
|
661
|
+
const body = payload.diffString.trim();
|
|
662
|
+
if (body.startsWith("diff --git") || body.includes("\n@@ ") || body.startsWith("@@ ")) {
|
|
663
|
+
if (body.startsWith("diff --git"))
|
|
664
|
+
return body;
|
|
665
|
+
return [
|
|
666
|
+
`diff --git a/${path} b/${path}`,
|
|
667
|
+
`--- a/${path}`,
|
|
668
|
+
`+++ b/${path}`,
|
|
669
|
+
body.startsWith("@@") ? body : `@@ -1,1 +1,1 @@\n${body}`,
|
|
670
|
+
].join("\n");
|
|
671
|
+
}
|
|
672
|
+
// Line-oriented +/- dump without hunk header
|
|
673
|
+
if (/^[+-]/m.test(body) || body.includes("\n+") || body.includes("\n-")) {
|
|
674
|
+
const lines = body.split("\n");
|
|
675
|
+
return [
|
|
676
|
+
`diff --git a/${path} b/${path}`,
|
|
677
|
+
`--- a/${path}`,
|
|
678
|
+
`+++ b/${path}`,
|
|
679
|
+
`@@ -1,${lines.filter((l) => l.startsWith("-")).length || 0} +1,${lines.filter((l) => l.startsWith("+")).length || 0} @@`,
|
|
680
|
+
body,
|
|
681
|
+
].join("\n");
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
const after = (typeof payload.afterFullFileContent === "string" &&
|
|
685
|
+
payload.afterFullFileContent) ||
|
|
686
|
+
(typeof payload.afterContent === "string" && payload.afterContent) ||
|
|
687
|
+
undefined;
|
|
688
|
+
if (typeof after === "string") {
|
|
689
|
+
const path = (typeof payload.path === "string" && payload.path) || pathHint || "file";
|
|
690
|
+
return buildUnifiedDiff(path, "", after);
|
|
691
|
+
}
|
|
692
|
+
return "";
|
|
693
|
+
}
|
|
694
|
+
function buildUnifiedDiff(filePath, before, after) {
|
|
695
|
+
const path = filePath.replace(/\\/g, "/").replace(/^\.\//, "") || "file";
|
|
696
|
+
if (before === after)
|
|
697
|
+
return "";
|
|
698
|
+
const oldLines = before.length
|
|
699
|
+
? before.replace(/\n$/, "").split("\n")
|
|
700
|
+
: [];
|
|
701
|
+
const newLines = after.length ? after.replace(/\n$/, "").split("\n") : [];
|
|
702
|
+
const deleted = after.length === 0 && before.length > 0;
|
|
703
|
+
const created = before.length === 0 && after.length > 0;
|
|
704
|
+
const hunk = [
|
|
705
|
+
`@@ -${created ? 0 : 1},${oldLines.length} +${deleted ? 0 : 1},${newLines.length} @@`,
|
|
706
|
+
];
|
|
707
|
+
for (const line of oldLines)
|
|
708
|
+
hunk.push(`-${line}`);
|
|
709
|
+
for (const line of newLines)
|
|
710
|
+
hunk.push(`+${line}`);
|
|
711
|
+
return [
|
|
712
|
+
`diff --git a/${path} b/${path}`,
|
|
713
|
+
deleted ? `deleted file mode 100644` : "",
|
|
714
|
+
created ? `new file mode 100644` : "",
|
|
715
|
+
created ? `--- /dev/null` : `--- a/${path}`,
|
|
716
|
+
deleted ? `+++ /dev/null` : `+++ b/${path}`,
|
|
717
|
+
...hunk,
|
|
718
|
+
]
|
|
719
|
+
.filter((l) => l !== "")
|
|
720
|
+
.join("\n");
|
|
721
|
+
}
|
|
722
|
+
export function diffFromToolArgs(toolName, args) {
|
|
723
|
+
if (!args)
|
|
724
|
+
return "";
|
|
725
|
+
const path = (typeof args.path === "string" && args.path) ||
|
|
726
|
+
(typeof args.file_path === "string" && args.file_path) ||
|
|
727
|
+
(typeof args.target_file === "string" && args.target_file) ||
|
|
728
|
+
(typeof args.filePath === "string" && args.filePath) ||
|
|
729
|
+
"file";
|
|
730
|
+
const name = toolName.replace(/ToolCall$/i, "").toLowerCase();
|
|
731
|
+
// Flat StrReplace / Edit args (Claude + older Cursor)
|
|
732
|
+
const oldStr = (typeof args.old_string === "string" && args.old_string) ||
|
|
733
|
+
(typeof args.oldString === "string" && args.oldString) ||
|
|
734
|
+
(typeof args.old_str === "string" && args.old_str) ||
|
|
735
|
+
(typeof args.OldString === "string" && args.OldString) ||
|
|
736
|
+
undefined;
|
|
737
|
+
const newStr = (typeof args.new_string === "string" && args.new_string) ||
|
|
738
|
+
(typeof args.newString === "string" && args.newString) ||
|
|
739
|
+
(typeof args.new_str === "string" && args.new_str) ||
|
|
740
|
+
(typeof args.NewString === "string" && args.NewString) ||
|
|
741
|
+
undefined;
|
|
742
|
+
if (typeof oldStr === "string" && typeof newStr === "string") {
|
|
743
|
+
return buildUnifiedDiff(path, oldStr, newStr);
|
|
744
|
+
}
|
|
745
|
+
// Cursor editToolCall nested strategies
|
|
746
|
+
const strReplace = args.strReplace && typeof args.strReplace === "object"
|
|
747
|
+
? args.strReplace
|
|
748
|
+
: null;
|
|
749
|
+
if (strReplace) {
|
|
750
|
+
const oldText = (typeof strReplace.oldText === "string" && strReplace.oldText) ||
|
|
751
|
+
(typeof strReplace.old_string === "string" && strReplace.old_string) ||
|
|
752
|
+
undefined;
|
|
753
|
+
const newText = (typeof strReplace.newText === "string" && strReplace.newText) ||
|
|
754
|
+
(typeof strReplace.new_string === "string" && strReplace.new_string) ||
|
|
755
|
+
undefined;
|
|
756
|
+
if (typeof oldText === "string" && typeof newText === "string") {
|
|
757
|
+
return buildUnifiedDiff(path, oldText, newText);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
const multi = args.multiStrReplace && typeof args.multiStrReplace === "object"
|
|
761
|
+
? args.multiStrReplace
|
|
762
|
+
: null;
|
|
763
|
+
if (multi && Array.isArray(multi.edits)) {
|
|
764
|
+
const parts = [];
|
|
765
|
+
for (const edit of multi.edits) {
|
|
766
|
+
if (!edit || typeof edit !== "object")
|
|
767
|
+
continue;
|
|
768
|
+
const e = edit;
|
|
769
|
+
const o = (typeof e.oldText === "string" && e.oldText) ||
|
|
770
|
+
(typeof e.old_string === "string" && e.old_string) ||
|
|
771
|
+
undefined;
|
|
772
|
+
const n = (typeof e.newText === "string" && e.newText) ||
|
|
773
|
+
(typeof e.new_string === "string" && e.new_string) ||
|
|
774
|
+
undefined;
|
|
775
|
+
if (typeof o === "string" && typeof n === "string") {
|
|
776
|
+
const hunk = buildUnifiedDiff(path, o, n);
|
|
777
|
+
if (hunk)
|
|
778
|
+
parts.push(hunk);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
if (parts.length)
|
|
782
|
+
return parts.join("\n");
|
|
783
|
+
}
|
|
784
|
+
const applyPatch = args.applyPatch && typeof args.applyPatch === "object"
|
|
785
|
+
? args.applyPatch
|
|
786
|
+
: null;
|
|
787
|
+
if (applyPatch) {
|
|
788
|
+
const patchContent = (typeof applyPatch.patchContent === "string" && applyPatch.patchContent) ||
|
|
789
|
+
(typeof applyPatch.patch === "string" && applyPatch.patch) ||
|
|
790
|
+
"";
|
|
791
|
+
if (patchContent && /diff --git|@@ /.test(patchContent)) {
|
|
792
|
+
return patchContent.trim();
|
|
793
|
+
}
|
|
794
|
+
if (patchContent.trim()) {
|
|
795
|
+
return [
|
|
796
|
+
`diff --git a/${path} b/${path}`,
|
|
797
|
+
`--- a/${path}`,
|
|
798
|
+
`+++ b/${path}`,
|
|
799
|
+
patchContent.trim().startsWith("@@")
|
|
800
|
+
? patchContent.trim()
|
|
801
|
+
: `@@\n${patchContent.trim()}`,
|
|
802
|
+
].join("\n");
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
// writeToolCall uses fileText (CLI) or contents/content
|
|
806
|
+
const contents = (typeof args.fileText === "string" && args.fileText) ||
|
|
807
|
+
(typeof args.file_text === "string" && args.file_text) ||
|
|
808
|
+
(typeof args.contents === "string" && args.contents) ||
|
|
809
|
+
(typeof args.content === "string" && args.content) ||
|
|
810
|
+
(typeof args.new_contents === "string" && args.new_contents) ||
|
|
811
|
+
(typeof args.newContents === "string" && args.newContents) ||
|
|
812
|
+
(typeof args.text === "string" && args.text) ||
|
|
813
|
+
undefined;
|
|
814
|
+
if (typeof contents === "string" &&
|
|
815
|
+
/^(write|create|updatefile|writefile|editnotebook)/i.test(name)) {
|
|
816
|
+
return buildUnifiedDiff(path, "", contents);
|
|
817
|
+
}
|
|
818
|
+
// Bare edit with only full contents (some agents)
|
|
819
|
+
if (typeof contents === "string" &&
|
|
820
|
+
name === "edit" &&
|
|
821
|
+
!strReplace &&
|
|
822
|
+
!multi &&
|
|
823
|
+
!applyPatch) {
|
|
824
|
+
return buildUnifiedDiff(path, "", contents);
|
|
825
|
+
}
|
|
826
|
+
if (typeof args.patch === "string" && /diff --git|@@ /.test(args.patch)) {
|
|
827
|
+
return args.patch.trim();
|
|
828
|
+
}
|
|
829
|
+
if (typeof args.unifiedDiff === "string" &&
|
|
830
|
+
/diff --git|@@ /.test(args.unifiedDiff)) {
|
|
831
|
+
return args.unifiedDiff.trim();
|
|
832
|
+
}
|
|
833
|
+
if (typeof args.unified_diff === "string" &&
|
|
834
|
+
/diff --git|@@ /.test(args.unified_diff)) {
|
|
835
|
+
return args.unified_diff.trim();
|
|
836
|
+
}
|
|
837
|
+
if (/^delete/i.test(name)) {
|
|
838
|
+
return buildUnifiedDiff(path, `/* deleted: ${path} */\n`, "");
|
|
839
|
+
}
|
|
840
|
+
return "";
|
|
841
|
+
}
|
|
842
|
+
/** Prefer result.diffString, then synthesize from args. */
|
|
843
|
+
export function diffFromToolEvent(toolName, args, result) {
|
|
844
|
+
const fromResult = diffFromToolResult(result, args && typeof args.path === "string" ? args.path : undefined);
|
|
845
|
+
if (fromResult)
|
|
846
|
+
return fromResult;
|
|
847
|
+
return diffFromToolArgs(toolName, args);
|
|
848
|
+
}
|
|
849
|
+
const EDIT_TOOL_RE = /^(write|edit|strreplace|searchreplace|delete|applypatch|editnotebook|create|updatefile|deletefile|writefile)/i;
|
|
850
|
+
const TODO_TOOL_RE = /^(todo|todowrite|todoread|updatetodos|write_todos|todo_write|todo_read)$/i;
|
|
851
|
+
export function isEditTool(name) {
|
|
852
|
+
return EDIT_TOOL_RE.test(name.replace(/ToolCall$/i, ""));
|
|
853
|
+
}
|
|
854
|
+
export function isTodoTool(name) {
|
|
855
|
+
const n = name.replace(/ToolCall$/i, "");
|
|
856
|
+
return TODO_TOOL_RE.test(n) || /todo/i.test(n);
|
|
857
|
+
}
|
|
858
|
+
function normalizeTodoStatus(raw) {
|
|
859
|
+
let s = String(raw ?? "pending").toLowerCase().replace(/[\s-]/g, "_");
|
|
860
|
+
// Cursor CLI enums: TODO_STATUS_COMPLETED, TODO_STATUS_IN_PROGRESS, …
|
|
861
|
+
if (s.startsWith("todo_status_"))
|
|
862
|
+
s = s.slice("todo_status_".length);
|
|
863
|
+
if (s === "completed" || s === "complete" || s === "done")
|
|
864
|
+
return "completed";
|
|
865
|
+
if (s === "in_progress" || s === "inprogress" || s === "active")
|
|
866
|
+
return "in_progress";
|
|
867
|
+
if (s === "cancelled" || s === "canceled")
|
|
868
|
+
return "cancelled";
|
|
869
|
+
if (s === "pending" || s === "todo" || s === "not_started")
|
|
870
|
+
return "pending";
|
|
871
|
+
return "pending";
|
|
872
|
+
}
|
|
873
|
+
function coerceArgsObject(args) {
|
|
874
|
+
if (!args)
|
|
875
|
+
return undefined;
|
|
876
|
+
if (typeof args === "string") {
|
|
877
|
+
try {
|
|
878
|
+
const parsed = JSON.parse(args);
|
|
879
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
880
|
+
return parsed;
|
|
881
|
+
}
|
|
882
|
+
if (Array.isArray(parsed))
|
|
883
|
+
return { todos: parsed };
|
|
884
|
+
}
|
|
885
|
+
catch {
|
|
886
|
+
return undefined;
|
|
887
|
+
}
|
|
888
|
+
return undefined;
|
|
889
|
+
}
|
|
890
|
+
if (typeof args === "object" && !Array.isArray(args)) {
|
|
891
|
+
return args;
|
|
892
|
+
}
|
|
893
|
+
if (Array.isArray(args))
|
|
894
|
+
return { todos: args };
|
|
895
|
+
return undefined;
|
|
896
|
+
}
|
|
897
|
+
export function todosFromToolArgs(args) {
|
|
898
|
+
const o = coerceArgsObject(args);
|
|
899
|
+
if (!o)
|
|
900
|
+
return [];
|
|
901
|
+
const raw = (Array.isArray(o.todos) && o.todos) ||
|
|
902
|
+
(Array.isArray(o.items) && o.items) ||
|
|
903
|
+
(Array.isArray(o.todo_list) && o.todo_list) ||
|
|
904
|
+
null;
|
|
905
|
+
if (!raw)
|
|
906
|
+
return [];
|
|
907
|
+
const out = [];
|
|
908
|
+
for (let i = 0; i < raw.length; i++) {
|
|
909
|
+
const item = raw[i];
|
|
910
|
+
if (!item || typeof item !== "object")
|
|
911
|
+
continue;
|
|
912
|
+
const t = item;
|
|
913
|
+
const content = String(t.content ?? t.text ?? t.title ?? t.description ?? "").trim();
|
|
914
|
+
if (!content)
|
|
915
|
+
continue;
|
|
916
|
+
out.push({
|
|
917
|
+
id: String(t.id ?? `todo-${i + 1}`),
|
|
918
|
+
content,
|
|
919
|
+
status: normalizeTodoStatus(t.status),
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
return out;
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Recover structured todos from a chat message — prefers `message.todos`,
|
|
926
|
+
* then parses JSON tool args that older servers stored as `content`.
|
|
927
|
+
*/
|
|
928
|
+
export function resolveMessageTodos(message) {
|
|
929
|
+
if (message.todos && message.todos.length > 0)
|
|
930
|
+
return message.todos;
|
|
931
|
+
const content = message.content?.trim() ?? "";
|
|
932
|
+
if (!content)
|
|
933
|
+
return [];
|
|
934
|
+
if (content.startsWith("{") ||
|
|
935
|
+
content.startsWith("[") ||
|
|
936
|
+
content.includes('"todos"')) {
|
|
937
|
+
const parsed = todosFromToolArgs(content);
|
|
938
|
+
if (parsed.length)
|
|
939
|
+
return parsed;
|
|
940
|
+
// Truncated JSON from older servers (JSON.stringify(args).slice(0, N)).
|
|
941
|
+
return todosFromTruncatedContent(content);
|
|
942
|
+
}
|
|
943
|
+
return [];
|
|
944
|
+
}
|
|
945
|
+
/** Whether a chat tool message should render as the Todos card. */
|
|
946
|
+
export function messageHasTodos(message) {
|
|
947
|
+
if (message.todos && message.todos.length > 0)
|
|
948
|
+
return true;
|
|
949
|
+
if (message.toolName && isTodoTool(message.toolName))
|
|
950
|
+
return true;
|
|
951
|
+
const content = message.content?.trim() ?? "";
|
|
952
|
+
if (!content)
|
|
953
|
+
return false;
|
|
954
|
+
return ((content.startsWith("{") || content.startsWith("[")) &&
|
|
955
|
+
/"todos"\s*:/.test(content));
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Keep only the latest todos card per agent so successive TodoWrite updates
|
|
959
|
+
* (2 → 3 → 4 items) don't stack as separate groups in the timeline.
|
|
960
|
+
*/
|
|
961
|
+
export function coalesceTodoMessages(messages) {
|
|
962
|
+
const latestIdByAgent = new Map();
|
|
963
|
+
for (const msg of messages) {
|
|
964
|
+
if (msg.role !== "tool" || !messageHasTodos(msg))
|
|
965
|
+
continue;
|
|
966
|
+
latestIdByAgent.set(msg.agentId || "__default__", msg.id);
|
|
967
|
+
}
|
|
968
|
+
if (latestIdByAgent.size === 0)
|
|
969
|
+
return messages;
|
|
970
|
+
return messages.filter((msg) => {
|
|
971
|
+
if (msg.role !== "tool" || !messageHasTodos(msg))
|
|
972
|
+
return true;
|
|
973
|
+
return latestIdByAgent.get(msg.agentId || "__default__") === msg.id;
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
/** Best-effort extraction when content is a sliced JSON blob. */
|
|
977
|
+
function todosFromTruncatedContent(content) {
|
|
978
|
+
if (!/"content"\s*:/.test(content))
|
|
979
|
+
return [];
|
|
980
|
+
const out = [];
|
|
981
|
+
const re = /\{\s*"id"\s*:\s*"((?:\\.|[^"\\])*)"\s*,\s*"content"\s*:\s*"((?:\\.|[^"\\])*)"\s*,\s*"status"\s*:\s*"((?:\\.|[^"\\])*)"/g;
|
|
982
|
+
let m;
|
|
983
|
+
let i = 0;
|
|
984
|
+
while ((m = re.exec(content)) !== null) {
|
|
985
|
+
i += 1;
|
|
986
|
+
const todoContent = m[2].replace(/\\"/g, '"').replace(/\\\\/g, "\\").trim();
|
|
987
|
+
if (!todoContent)
|
|
988
|
+
continue;
|
|
989
|
+
out.push({
|
|
990
|
+
id: m[1] || `todo-${i}`,
|
|
991
|
+
content: todoContent,
|
|
992
|
+
status: normalizeTodoStatus(m[3]),
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
if (out.length)
|
|
996
|
+
return out;
|
|
997
|
+
// Looser: pull content fields even if id/status order differs or was cut off.
|
|
998
|
+
const contentRe = /"content"\s*:\s*"((?:\\.|[^"\\])*)"/g;
|
|
999
|
+
while ((m = contentRe.exec(content)) !== null) {
|
|
1000
|
+
i += 1;
|
|
1001
|
+
const todoContent = m[1].replace(/\\"/g, '"').replace(/\\\\/g, "\\").trim();
|
|
1002
|
+
if (!todoContent)
|
|
1003
|
+
continue;
|
|
1004
|
+
out.push({
|
|
1005
|
+
id: `todo-${i}`,
|
|
1006
|
+
content: todoContent,
|
|
1007
|
+
status: "pending",
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
return out;
|
|
1011
|
+
}
|
|
1012
|
+
export function todoStatusSummary(todos) {
|
|
1013
|
+
const completed = todos.filter((t) => t.status === "completed").length;
|
|
1014
|
+
const active = todos.filter((t) => t.status === "in_progress").length;
|
|
1015
|
+
const pending = todos.filter((t) => t.status === "pending").length;
|
|
1016
|
+
const parts = [];
|
|
1017
|
+
if (active)
|
|
1018
|
+
parts.push(`${active} in progress`);
|
|
1019
|
+
if (completed)
|
|
1020
|
+
parts.push(`${completed} done`);
|
|
1021
|
+
if (pending)
|
|
1022
|
+
parts.push(`${pending} pending`);
|
|
1023
|
+
return parts.join(" · ") || "updated";
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Cursor CLI headless agent backend.
|
|
1027
|
+
* Spawns: `cursor agent --print --output-format stream-json ...`
|
|
1028
|
+
*/
|
|
1029
|
+
export class CursorAgentBackend {
|
|
1030
|
+
kind = "cursor";
|
|
1031
|
+
available = true;
|
|
1032
|
+
command = "cursor";
|
|
1033
|
+
buildArgs(opts) {
|
|
1034
|
+
const args = [
|
|
1035
|
+
"agent",
|
|
1036
|
+
"--print",
|
|
1037
|
+
"--output-format",
|
|
1038
|
+
"stream-json",
|
|
1039
|
+
"--stream-partial-output",
|
|
1040
|
+
"--force",
|
|
1041
|
+
"--trust",
|
|
1042
|
+
];
|
|
1043
|
+
if (opts.mode === "plan") {
|
|
1044
|
+
args.push("--mode", "plan");
|
|
1045
|
+
}
|
|
1046
|
+
if (opts.modelId && opts.modelId !== "auto") {
|
|
1047
|
+
args.push("--model", opts.modelId);
|
|
1048
|
+
}
|
|
1049
|
+
if (opts.sessionId) {
|
|
1050
|
+
args.push("--resume", opts.sessionId);
|
|
1051
|
+
}
|
|
1052
|
+
if (opts.mcpServers && Object.keys(opts.mcpServers).length) {
|
|
1053
|
+
// Cursor CLI reads MCP servers from `.cursor/mcp.json`, not a flag.
|
|
1054
|
+
// `--approve-mcps` skips the headless approval prompt for those servers.
|
|
1055
|
+
args.push("--approve-mcps");
|
|
1056
|
+
}
|
|
1057
|
+
args.push(opts.prompt);
|
|
1058
|
+
return args;
|
|
1059
|
+
}
|
|
1060
|
+
parseLine(json, ctx = {
|
|
1061
|
+
assistantBuf: { value: "" },
|
|
1062
|
+
gotTerminalEvent: { value: false },
|
|
1063
|
+
}) {
|
|
1064
|
+
if (!json || typeof json !== "object")
|
|
1065
|
+
return [];
|
|
1066
|
+
const ev = json;
|
|
1067
|
+
const type = ev.type;
|
|
1068
|
+
const out = [];
|
|
1069
|
+
if (type === "system" && ev.subtype === "init" && ev.session_id) {
|
|
1070
|
+
out.push({ kind: "session", sessionId: String(ev.session_id) });
|
|
1071
|
+
return out;
|
|
1072
|
+
}
|
|
1073
|
+
if (type === "thinking" ||
|
|
1074
|
+
type === "reasoning" ||
|
|
1075
|
+
type === "thinkingMessage" ||
|
|
1076
|
+
type === "reasoningMessage") {
|
|
1077
|
+
if (ctx.toolsOnly)
|
|
1078
|
+
return out;
|
|
1079
|
+
const text = extractThinkingText(ev) ||
|
|
1080
|
+
(typeof ev.text === "string" ? ev.text : "") ||
|
|
1081
|
+
(typeof ev.thinking === "string" ? ev.thinking : "");
|
|
1082
|
+
if (!text)
|
|
1083
|
+
return out;
|
|
1084
|
+
ctx.thinkingBuf ??= { value: "" };
|
|
1085
|
+
const step = advanceThinking(ctx.thinkingBuf.value, text);
|
|
1086
|
+
if (step.finalize) {
|
|
1087
|
+
out.push({ kind: "thinking_final", text: step.finalize });
|
|
1088
|
+
}
|
|
1089
|
+
if (step.current === ctx.thinkingBuf.value && !step.finalize)
|
|
1090
|
+
return out;
|
|
1091
|
+
ctx.thinkingBuf.value = step.current;
|
|
1092
|
+
const done = ev.subtype === "completed";
|
|
1093
|
+
out.push({
|
|
1094
|
+
kind: done ? "thinking_final" : "thinking_delta",
|
|
1095
|
+
text: step.current,
|
|
1096
|
+
});
|
|
1097
|
+
if (done)
|
|
1098
|
+
ctx.thinkingBuf.value = "";
|
|
1099
|
+
return out;
|
|
1100
|
+
}
|
|
1101
|
+
if (type === "assistant") {
|
|
1102
|
+
if (!ctx.toolsOnly) {
|
|
1103
|
+
const thinking = extractThinkingText(ev.message ?? ev);
|
|
1104
|
+
if (thinking) {
|
|
1105
|
+
ctx.thinkingBuf ??= { value: "" };
|
|
1106
|
+
const step = advanceThinking(ctx.thinkingBuf.value, thinking);
|
|
1107
|
+
if (step.finalize) {
|
|
1108
|
+
out.push({ kind: "thinking_final", text: step.finalize });
|
|
1109
|
+
}
|
|
1110
|
+
if (step.current !== ctx.thinkingBuf.value || step.finalize) {
|
|
1111
|
+
ctx.thinkingBuf.value = step.current;
|
|
1112
|
+
const done = !("timestamp_ms" in ev);
|
|
1113
|
+
out.push({
|
|
1114
|
+
kind: done ? "thinking_final" : "thinking_delta",
|
|
1115
|
+
text: step.current,
|
|
1116
|
+
});
|
|
1117
|
+
if (done)
|
|
1118
|
+
ctx.thinkingBuf.value = "";
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
const text = extractText(ev.message);
|
|
1123
|
+
if (!text)
|
|
1124
|
+
return out;
|
|
1125
|
+
const merged = mergeAssistantText(ctx.assistantBuf.value, text);
|
|
1126
|
+
ctx.assistantBuf.value = merged;
|
|
1127
|
+
if ("timestamp_ms" in ev) {
|
|
1128
|
+
out.push({
|
|
1129
|
+
kind: "assistant_delta",
|
|
1130
|
+
text: merged,
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
else {
|
|
1134
|
+
out.push({ kind: "assistant_final", text: merged });
|
|
1135
|
+
}
|
|
1136
|
+
return out;
|
|
1137
|
+
}
|
|
1138
|
+
if (type === "tool_call") {
|
|
1139
|
+
const callId = String(ev.call_id ?? "");
|
|
1140
|
+
const info = toolInfo(ev.tool_call);
|
|
1141
|
+
const todos = info.args ? todosFromToolArgs(info.args) : [];
|
|
1142
|
+
const todoTool = isTodoTool(info.name) || todos.length > 0;
|
|
1143
|
+
const questions = info.args ? parseQuestionToolArgs(info.args) : [];
|
|
1144
|
+
const questionTool = isQuestionTool(info.name) || questions.length > 0;
|
|
1145
|
+
const toolName = todoTool && !isTodoTool(info.name)
|
|
1146
|
+
? "todo"
|
|
1147
|
+
: questionTool && !isQuestionTool(info.name)
|
|
1148
|
+
? "AskUserQuestion"
|
|
1149
|
+
: /^task$/i.test(info.name)
|
|
1150
|
+
? "Task"
|
|
1151
|
+
: info.name;
|
|
1152
|
+
// Nested Task/subagent tools (parentToolCallId) still become first-class
|
|
1153
|
+
// chat cards so cloud/local Cursor subagent work is visible.
|
|
1154
|
+
if (ev.subtype === "started") {
|
|
1155
|
+
ctx.thinkingBuf = { value: "" };
|
|
1156
|
+
out.push({
|
|
1157
|
+
kind: "tool_start",
|
|
1158
|
+
callId,
|
|
1159
|
+
name: toolName,
|
|
1160
|
+
detail: info.detail || toolName,
|
|
1161
|
+
path: info.path,
|
|
1162
|
+
todos: todos.length ? todos : undefined,
|
|
1163
|
+
questions: questions.length ? questions : undefined,
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
else if (ev.subtype === "completed") {
|
|
1167
|
+
const path = info.path || pathFromToolResult(info.result);
|
|
1168
|
+
const failure = toolResultError(info.result);
|
|
1169
|
+
const detail = failure ??
|
|
1170
|
+
formatToolResultDetail(info.name, info.args, info.result, info.detail || path || "Done");
|
|
1171
|
+
const fromResult = !todoTool
|
|
1172
|
+
? diffFromToolResult(info.result, path)
|
|
1173
|
+
: "";
|
|
1174
|
+
const diffPatch = fromResult ||
|
|
1175
|
+
(!todoTool && isEditTool(info.name)
|
|
1176
|
+
? diffFromToolArgs(info.name, info.args)
|
|
1177
|
+
: "") ||
|
|
1178
|
+
undefined;
|
|
1179
|
+
out.push({
|
|
1180
|
+
kind: "tool_done",
|
|
1181
|
+
callId,
|
|
1182
|
+
name: toolName,
|
|
1183
|
+
detail,
|
|
1184
|
+
path,
|
|
1185
|
+
...(failure ? { isError: true } : {}),
|
|
1186
|
+
diffPatch: failure ? undefined : diffPatch || undefined,
|
|
1187
|
+
todos: todos.length ? todos : undefined,
|
|
1188
|
+
questions: questions.length ? questions : undefined,
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
return out;
|
|
1192
|
+
}
|
|
1193
|
+
if (type === "result") {
|
|
1194
|
+
ctx.gotTerminalEvent.value = true;
|
|
1195
|
+
if (ev.session_id) {
|
|
1196
|
+
out.push({ kind: "session", sessionId: String(ev.session_id) });
|
|
1197
|
+
}
|
|
1198
|
+
if (ev.is_error) {
|
|
1199
|
+
const msg = stringifyUnknown(ev.result) ||
|
|
1200
|
+
stringifyUnknown(ev.errors) ||
|
|
1201
|
+
stringifyUnknown(ev.error) ||
|
|
1202
|
+
ctx.stderr ||
|
|
1203
|
+
"Agent error";
|
|
1204
|
+
out.push({ kind: "error", message: msg });
|
|
1205
|
+
}
|
|
1206
|
+
else {
|
|
1207
|
+
out.push({
|
|
1208
|
+
kind: "done",
|
|
1209
|
+
result: stringifyUnknown(ev.result) || ctx.assistantBuf.value || "",
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
return out;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
export const cursorAgentBackend = new CursorAgentBackend();
|