@iinm/plain-agent 1.8.4 → 1.8.5

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.
Files changed (84) hide show
  1. package/bin/plain +1 -1
  2. package/package.json +7 -5
  3. package/sandbox/bin/plain-sandbox +13 -0
  4. package/src/agent.d.ts +52 -0
  5. package/src/agent.mjs +204 -0
  6. package/src/agentLoop.mjs +419 -0
  7. package/src/agentState.mjs +41 -0
  8. package/src/claudeCodePlugin.mjs +164 -0
  9. package/src/cliArgs.mjs +175 -0
  10. package/src/cliBatch.mjs +147 -0
  11. package/src/cliCommands.mjs +283 -0
  12. package/src/cliCompleter.mjs +227 -0
  13. package/src/cliCost.mjs +309 -0
  14. package/src/cliFormatter.mjs +413 -0
  15. package/src/cliInteractive.mjs +529 -0
  16. package/src/cliInterruptTransform.mjs +51 -0
  17. package/src/cliMuteTransform.mjs +26 -0
  18. package/src/cliPasteTransform.mjs +183 -0
  19. package/src/config.d.ts +36 -0
  20. package/src/config.mjs +197 -0
  21. package/src/context/loadAgentRoles.mjs +294 -0
  22. package/src/context/loadPrompts.mjs +337 -0
  23. package/src/context/loadUserMessageContext.mjs +147 -0
  24. package/src/costTracker.mjs +210 -0
  25. package/src/env.mjs +44 -0
  26. package/src/main.mjs +281 -0
  27. package/src/mcpClient.mjs +351 -0
  28. package/src/mcpIntegration.mjs +160 -0
  29. package/src/model.d.ts +109 -0
  30. package/src/modelCaller.mjs +32 -0
  31. package/src/modelDefinition.d.ts +92 -0
  32. package/src/prompt.mjs +138 -0
  33. package/src/providers/anthropic.d.ts +248 -0
  34. package/src/providers/anthropic.mjs +587 -0
  35. package/src/providers/bedrock.d.ts +249 -0
  36. package/src/providers/bedrock.mjs +700 -0
  37. package/src/providers/gemini.d.ts +208 -0
  38. package/src/providers/gemini.mjs +754 -0
  39. package/src/providers/openai.d.ts +281 -0
  40. package/src/providers/openai.mjs +544 -0
  41. package/src/providers/openaiCompatible.d.ts +147 -0
  42. package/src/providers/openaiCompatible.mjs +652 -0
  43. package/src/providers/platform/awsSigV4.mjs +184 -0
  44. package/src/providers/platform/azure.mjs +42 -0
  45. package/src/providers/platform/bedrock.mjs +78 -0
  46. package/src/providers/platform/googleCloud.mjs +34 -0
  47. package/src/subagent.mjs +265 -0
  48. package/src/tmpfile.mjs +27 -0
  49. package/src/tool.d.ts +74 -0
  50. package/src/toolExecutor.mjs +236 -0
  51. package/src/toolInputValidator.mjs +183 -0
  52. package/src/toolUseApprover.mjs +99 -0
  53. package/src/tools/askURL.mjs +209 -0
  54. package/src/tools/askWeb.mjs +208 -0
  55. package/src/tools/compactContext.d.ts +4 -0
  56. package/src/tools/compactContext.mjs +87 -0
  57. package/src/tools/execCommand.d.ts +22 -0
  58. package/src/tools/execCommand.mjs +200 -0
  59. package/src/tools/patchFile.d.ts +4 -0
  60. package/src/tools/patchFile.mjs +133 -0
  61. package/src/tools/switchToMainAgent.d.ts +3 -0
  62. package/src/tools/switchToMainAgent.mjs +43 -0
  63. package/src/tools/switchToSubagent.d.ts +4 -0
  64. package/src/tools/switchToSubagent.mjs +59 -0
  65. package/src/tools/tmuxCommand.d.ts +14 -0
  66. package/src/tools/tmuxCommand.mjs +194 -0
  67. package/src/tools/writeFile.d.ts +4 -0
  68. package/src/tools/writeFile.mjs +56 -0
  69. package/src/usageStore.mjs +167 -0
  70. package/src/utils/evalJSONConfig.mjs +72 -0
  71. package/src/utils/matchValue.d.ts +6 -0
  72. package/src/utils/matchValue.mjs +40 -0
  73. package/src/utils/noThrow.mjs +31 -0
  74. package/src/utils/notify.mjs +29 -0
  75. package/src/utils/parseFileRange.mjs +18 -0
  76. package/src/utils/readFileRange.mjs +33 -0
  77. package/src/utils/retryOnError.mjs +41 -0
  78. package/src/voiceInput.mjs +61 -0
  79. package/src/voiceInputGemini.mjs +105 -0
  80. package/src/voiceInputOpenAI.mjs +104 -0
  81. package/src/voiceInputSession.mjs +543 -0
  82. package/src/voiceToggleKey.mjs +62 -0
  83. package/dist/main.mjs +0 -473
  84. package/dist/main.mjs.map +0 -7
