@iinm/plain-agent 1.8.3 → 1.8.4

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 (86) hide show
  1. package/README.md +2 -2
  2. package/bin/plain +1 -1
  3. package/config/config.predefined.json +1 -1
  4. package/config/prompts.predefined/shortcuts/configure.md +1 -1
  5. package/dist/main.mjs +473 -0
  6. package/dist/main.mjs.map +7 -0
  7. package/package.json +5 -7
  8. package/src/agent.d.ts +0 -52
  9. package/src/agent.mjs +0 -204
  10. package/src/agentLoop.mjs +0 -419
  11. package/src/agentState.mjs +0 -41
  12. package/src/claudeCodePlugin.mjs +0 -164
  13. package/src/cliArgs.mjs +0 -175
  14. package/src/cliBatch.mjs +0 -147
  15. package/src/cliCommands.mjs +0 -283
  16. package/src/cliCompleter.mjs +0 -227
  17. package/src/cliCost.mjs +0 -309
  18. package/src/cliFormatter.mjs +0 -413
  19. package/src/cliInteractive.mjs +0 -529
  20. package/src/cliInterruptTransform.mjs +0 -51
  21. package/src/cliMuteTransform.mjs +0 -26
  22. package/src/cliPasteTransform.mjs +0 -183
  23. package/src/config.d.ts +0 -36
  24. package/src/config.mjs +0 -197
  25. package/src/context/loadAgentRoles.mjs +0 -283
  26. package/src/context/loadPrompts.mjs +0 -324
  27. package/src/context/loadUserMessageContext.mjs +0 -147
  28. package/src/costTracker.mjs +0 -210
  29. package/src/env.mjs +0 -44
  30. package/src/main.mjs +0 -279
  31. package/src/mcpClient.mjs +0 -351
  32. package/src/mcpIntegration.mjs +0 -160
  33. package/src/model.d.ts +0 -109
  34. package/src/modelCaller.mjs +0 -32
  35. package/src/modelDefinition.d.ts +0 -92
  36. package/src/prompt.mjs +0 -138
  37. package/src/providers/anthropic.d.ts +0 -248
  38. package/src/providers/anthropic.mjs +0 -587
  39. package/src/providers/bedrock.d.ts +0 -249
  40. package/src/providers/bedrock.mjs +0 -700
  41. package/src/providers/gemini.d.ts +0 -208
  42. package/src/providers/gemini.mjs +0 -754
  43. package/src/providers/openai.d.ts +0 -281
  44. package/src/providers/openai.mjs +0 -544
  45. package/src/providers/openaiCompatible.d.ts +0 -147
  46. package/src/providers/openaiCompatible.mjs +0 -652
  47. package/src/providers/platform/awsSigV4.mjs +0 -184
  48. package/src/providers/platform/azure.mjs +0 -42
  49. package/src/providers/platform/bedrock.mjs +0 -78
  50. package/src/providers/platform/googleCloud.mjs +0 -34
  51. package/src/subagent.mjs +0 -265
  52. package/src/tmpfile.mjs +0 -27
  53. package/src/tool.d.ts +0 -74
  54. package/src/toolExecutor.mjs +0 -236
  55. package/src/toolInputValidator.mjs +0 -183
  56. package/src/toolUseApprover.mjs +0 -99
  57. package/src/tools/askURL.mjs +0 -209
  58. package/src/tools/askWeb.mjs +0 -208
  59. package/src/tools/compactContext.d.ts +0 -4
  60. package/src/tools/compactContext.mjs +0 -87
  61. package/src/tools/delegateToSubagent.d.ts +0 -4
  62. package/src/tools/delegateToSubagent.mjs +0 -48
  63. package/src/tools/execCommand.d.ts +0 -22
  64. package/src/tools/execCommand.mjs +0 -200
  65. package/src/tools/patchFile.d.ts +0 -4
  66. package/src/tools/patchFile.mjs +0 -133
  67. package/src/tools/reportAsSubagent.d.ts +0 -3
  68. package/src/tools/reportAsSubagent.mjs +0 -44
  69. package/src/tools/tmuxCommand.d.ts +0 -14
  70. package/src/tools/tmuxCommand.mjs +0 -194
  71. package/src/tools/writeFile.d.ts +0 -4
  72. package/src/tools/writeFile.mjs +0 -56
  73. package/src/usageStore.mjs +0 -167
  74. package/src/utils/evalJSONConfig.mjs +0 -72
  75. package/src/utils/matchValue.d.ts +0 -6
  76. package/src/utils/matchValue.mjs +0 -40
  77. package/src/utils/noThrow.mjs +0 -31
  78. package/src/utils/notify.mjs +0 -29
  79. package/src/utils/parseFileRange.mjs +0 -18
  80. package/src/utils/readFileRange.mjs +0 -33
  81. package/src/utils/retryOnError.mjs +0 -41
  82. package/src/voiceInput.mjs +0 -61
  83. package/src/voiceInputGemini.mjs +0 -105
  84. package/src/voiceInputOpenAI.mjs +0 -104
  85. package/src/voiceInputSession.mjs +0 -543
  86. package/src/voiceToggleKey.mjs +0 -62
