@iinm/plain-agent 1.8.4 → 1.8.6
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/bin/plain +1 -1
- package/package.json +8 -9
- package/sandbox/bin/plain-sandbox +13 -0
- package/src/agent.d.ts +52 -0
- package/src/agent.mjs +204 -0
- package/src/agentLoop.mjs +419 -0
- package/src/agentState.mjs +41 -0
- package/src/claudeCodePlugin.mjs +164 -0
- package/src/cliArgs.mjs +175 -0
- package/src/cliBatch.mjs +147 -0
- package/src/cliCommands.mjs +283 -0
- package/src/cliCompleter.mjs +227 -0
- package/src/cliCost.mjs +309 -0
- package/src/cliFormatter.mjs +518 -0
- package/src/cliInteractive.mjs +533 -0
- package/src/cliInterruptTransform.mjs +51 -0
- package/src/cliMuteTransform.mjs +26 -0
- package/src/cliPasteTransform.mjs +183 -0
- package/src/config.d.ts +36 -0
- package/src/config.mjs +197 -0
- package/src/context/loadAgentRoles.mjs +267 -0
- package/src/context/loadPrompts.mjs +303 -0
- package/src/context/loadUserMessageContext.mjs +147 -0
- package/src/costTracker.mjs +210 -0
- package/src/env.mjs +44 -0
- package/src/main.mjs +281 -0
- package/src/mcpClient.mjs +351 -0
- package/src/mcpIntegration.mjs +160 -0
- package/src/model.d.ts +109 -0
- package/src/modelCaller.mjs +32 -0
- package/src/modelDefinition.d.ts +92 -0
- package/src/prompt.mjs +138 -0
- package/src/providers/anthropic.d.ts +248 -0
- package/src/providers/anthropic.mjs +587 -0
- package/src/providers/bedrock.d.ts +249 -0
- package/src/providers/bedrock.mjs +700 -0
- package/src/providers/gemini.d.ts +208 -0
- package/src/providers/gemini.mjs +754 -0
- package/src/providers/openai.d.ts +281 -0
- package/src/providers/openai.mjs +544 -0
- package/src/providers/openaiCompatible.d.ts +147 -0
- package/src/providers/openaiCompatible.mjs +652 -0
- package/src/providers/platform/awsSigV4.mjs +184 -0
- package/src/providers/platform/azure.mjs +42 -0
- package/src/providers/platform/bedrock.mjs +78 -0
- package/src/providers/platform/googleCloud.mjs +34 -0
- package/src/subagent.mjs +265 -0
- package/src/tmpfile.mjs +27 -0
- package/src/tool.d.ts +74 -0
- package/src/toolExecutor.mjs +236 -0
- package/src/toolInputValidator.mjs +183 -0
- package/src/toolUseApprover.mjs +99 -0
- package/src/tools/askURL.mjs +209 -0
- package/src/tools/askWeb.mjs +208 -0
- package/src/tools/compactContext.d.ts +4 -0
- package/src/tools/compactContext.mjs +87 -0
- package/src/tools/execCommand.d.ts +22 -0
- package/src/tools/execCommand.mjs +200 -0
- package/src/tools/patchFile.d.ts +4 -0
- package/src/tools/patchFile.mjs +133 -0
- package/src/tools/switchToMainAgent.d.ts +3 -0
- package/src/tools/switchToMainAgent.mjs +43 -0
- package/src/tools/switchToSubagent.d.ts +4 -0
- package/src/tools/switchToSubagent.mjs +59 -0
- package/src/tools/tmuxCommand.d.ts +14 -0
- package/src/tools/tmuxCommand.mjs +194 -0
- package/src/tools/writeFile.d.ts +4 -0
- package/src/tools/writeFile.mjs +56 -0
- package/src/usageStore.mjs +167 -0
- package/src/utils/evalJSONConfig.mjs +72 -0
- package/src/utils/matchValue.d.ts +6 -0
- package/src/utils/matchValue.mjs +40 -0
- package/src/utils/noThrow.mjs +31 -0
- package/src/utils/notify.mjs +29 -0
- package/src/utils/parseFileRange.mjs +18 -0
- package/src/utils/parseFrontmatter.mjs +19 -0
- package/src/utils/readFileRange.mjs +33 -0
- package/src/utils/retryOnError.mjs +41 -0
- package/src/voiceInput.mjs +61 -0
- package/src/voiceInputGemini.mjs +105 -0
- package/src/voiceInputOpenAI.mjs +104 -0
- package/src/voiceInputSession.mjs +543 -0
- package/src/voiceToggleKey.mjs +62 -0
- package/dist/main.mjs +0 -473
- package/dist/main.mjs.map +0 -7
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { Message, MessageContentToolUse, MessageContentToolResult, ProviderTokenUsage } from "./model"
|
|
3
|
+
* @import { CompactContextInput } from "./tools/compactContext"
|
|
4
|
+
* @import { ExecCommandInput } from "./tools/execCommand"
|
|
5
|
+
* @import { PatchFileInput } from "./tools/patchFile"
|
|
6
|
+
* @import { WriteFileInput } from "./tools/writeFile"
|
|
7
|
+
* @import { TmuxCommandInput } from "./tools/tmuxCommand"
|
|
8
|
+
* @import { SwitchToSubagentInput } from "./tools/switchToSubagent"
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { execFile } from "node:child_process";
|
|
12
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { styleText } from "node:util";
|
|
16
|
+
import { noThrow } from "./utils/noThrow.mjs";
|
|
17
|
+
|
|
18
|
+
/** Length above which a single-line arg forces block-form rendering. */
|
|
19
|
+
const ARG_BLOCK_LENGTH_THRESHOLD = 60;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Format an args array for display.
|
|
23
|
+
* Uses compact JSON for short single-line args; switches to a YAML-style
|
|
24
|
+
* block form when any arg contains newlines or exceeds
|
|
25
|
+
* {@link ARG_BLOCK_LENGTH_THRESHOLD} characters so that long scripts passed
|
|
26
|
+
* to `bash -c`, `python -c`, `node -e`, etc. stay readable.
|
|
27
|
+
* @param {unknown} args
|
|
28
|
+
* @returns {string}
|
|
29
|
+
*/
|
|
30
|
+
export function formatArgs(args) {
|
|
31
|
+
if (!Array.isArray(args) || args.length === 0) {
|
|
32
|
+
return `args: ${JSON.stringify(args ?? [])}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const needsBlock = args.some(
|
|
36
|
+
(a) =>
|
|
37
|
+
typeof a === "string" &&
|
|
38
|
+
(a.includes("\n") || a.length > ARG_BLOCK_LENGTH_THRESHOLD),
|
|
39
|
+
);
|
|
40
|
+
if (!needsBlock) {
|
|
41
|
+
return `args: ${JSON.stringify(args)}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const lines = ["args:"];
|
|
45
|
+
for (const arg of args) {
|
|
46
|
+
if (
|
|
47
|
+
typeof arg === "string" &&
|
|
48
|
+
(arg.includes("\n") || arg.length > ARG_BLOCK_LENGTH_THRESHOLD)
|
|
49
|
+
) {
|
|
50
|
+
lines.push(" - |");
|
|
51
|
+
for (const line of arg.split("\n")) {
|
|
52
|
+
lines.push(` ${line}`);
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
lines.push(` - ${JSON.stringify(arg)}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return lines.join("\n");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Format tool use for display.
|
|
63
|
+
* @param {MessageContentToolUse} toolUse
|
|
64
|
+
* @param {{ createDiff?: (oldContent: string, newContent: string) => Promise<string | null> }} [options]
|
|
65
|
+
* @returns {Promise<string>}
|
|
66
|
+
*/
|
|
67
|
+
export async function formatToolUse(toolUse, options = {}) {
|
|
68
|
+
const { createDiff = tryGitDiff } = options;
|
|
69
|
+
const { toolName, input } = toolUse;
|
|
70
|
+
|
|
71
|
+
if (toolName === "exec_command") {
|
|
72
|
+
/** @type {Partial<ExecCommandInput>} */
|
|
73
|
+
const execCommandInput = input;
|
|
74
|
+
return [
|
|
75
|
+
`tool: ${toolName}`,
|
|
76
|
+
`command: ${JSON.stringify(execCommandInput.command)}`,
|
|
77
|
+
formatArgs(execCommandInput.args),
|
|
78
|
+
].join("\n");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (toolName === "write_file") {
|
|
82
|
+
/** @type {Partial<WriteFileInput>} */
|
|
83
|
+
const writeFileInput = input;
|
|
84
|
+
return [
|
|
85
|
+
`tool: ${toolName}`,
|
|
86
|
+
`filePath: ${writeFileInput.filePath}`,
|
|
87
|
+
`content:\n${writeFileInput.content}`,
|
|
88
|
+
].join("\n");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (toolName === "patch_file") {
|
|
92
|
+
/** @type {Partial<PatchFileInput>} */
|
|
93
|
+
const patchFileInput = input;
|
|
94
|
+
const diff = patchFileInput.diff || "";
|
|
95
|
+
|
|
96
|
+
/** @type {{search:string; replace:string}[]} */
|
|
97
|
+
const diffs = [];
|
|
98
|
+
const matches = Array.from(
|
|
99
|
+
diff.matchAll(
|
|
100
|
+
/<<< [0-9a-z]{3} <<< SEARCH\n(.*?)\n=== [0-9a-z]{3} ===\n(.*?)\n?>>> [0-9a-z]{3} >>> REPLACE/gs,
|
|
101
|
+
),
|
|
102
|
+
);
|
|
103
|
+
for (const match of matches) {
|
|
104
|
+
const [_, search, replace] = match;
|
|
105
|
+
diffs.push({ search, replace });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const highlightedDiff = await Promise.all(
|
|
109
|
+
diffs.map(async ({ search, replace }) => {
|
|
110
|
+
const gitDiffOutput = await createDiff(search, replace);
|
|
111
|
+
if (gitDiffOutput) {
|
|
112
|
+
return `${gitDiffOutput}\n-------\n${replace}`;
|
|
113
|
+
}
|
|
114
|
+
return [
|
|
115
|
+
`${styleText("yellow", "(git diff unavailable, showing plain diff)")}`,
|
|
116
|
+
"--- old",
|
|
117
|
+
`${search}`,
|
|
118
|
+
"+++ new",
|
|
119
|
+
`${replace}`,
|
|
120
|
+
].join("\n");
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
return [
|
|
125
|
+
`tool: ${toolName}`,
|
|
126
|
+
`path: ${patchFileInput.filePath}`,
|
|
127
|
+
`diff:\n${highlightedDiff.join("\n\n")}`,
|
|
128
|
+
].join("\n");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (toolName === "tmux_command") {
|
|
132
|
+
/** @type {Partial<TmuxCommandInput>} */
|
|
133
|
+
const tmuxCommandInput = input;
|
|
134
|
+
return [
|
|
135
|
+
`tool: ${toolName}`,
|
|
136
|
+
`command: ${tmuxCommandInput.command}`,
|
|
137
|
+
formatArgs(tmuxCommandInput.args),
|
|
138
|
+
].join("\n");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (toolName === "switch_to_subagent") {
|
|
142
|
+
/** @type {Partial<SwitchToSubagentInput>} */
|
|
143
|
+
const switchToSubagentInput = input;
|
|
144
|
+
return [
|
|
145
|
+
`tool: ${toolName}`,
|
|
146
|
+
`name: ${switchToSubagentInput.name}`,
|
|
147
|
+
`goal: ${switchToSubagentInput.goal}`,
|
|
148
|
+
].join("\n");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (toolName === "compact_context") {
|
|
152
|
+
/** @type {Partial<CompactContextInput>} */
|
|
153
|
+
const compactContextInput = input;
|
|
154
|
+
return [
|
|
155
|
+
`tool: ${toolName}`,
|
|
156
|
+
`memoryPath: ${compactContextInput.memoryPath}`,
|
|
157
|
+
`reason: ${compactContextInput.reason}`,
|
|
158
|
+
].join("\n");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (toolName === "switch_to_main_agent") {
|
|
162
|
+
/** @type {Partial<import("./tools/switchToMainAgent").SwitchToMainAgentInput>} */
|
|
163
|
+
const switchToMainAgentInput = input;
|
|
164
|
+
return [
|
|
165
|
+
`tool: ${toolName}`,
|
|
166
|
+
`memoryPath: ${switchToMainAgentInput.memoryPath}`,
|
|
167
|
+
].join("\n");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (toolName === "ask_web") {
|
|
171
|
+
/** @type {Partial<import("./tools/askWeb.mjs").AskWebInput>} */
|
|
172
|
+
const askWebInput = input;
|
|
173
|
+
return [`tool: ${toolName}`, `question: ${askWebInput.question}`].join(
|
|
174
|
+
"\n",
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (toolName === "ask_url") {
|
|
179
|
+
/** @type {Partial<import("./tools/askURL.mjs").AskURLInput>} */
|
|
180
|
+
const askURLInput = input;
|
|
181
|
+
return [`tool: ${toolName}`, `question: ${askURLInput.question}`].join(
|
|
182
|
+
"\n",
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const { provider: _, ...filteredToolUse } = toolUse;
|
|
187
|
+
|
|
188
|
+
return JSON.stringify(filteredToolUse, null, 2);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Maximum length of output to display */
|
|
192
|
+
const MAX_DISPLAY_OUTPUT_LENGTH = 1024;
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Format tool result for display.
|
|
196
|
+
* @param {MessageContentToolResult} toolResult
|
|
197
|
+
* @returns {string}
|
|
198
|
+
*/
|
|
199
|
+
export function formatToolResult(toolResult) {
|
|
200
|
+
const { content, isError } = toolResult;
|
|
201
|
+
|
|
202
|
+
/** @type {string[]} */
|
|
203
|
+
const contentStringParts = [];
|
|
204
|
+
for (const part of content) {
|
|
205
|
+
switch (part.type) {
|
|
206
|
+
case "text":
|
|
207
|
+
contentStringParts.push(part.text);
|
|
208
|
+
break;
|
|
209
|
+
case "image":
|
|
210
|
+
contentStringParts.push(
|
|
211
|
+
`data:${part.mimeType};base64,${part.data.slice(0, 20)}...`,
|
|
212
|
+
);
|
|
213
|
+
break;
|
|
214
|
+
default:
|
|
215
|
+
console.log(`Unsupported content part: ${JSON.stringify(part)}`);
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const contentString = contentStringParts.join("\n\n");
|
|
221
|
+
|
|
222
|
+
if (isError) {
|
|
223
|
+
return styleText("red", contentString);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (toolResult.toolName === "exec_command") {
|
|
227
|
+
return contentString
|
|
228
|
+
.replace(/(^<stdout>|<\/stdout>$)/gm, styleText("blue", "$1"))
|
|
229
|
+
.replace(
|
|
230
|
+
/(<truncated_output.+?>|<\/truncated_output>)/g,
|
|
231
|
+
styleText("yellow", "$1"),
|
|
232
|
+
)
|
|
233
|
+
.replace(/(^<stderr>|<\/stderr>$)/gm, styleText("magenta", "$1"))
|
|
234
|
+
.replace(/(^<error>|<\/error>$)/gm, styleText("red", "$1"));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (toolResult.toolName === "tmux_command") {
|
|
238
|
+
return contentString
|
|
239
|
+
.replace(/(^<stdout>|<\/stdout>$)/gm, styleText("blue", "$1"))
|
|
240
|
+
.replace(/(^<stderr>|<\/stderr>$)/gm, styleText("magenta", "$1"))
|
|
241
|
+
.replace(/(^<error>|<\/error>$)/gm, styleText("red", "$1"))
|
|
242
|
+
.replace(/(^<tmux:.*?>|<\/tmux:.*?>$)/gm, styleText("green", "$1"));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (contentString.length > MAX_DISPLAY_OUTPUT_LENGTH) {
|
|
246
|
+
return [
|
|
247
|
+
contentString.slice(0, MAX_DISPLAY_OUTPUT_LENGTH),
|
|
248
|
+
styleText("yellow", "... (Output truncated for display)"),
|
|
249
|
+
"\n",
|
|
250
|
+
].join("");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return contentString;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Format provider token usage for display.
|
|
258
|
+
* @param {ProviderTokenUsage} usage
|
|
259
|
+
* @returns {string}
|
|
260
|
+
*/
|
|
261
|
+
export function formatProviderTokenUsage(usage) {
|
|
262
|
+
/** @type {string[]} */
|
|
263
|
+
const lines = [];
|
|
264
|
+
/** @type {string[]} */
|
|
265
|
+
const header = [];
|
|
266
|
+
for (const [key, value] of Object.entries(usage)) {
|
|
267
|
+
if (typeof value === "number") {
|
|
268
|
+
header.push(`${key}: ${value}`);
|
|
269
|
+
} else if (typeof value === "string") {
|
|
270
|
+
header.push(`${key}: ${value}`);
|
|
271
|
+
} else if (value) {
|
|
272
|
+
lines.push(
|
|
273
|
+
`(${key}) ${Object.entries(value)
|
|
274
|
+
.filter(
|
|
275
|
+
([k]) =>
|
|
276
|
+
![
|
|
277
|
+
// OpenAI
|
|
278
|
+
"audio_tokens",
|
|
279
|
+
"accepted_prediction_tokens",
|
|
280
|
+
"rejected_prediction_tokens",
|
|
281
|
+
].includes(k),
|
|
282
|
+
)
|
|
283
|
+
.map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
|
|
284
|
+
.join(", ")}`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const outputLines = [`\n${header.join(", ")}`];
|
|
290
|
+
|
|
291
|
+
if (lines.length) {
|
|
292
|
+
outputLines.push(lines.join(" / "));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return styleText("gray", outputLines.join("\n"));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Format cost summary for interactive display
|
|
300
|
+
* @param {import("./costTracker.mjs").CostSummary} summary
|
|
301
|
+
* @returns {string}
|
|
302
|
+
*/
|
|
303
|
+
export function formatCostSummary(summary) {
|
|
304
|
+
if (!summary || Object.keys(summary.breakdown).length === 0) {
|
|
305
|
+
return styleText("gray", "No token usage recorded yet.");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const lines = [];
|
|
309
|
+
|
|
310
|
+
if (summary.totalCost !== undefined) {
|
|
311
|
+
lines.push(
|
|
312
|
+
styleText(
|
|
313
|
+
"bold",
|
|
314
|
+
`\nTotal: ${summary.totalCost.toFixed(4)} ${summary.currency}`,
|
|
315
|
+
),
|
|
316
|
+
);
|
|
317
|
+
} else {
|
|
318
|
+
lines.push(styleText("yellow", "Total: N/A (no cost configuration)"));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
lines.push(styleText("bold", "\nTokens:"));
|
|
322
|
+
for (const [key, { tokens, cost }] of Object.entries(summary.breakdown)) {
|
|
323
|
+
const tokenStr = `${key}: ${tokens.toLocaleString()}`;
|
|
324
|
+
|
|
325
|
+
if (cost !== undefined) {
|
|
326
|
+
const costStr = `${cost.toFixed(4)} ${summary.currency}`;
|
|
327
|
+
lines.push(` ${tokenStr.padEnd(30)} ${styleText("cyan", costStr)}`);
|
|
328
|
+
} else {
|
|
329
|
+
lines.push(` ${tokenStr.padEnd(30)} ${styleText("gray", "N/A")}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return lines.join("\n");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Format cost for batch mode JSON output
|
|
338
|
+
* @param {import("./costTracker.mjs").CostSummary} summary
|
|
339
|
+
*/
|
|
340
|
+
export function formatCostForBatch(summary) {
|
|
341
|
+
if (!summary || Object.keys(summary.breakdown).length === 0) {
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
total: summary.totalCost,
|
|
347
|
+
currency: summary.currency,
|
|
348
|
+
unit: summary.unit,
|
|
349
|
+
breakdown: Object.fromEntries(
|
|
350
|
+
Object.entries(summary.breakdown).map(([key, { tokens, cost }]) => [
|
|
351
|
+
key,
|
|
352
|
+
{ tokens, cost },
|
|
353
|
+
]),
|
|
354
|
+
),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Print a message to the console.
|
|
360
|
+
* @param {Message} message
|
|
361
|
+
* @returns {Promise<void>}
|
|
362
|
+
*/
|
|
363
|
+
export async function printMessage(message) {
|
|
364
|
+
switch (message.role) {
|
|
365
|
+
case "assistant": {
|
|
366
|
+
// console.log(styleText("bold", "\nAgent:"));
|
|
367
|
+
// Pre-format all tool_use parts in parallel to avoid sequential awaits
|
|
368
|
+
const toolUseParts = message.content.filter(
|
|
369
|
+
(part) => part.type === "tool_use",
|
|
370
|
+
);
|
|
371
|
+
const formattedToolUses = await Promise.all(
|
|
372
|
+
toolUseParts.map((part) => formatToolUse(part)),
|
|
373
|
+
);
|
|
374
|
+
let toolUseIndex = 0;
|
|
375
|
+
for (const part of message.content) {
|
|
376
|
+
switch (part.type) {
|
|
377
|
+
// Note: Streamで表示するためここでは表示しない
|
|
378
|
+
// case "thinking":
|
|
379
|
+
// console.log(
|
|
380
|
+
// [
|
|
381
|
+
// styleText("blue", "<thinking>"),
|
|
382
|
+
// part.thinking,
|
|
383
|
+
// styleText("blue", "</thinking>\n"),
|
|
384
|
+
// ].join("\n"),
|
|
385
|
+
// );
|
|
386
|
+
// break;
|
|
387
|
+
// case "text":
|
|
388
|
+
// console.log(part.text);
|
|
389
|
+
// break;
|
|
390
|
+
case "tool_use":
|
|
391
|
+
console.log(styleText("bold", "\nTool call:"));
|
|
392
|
+
console.log(formattedToolUses[toolUseIndex++]);
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
case "user": {
|
|
399
|
+
for (const part of message.content) {
|
|
400
|
+
switch (part.type) {
|
|
401
|
+
case "tool_result": {
|
|
402
|
+
console.log(styleText("bold", "\nTool result:"));
|
|
403
|
+
console.log(formatToolResult(part));
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
case "text": {
|
|
407
|
+
console.log(styleText("bold", "\nUser:"));
|
|
408
|
+
const highlighted = part.text.replace(
|
|
409
|
+
/^(<context.+?>|<\/context>)/gm,
|
|
410
|
+
styleText("green", "$1"),
|
|
411
|
+
);
|
|
412
|
+
console.log(highlighted);
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
case "image": {
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
default: {
|
|
419
|
+
console.log(styleText("bold", "\nUnknown Message Format:"));
|
|
420
|
+
console.log(JSON.stringify(part, null, 2));
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
default: {
|
|
427
|
+
console.log(styleText("bold", "\nUnknown Message Format:"));
|
|
428
|
+
console.log(JSON.stringify(message, null, 2));
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Generate a colored unified diff using `git diff --color`.
|
|
435
|
+
* Falls back to `null` if git is unavailable or if any step fails
|
|
436
|
+
* (temp directory creation, file writing, git execution, or cleanup).
|
|
437
|
+
* @param {string} oldContent
|
|
438
|
+
* @param {string} newContent
|
|
439
|
+
* @returns {Promise<string | null>}
|
|
440
|
+
*/
|
|
441
|
+
async function tryGitDiff(oldContent, newContent) {
|
|
442
|
+
const tmpDir = await noThrow(() =>
|
|
443
|
+
mkdtemp(path.join(os.tmpdir(), "git-diff-")),
|
|
444
|
+
);
|
|
445
|
+
if (tmpDir instanceof Error) {
|
|
446
|
+
console.error(
|
|
447
|
+
styleText("yellow", `git diff: mkdtemp failed: ${tmpDir.message}`),
|
|
448
|
+
);
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const oldPath = path.join(tmpDir, "old");
|
|
453
|
+
const newPath = path.join(tmpDir, "new");
|
|
454
|
+
|
|
455
|
+
try {
|
|
456
|
+
const w1 = await noThrow(() => writeFile(oldPath, oldContent, "utf8"));
|
|
457
|
+
if (w1 instanceof Error) {
|
|
458
|
+
console.error(
|
|
459
|
+
styleText("yellow", `git diff: writeFile(old) failed: ${w1.message}`),
|
|
460
|
+
);
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const w2 = await noThrow(() => writeFile(newPath, newContent, "utf8"));
|
|
465
|
+
if (w2 instanceof Error) {
|
|
466
|
+
console.error(
|
|
467
|
+
styleText("yellow", `git diff: writeFile(new) failed: ${w2.message}`),
|
|
468
|
+
);
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const diffResult = await noThrow(() => execGitDiff(oldPath, newPath));
|
|
473
|
+
if (diffResult instanceof Error) {
|
|
474
|
+
console.error(
|
|
475
|
+
styleText("yellow", `git diff: exec failed: ${diffResult.message}`),
|
|
476
|
+
);
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return diffResult;
|
|
481
|
+
} finally {
|
|
482
|
+
const cleanup = await noThrow(() =>
|
|
483
|
+
rm(tmpDir, { recursive: true, force: true }),
|
|
484
|
+
);
|
|
485
|
+
if (cleanup instanceof Error) {
|
|
486
|
+
console.error(
|
|
487
|
+
styleText("yellow", `git diff: cleanup failed: ${cleanup.message}`),
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Execute git diff accepting exit code 1 as success (differences found).
|
|
495
|
+
* @param {string} oldPath
|
|
496
|
+
* @param {string} newPath
|
|
497
|
+
* @returns {Promise<string>}
|
|
498
|
+
*/
|
|
499
|
+
function execGitDiff(oldPath, newPath) {
|
|
500
|
+
return new Promise((resolve, reject) => {
|
|
501
|
+
execFile(
|
|
502
|
+
"git",
|
|
503
|
+
["--no-pager", "diff", "--color", "--no-index", "--", oldPath, newPath],
|
|
504
|
+
{ encoding: "utf8", maxBuffer: 10 * 1024 * 1024 },
|
|
505
|
+
(error, stdout, stderr) => {
|
|
506
|
+
if (stderr) {
|
|
507
|
+
console.error(styleText("yellow", `git diff stderr: ${stderr}`));
|
|
508
|
+
}
|
|
509
|
+
// git diff returns exit code 1 when there are differences, which is expected
|
|
510
|
+
if (error && error.code !== 1) {
|
|
511
|
+
reject(error);
|
|
512
|
+
} else {
|
|
513
|
+
resolve(stdout);
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
);
|
|
517
|
+
});
|
|
518
|
+
}
|