@@ -0,0 +1,413 @@
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 { styleText } from "node:util";
12
+ import { createPatch } from "diff";
13
+
14
+ /** Length above which a single-line arg forces block-form rendering. */
15
+ const ARG_BLOCK_LENGTH_THRESHOLD = 60;
16
+
17
+ /**
18
+ * Format an args array for display.
19
+ * Uses compact JSON for short single-line args; switches to a YAML-style
20
+ * block form when any arg contains newlines or exceeds
21
+ * {@link ARG_BLOCK_LENGTH_THRESHOLD} characters so that long scripts passed
22
+ * to `bash -c`, `python -c`, `node -e`, etc. stay readable.
23
+ * @param {unknown} args
24
+ * @returns {string}
25
+ */
26
+ export function formatArgs(args) {
27
+ if (!Array.isArray(args) || args.length === 0) {
28
+ return `args: ${JSON.stringify(args ?? [])}`;
29
+ }
30
+
31
+ const needsBlock = args.some(
32
+ (a) =>
33
+ typeof a === "string" &&
34
+ (a.includes("\n") || a.length > ARG_BLOCK_LENGTH_THRESHOLD),
35
+ );
36
+ if (!needsBlock) {
37
+ return `args: ${JSON.stringify(args)}`;
38
+ }
39
+
40
+ const lines = ["args:"];
41
+ for (const arg of args) {
42
+ if (
43
+ typeof arg === "string" &&
44
+ (arg.includes("\n") || arg.length > ARG_BLOCK_LENGTH_THRESHOLD)
45
+ ) {
46
+ lines.push(" - |");
47
+ for (const line of arg.split("\n")) {
48
+ lines.push(` ${line}`);
49
+ }
50
+ } else {
51
+ lines.push(` - ${JSON.stringify(arg)}`);
52
+ }
53
+ }
54
+ return lines.join("\n");
55
+ }
56
+
57
+ /**
58
+ * Format tool use for display.
59
+ * @param {MessageContentToolUse} toolUse
60
+ * @returns {string}
61
+ */
62
+ export function formatToolUse(toolUse) {
63
+ const { toolName, input } = toolUse;
64
+
65
+ if (toolName === "exec_command") {
66
+ /** @type {Partial<ExecCommandInput>} */
67
+ const execCommandInput = input;
68
+ return [
69
+ `tool: ${toolName}`,
70
+ `command: ${JSON.stringify(execCommandInput.command)}`,
71
+ formatArgs(execCommandInput.args),
72
+ ].join("\n");
73
+ }
74
+
75
+ if (toolName === "write_file") {
76
+ /** @type {Partial<WriteFileInput>} */
77
+ const writeFileInput = input;
78
+ return [
79
+ `tool: ${toolName}`,
80
+ `filePath: ${writeFileInput.filePath}`,
81
+ `content:\n${writeFileInput.content}`,
82
+ ].join("\n");
83
+ }
84
+
85
+ if (toolName === "patch_file") {
86
+ /** @type {Partial<PatchFileInput>} */
87
+ const patchFileInput = input;
88
+ const diff = patchFileInput.diff || "";
89
+
90
+ /** @type {{search:string; replace:string}[]} */
91
+ const diffs = [];
92
+ const matches = Array.from(
93
+ diff.matchAll(
94
+ /<<< [0-9a-z]{3} <<< SEARCH\n(.*?)\n=== [0-9a-z]{3} ===\n(.*?)\n?>>> [0-9a-z]{3} >>> REPLACE/gs,
95
+ ),
96
+ );
97
+ for (const match of matches) {
98
+ const [_, search, replace] = match;
99
+ diffs.push({ search, replace });
100
+ }
101
+
102
+ const highlightedDiff = diffs
103
+ .map(
104
+ ({ search, replace }) =>
105
+ `${createPatch(patchFileInput.filePath || "", search, replace)
106
+ .replace(/^-.+$/gm, (match) => styleText("red", match))
107
+ .replace(/^\+.+$/gm, (match) => styleText("green", match))
108
+ .replace(/^@@.+$/gm, (match) => styleText("gray", match))
109
+ .replace(/^\$/gm, (match) =>
110
+ styleText("gray", match),
111
+ )}\n-------\n${replace}`,
112
+ )
113
+ .join("\n\n");
114
+
115
+ return [
116
+ `tool: ${toolName}`,
117
+ `path: ${patchFileInput.filePath}`,
118
+ `diff:\n${highlightedDiff}`,
119
+ ].join("\n");
120
+ }
121
+
122
+ if (toolName === "tmux_command") {
123
+ /** @type {Partial<TmuxCommandInput>} */
124
+ const tmuxCommandInput = input;
125
+ return [
126
+ `tool: ${toolName}`,
127
+ `command: ${tmuxCommandInput.command}`,
128
+ formatArgs(tmuxCommandInput.args),
129
+ ].join("\n");
130
+ }
131
+
132
+ if (toolName === "switch_to_subagent") {
133
+ /** @type {Partial<SwitchToSubagentInput>} */
134
+ const switchToSubagentInput = input;
135
+ return [
136
+ `tool: ${toolName}`,
137
+ `name: ${switchToSubagentInput.name}`,
138
+ `goal: ${switchToSubagentInput.goal}`,
139
+ ].join("\n");
140
+ }
141
+
142
+ if (toolName === "compact_context") {
143
+ /** @type {Partial<CompactContextInput>} */
144
+ const compactContextInput = input;
145
+ return [
146
+ `tool: ${toolName}`,
147
+ `memoryPath: ${compactContextInput.memoryPath}`,
148
+ `reason: ${compactContextInput.reason}`,
149
+ ].join("\n");
150
+ }
151
+
152
+ if (toolName === "switch_to_main_agent") {
153
+ /** @type {Partial<import("./tools/switchToMainAgent").SwitchToMainAgentInput>} */
154
+ const switchToMainAgentInput = input;
155
+ return [
156
+ `tool: ${toolName}`,
157
+ `memoryPath: ${switchToMainAgentInput.memoryPath}`,
158
+ ].join("\n");
159
+ }
160
+
161
+ if (toolName === "ask_web") {
162
+ /** @type {Partial<import("./tools/askWeb.mjs").AskWebInput>} */
163
+ const askWebInput = input;
164
+ return [`tool: ${toolName}`, `question: ${askWebInput.question}`].join(
165
+ "\n",
166
+ );
167
+ }
168
+
169
+ if (toolName === "ask_url") {
170
+ /** @type {Partial<import("./tools/askURL.mjs").AskURLInput>} */
171
+ const askURLInput = input;
172
+ return [`tool: ${toolName}`, `question: ${askURLInput.question}`].join(
173
+ "\n",
174
+ );
175
+ }
176
+
177
+ const { provider: _, ...filteredToolUse } = toolUse;
178
+
179
+ return JSON.stringify(filteredToolUse, null, 2);
180
+ }
181
+
182
+ /** Maximum length of output to display */
183
+ const MAX_DISPLAY_OUTPUT_LENGTH = 1024;
184
+
185
+ /**
186
+ * Format tool result for display.
187
+ * @param {MessageContentToolResult} toolResult
188
+ * @returns {string}
189
+ */
190
+ export function formatToolResult(toolResult) {
191
+ const { content, isError } = toolResult;
192
+
193
+ /** @type {string[]} */
194
+ const contentStringParts = [];
195
+ for (const part of content) {
196
+ switch (part.type) {
197
+ case "text":
198
+ contentStringParts.push(part.text);
199
+ break;
200
+ case "image":
201
+ contentStringParts.push(
202
+ `data:${part.mimeType};base64,${part.data.slice(0, 20)}...`,
203
+ );
204
+ break;
205
+ default:
206
+ console.log(`Unsupported content part: ${JSON.stringify(part)}`);
207
+ break;
208
+ }
209
+ }
210
+
211
+ const contentString = contentStringParts.join("\n\n");
212
+
213
+ if (isError) {
214
+ return styleText("red", contentString);
215
+ }
216
+
217
+ if (toolResult.toolName === "exec_command") {
218
+ return contentString
219
+ .replace(/(^<stdout>|<\/stdout>$)/gm, styleText("blue", "$1"))
220
+ .replace(
221
+ /(<truncated_output.+?>|<\/truncated_output>)/g,
222
+ styleText("yellow", "$1"),
223
+ )
224
+ .replace(/(^<stderr>|<\/stderr>$)/gm, styleText("magenta", "$1"))
225
+ .replace(/(^<error>|<\/error>$)/gm, styleText("red", "$1"));
226
+ }
227
+
228
+ if (toolResult.toolName === "tmux_command") {
229
+ return contentString
230
+ .replace(/(^<stdout>|<\/stdout>$)/gm, styleText("blue", "$1"))
231
+ .replace(/(^<stderr>|<\/stderr>$)/gm, styleText("magenta", "$1"))
232
+ .replace(/(^<error>|<\/error>$)/gm, styleText("red", "$1"))
233
+ .replace(/(^<tmux:.*?>|<\/tmux:.*?>$)/gm, styleText("green", "$1"));
234
+ }
235
+
236
+ if (contentString.length > MAX_DISPLAY_OUTPUT_LENGTH) {
237
+ return [
238
+ contentString.slice(0, MAX_DISPLAY_OUTPUT_LENGTH),
239
+ styleText("yellow", "... (Output truncated for display)"),
240
+ "\n",
241
+ ].join("");
242
+ }
243
+
244
+ return contentString;
245
+ }
246
+
247
+ /**
248
+ * Format provider token usage for display.
249
+ * @param {ProviderTokenUsage} usage
250
+ * @returns {string}
251
+ */
252
+ export function formatProviderTokenUsage(usage) {
253
+ /** @type {string[]} */
254
+ const lines = [];
255
+ /** @type {string[]} */
256
+ const header = [];
257
+ for (const [key, value] of Object.entries(usage)) {
258
+ if (typeof value === "number") {
259
+ header.push(`${key}: ${value}`);
260
+ } else if (typeof value === "string") {
261
+ header.push(`${key}: ${value}`);
262
+ } else if (value) {
263
+ lines.push(
264
+ `(${key}) ${Object.entries(value)
265
+ .filter(
266
+ ([k]) =>
267
+ ![
268
+ // OpenAI
269
+ "audio_tokens",
270
+ "accepted_prediction_tokens",
271
+ "rejected_prediction_tokens",
272
+ ].includes(k),
273
+ )
274
+ .map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
275
+ .join(", ")}`,
276
+ );
277
+ }
278
+ }
279
+
280
+ const outputLines = [`\n${header.join(", ")}`];
281
+
282
+ if (lines.length) {
283
+ outputLines.push(lines.join(" / "));
284
+ }
285
+
286
+ return styleText("gray", outputLines.join("\n"));
287
+ }
288
+
289
+ /**
290
+ * Format cost summary for interactive display
291
+ * @param {import("./costTracker.mjs").CostSummary} summary
292
+ * @returns {string}
293
+ */
294
+ export function formatCostSummary(summary) {
295
+ if (!summary || Object.keys(summary.breakdown).length === 0) {
296
+ return styleText("gray", "No token usage recorded yet.");
297
+ }
298
+
299
+ const lines = [];
300
+
301
+ if (summary.totalCost !== undefined) {
302
+ lines.push(
303
+ styleText(
304
+ "bold",
305
+ `\nTotal: ${summary.totalCost.toFixed(4)} ${summary.currency}`,
306
+ ),
307
+ );
308
+ } else {
309
+ lines.push(styleText("yellow", "Total: N/A (no cost configuration)"));
310
+ }
311
+
312
+ lines.push(styleText("bold", "\nTokens:"));
313
+ for (const [key, { tokens, cost }] of Object.entries(summary.breakdown)) {
314
+ const tokenStr = `${key}: ${tokens.toLocaleString()}`;
315
+
316
+ if (cost !== undefined) {
317
+ const costStr = `${cost.toFixed(4)} ${summary.currency}`;
318
+ lines.push(` ${tokenStr.padEnd(30)} ${styleText("cyan", costStr)}`);
319
+ } else {
320
+ lines.push(` ${tokenStr.padEnd(30)} ${styleText("gray", "N/A")}`);
321
+ }
322
+ }
323
+
324
+ return lines.join("\n");
325
+ }
326
+
327
+ /**
328
+ * Format cost for batch mode JSON output
329
+ * @param {import("./costTracker.mjs").CostSummary} summary
330
+ */
331
+ export function formatCostForBatch(summary) {
332
+ if (!summary || Object.keys(summary.breakdown).length === 0) {
333
+ return undefined;
334
+ }
335
+
336
+ return {
337
+ total: summary.totalCost,
338
+ currency: summary.currency,
339
+ unit: summary.unit,
340
+ breakdown: Object.fromEntries(
341
+ Object.entries(summary.breakdown).map(([key, { tokens, cost }]) => [
342
+ key,
343
+ { tokens, cost },
344
+ ]),
345
+ ),
346
+ };
347
+ }
348
+
349
+ /**
350
+ * Print a message to the console.
351
+ * @param {Message} message
352
+ */
353
+ export function printMessage(message) {
354
+ switch (message.role) {
355
+ case "assistant": {
356
+ // console.log(styleText("bold", "\nAgent:"));
357
+ for (const part of message.content) {
358
+ switch (part.type) {
359
+ // Note: Streamで表示するためここでは表示しない
360
+ // case "thinking":
361
+ // console.log(
362
+ // [
363
+ // styleText("blue", "<thinking>"),
364
+ // part.thinking,
365
+ // styleText("blue", "</thinking>\n"),
366
+ // ].join("\n"),
367
+ // );
368
+ // break;
369
+ // case "text":
370
+ // console.log(part.text);
371
+ // break;
372
+ case "tool_use":
373
+ console.log(styleText("bold", "\nTool call:"));
374
+ console.log(formatToolUse(part));
375
+ break;
376
+ }
377
+ }
378
+ break;
379
+ }
380
+ case "user": {
381
+ for (const part of message.content) {
382
+ switch (part.type) {
383
+ case "tool_result": {
384
+ console.log(styleText("bold", "\nTool result:"));
385
+ console.log(formatToolResult(part));
386
+ break;
387
+ }
388
+ case "text": {
389
+ console.log(styleText("bold", "\nUser:"));
390
+ const highlighted = part.text.replace(
391
+ /^(<context.+?>|<\/context>)/gm,
392
+ styleText("green", "$1"),
393
+ );
394
+ console.log(highlighted);
395
+ break;
396
+ }
397
+ case "image": {
398
+ break;
399
+ }
400
+ default: {
401
+ console.log(styleText("bold", "\nUnknown Message Format:"));
402
+ console.log(JSON.stringify(part, null, 2));
403
+ }
404
+ }
405
+ }
406
+ break;
407
+ }
408
+ default: {
409
+ console.log(styleText("bold", "\nUnknown Message Format:"));
410
+ console.log(JSON.stringify(message, null, 2));
411
+ }
412
+ }
413
+ }