package/src/cliCost.mjs DELETED
@@ -1,309 +0,0 @@
1
- /**
2
- * @import { UsageRecord } from "./usageStore.mjs"
3
- */
4
-
5
- import { styleText } from "node:util";
6
- import * as usageStore from "./usageStore.mjs";
7
-
8
- /**
9
- * @typedef {Object} CostPeriod
10
- * @property {string} from - YYYY-MM-DD (inclusive, local date)
11
- * @property {string} to - YYYY-MM-DD (inclusive, local date)
12
- */
13
-
14
- /**
15
- * @typedef {Object} DailyEntry
16
- * @property {string} date - YYYY-MM-DD
17
- * @property {number} totalCost
18
- * @property {number} sessionCount
19
- */
20
-
21
- /**
22
- * @typedef {Object} CurrencyAggregation
23
- * @property {string} currency
24
- * @property {DailyEntry[]} daily - sorted by date ascending
25
- * @property {number} totalCost
26
- * @property {number} sessionCount
27
- */
28
-
29
- /**
30
- * @typedef {Object} CostReport
31
- * @property {CostPeriod} period
32
- * @property {CurrencyAggregation[]} byCurrency - sorted by currency
33
- * @property {number} noPricingSessionCount - sessions without cost data
34
- * @property {number} excludedOutOfRange - records dropped (out of period)
35
- * @property {number} totalRecords - records considered (before filtering)
36
- */
37
-
38
- /**
39
- * Compute the default period: first day of current month (local) through today (local).
40
- * @param {Date} [now]
41
- * @returns {CostPeriod}
42
- */
43
- export function defaultPeriod(now = new Date()) {
44
- const y = now.getFullYear();
45
- const m = now.getMonth();
46
- const firstOfMonth = new Date(y, m, 1);
47
- return {
48
- from: formatLocalDate(firstOfMonth),
49
- to: formatLocalDate(now),
50
- };
51
- }
52
-
53
- /**
54
- * Format a Date as YYYY-MM-DD in local time.
55
- * @param {Date} date
56
- * @returns {string}
57
- */
58
- export function formatLocalDate(date) {
59
- const y = date.getFullYear();
60
- const m = `${date.getMonth() + 1}`.padStart(2, "0");
61
- const d = `${date.getDate()}`.padStart(2, "0");
62
- return `${y}-${m}-${d}`;
63
- }
64
-
65
- /**
66
- * Parse and validate a YYYY-MM-DD string, returning a Date at local midnight.
67
- * @param {string} value
68
- * @returns {Date}
69
- */
70
- export function parseDateOnly(value) {
71
- const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
72
- if (!match) {
73
- throw new Error(`Invalid date: "${value}" (expected YYYY-MM-DD)`);
74
- }
75
- const [, year, month, day] = match;
76
- const y = Number(year);
77
- const m = Number(month);
78
- const d = Number(day);
79
- const date = new Date(y, m - 1, d);
80
- if (
81
- date.getFullYear() !== y ||
82
- date.getMonth() !== m - 1 ||
83
- date.getDate() !== d
84
- ) {
85
- throw new Error(`Invalid date: "${value}"`);
86
- }
87
- return date;
88
- }
89
-
90
- /**
91
- * Aggregate usage records into a cost report.
92
- *
93
- * @param {UsageRecord[]} records
94
- * @param {CostPeriod} period
95
- * @returns {CostReport}
96
- */
97
- export function aggregateUsage(records, period) {
98
- const fromDate = parseDateOnly(period.from);
99
- const toDate = parseDateOnly(period.to);
100
- if (fromDate.getTime() > toDate.getTime()) {
101
- throw new Error(
102
- `"from" (${period.from}) must be on or before "to" (${period.to}).`,
103
- );
104
- }
105
-
106
- /** @type {Map<string, Map<string, DailyEntry>>} */
107
- const byCurrency = new Map();
108
- let noPricingSessionCount = 0;
109
- let excludedOutOfRange = 0;
110
-
111
- for (const record of records) {
112
- if (record.timestamp == null) {
113
- excludedOutOfRange++;
114
- continue;
115
- }
116
- const recordedAt = new Date(record.timestamp);
117
- if (Number.isNaN(recordedAt.getTime())) {
118
- excludedOutOfRange++;
119
- continue;
120
- }
121
- const localDate = formatLocalDate(recordedAt);
122
- if (localDate < period.from || localDate > period.to) {
123
- excludedOutOfRange++;
124
- continue;
125
- }
126
- if (record.totalCost === null) {
127
- noPricingSessionCount++;
128
- continue;
129
- }
130
- if (!record.currency || typeof record.currency !== "string") {
131
- excludedOutOfRange++;
132
- continue;
133
- }
134
-
135
- const perDate = byCurrency.get(record.currency) ?? new Map();
136
- byCurrency.set(record.currency, perDate);
137
- const existing = perDate.get(localDate);
138
- if (existing) {
139
- existing.totalCost += record.totalCost;
140
- existing.sessionCount += 1;
141
- } else {
142
- perDate.set(localDate, {
143
- date: localDate,
144
- totalCost: record.totalCost,
145
- sessionCount: 1,
146
- });
147
- }
148
- }
149
-
150
- /** @type {CurrencyAggregation[]} */
151
- const aggregations = [];
152
- for (const [currency, perDate] of byCurrency) {
153
- const daily = Array.from(perDate.values()).sort((a, b) =>
154
- a.date.localeCompare(b.date),
155
- );
156
- let totalCost = 0;
157
- let sessionCount = 0;
158
- for (const entry of daily) {
159
- totalCost += entry.totalCost;
160
- sessionCount += entry.sessionCount;
161
- }
162
- aggregations.push({ currency, daily, totalCost, sessionCount });
163
- }
164
- aggregations.sort((a, b) => a.currency.localeCompare(b.currency));
165
-
166
- return {
167
- period,
168
- byCurrency: aggregations,
169
- noPricingSessionCount,
170
- excludedOutOfRange,
171
- totalRecords: records.length,
172
- };
173
- }
174
-
175
- /**
176
- * @param {number} count
177
- * @returns {string}
178
- */
179
- function formatSessions(count) {
180
- return `${count} session${count === 1 ? "" : "s"}`;
181
- }
182
-
183
- /**
184
- * Render a cost report as a human-readable string.
185
- *
186
- * @param {CostReport} report
187
- * @param {{ color?: boolean }} [options]
188
- * @returns {string}
189
- */
190
- export function formatCostReport(report, options = {}) {
191
- const color = options.color ?? true;
192
- /** @param {string | string[]} _modifiers @param {string} text @returns {string} */
193
- const plainStyle = (_modifiers, text) => text;
194
- const style = color ? styleText : plainStyle;
195
-
196
- const lines = [];
197
- lines.push(
198
- style("bold", `Period: ${report.period.from} to ${report.period.to}`),
199
- );
200
-
201
- if (report.byCurrency.length === 0) {
202
- lines.push("");
203
- lines.push(style("gray", "No usage recorded in this period."));
204
- if (report.noPricingSessionCount > 0) {
205
- lines.push(
206
- style(
207
- "gray",
208
- `(${report.noPricingSessionCount} session(s) had no pricing configuration)`,
209
- ),
210
- );
211
- }
212
- return lines.join("\n");
213
- }
214
-
215
- for (const agg of report.byCurrency) {
216
- lines.push("");
217
- lines.push(style("bold", `Daily cost (${agg.currency}):`));
218
- for (const entry of agg.daily) {
219
- lines.push(
220
- ` ${entry.date} ${formatCost(entry.totalCost)} ${agg.currency} (${formatSessions(entry.sessionCount)})`,
221
- );
222
- }
223
- lines.push("");
224
- lines.push(
225
- style(
226
- "bold",
227
- `Total: ${formatCost(agg.totalCost)} ${agg.currency} (${formatSessions(agg.sessionCount)})`,
228
- ),
229
- );
230
- }
231
-
232
- if (report.noPricingSessionCount > 0) {
233
- lines.push("");
234
- lines.push(
235
- style(
236
- "gray",
237
- `Note: ${report.noPricingSessionCount} session(s) had no pricing configuration and are excluded from totals.`,
238
- ),
239
- );
240
- }
241
-
242
- return lines.join("\n");
243
- }
244
-
245
- /**
246
- * Format a cost value with 4 decimals.
247
- * @param {number} value
248
- * @returns {string}
249
- */
250
- function formatCost(value) {
251
- return value.toFixed(4);
252
- }
253
-
254
- /**
255
- * Run the `plain cost` subcommand.
256
- *
257
- * @param {{ from: string | null, to: string | null }} args
258
- * @param {{ readUsageRecords?: typeof import("./usageStore.mjs").readUsageRecords }} [deps]
259
- * @returns {Promise<number>} exit code
260
- */
261
- export async function runCostCommand(args, deps = {}) {
262
- let from;
263
- let to;
264
- try {
265
- ({ from, to } = resolvePeriod(args));
266
- } catch (err) {
267
- if (err instanceof Error) {
268
- console.error(`Error: ${err.message}`);
269
- } else {
270
- console.error("Error: invalid period arguments");
271
- }
272
- return 1;
273
- }
274
-
275
- const { records, skipped } = await (
276
- deps.readUsageRecords ?? usageStore.readUsageRecords
277
- )();
278
- if (skipped.length > 0) {
279
- const details = skipped
280
- .slice(0, 3)
281
- .map((s) => `line ${s.line}: ${s.reason}`)
282
- .join(", ");
283
- const ellipsis =
284
- skipped.length > 3 ? `, and ${skipped.length - 3} more` : "";
285
- console.error(
286
- `Warning: skipped ${skipped.length} malformed line(s) in usage log (${details}${ellipsis}).`,
287
- );
288
- }
289
-
290
- const report = aggregateUsage(records, { from, to });
291
- console.log(formatCostReport(report));
292
- return skipped.length > 0 ? 1 : 0;
293
- }
294
-
295
- /**
296
- * Resolve a period from CLI arguments, falling back to the current month.
297
- *
298
- * @param {{ from: string | null, to: string | null }} args
299
- * @returns {CostPeriod}
300
- */
301
- export function resolvePeriod(args) {
302
- const fallback = defaultPeriod();
303
- const from = args.from ?? fallback.from;
304
- const to = args.to ?? fallback.to;
305
- // Validate format (throws on invalid input).
306
- parseDateOnly(from);
307
- parseDateOnly(to);
308
- return { from, to };
309
- }
@@ -1,413 +0,0 @@
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 { DelegateToSubagentInput } from "./tools/delegateToSubagent"
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 === "delegate_to_subagent") {
133
- /** @type {Partial<DelegateToSubagentInput>} */
134
- const delegateInput = input;
135
- return [
136
- `tool: ${toolName}`,
137
- `name: ${delegateInput.name}`,
138
- `goal: ${delegateInput.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 === "report_as_subagent") {
153
- /** @type {Partial<import("./tools/reportAsSubagent").ReportAsSubagentInput>} */
154
- const reportAsSubagentInput = input;
155
- return [
156
- `tool: ${toolName}`,
157
- `memoryPath: ${reportAsSubagentInput.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
- }