@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.
- package/README.md +2 -2
- package/bin/plain +1 -1
- package/config/config.predefined.json +1 -1
- package/config/prompts.predefined/shortcuts/configure.md +1 -1
- package/dist/main.mjs +473 -0
- package/dist/main.mjs.map +7 -0
- package/package.json +5 -7
- package/src/agent.d.ts +0 -52
- package/src/agent.mjs +0 -204
- package/src/agentLoop.mjs +0 -419
- package/src/agentState.mjs +0 -41
- package/src/claudeCodePlugin.mjs +0 -164
- package/src/cliArgs.mjs +0 -175
- package/src/cliBatch.mjs +0 -147
- package/src/cliCommands.mjs +0 -283
- package/src/cliCompleter.mjs +0 -227
- package/src/cliCost.mjs +0 -309
- package/src/cliFormatter.mjs +0 -413
- package/src/cliInteractive.mjs +0 -529
- package/src/cliInterruptTransform.mjs +0 -51
- package/src/cliMuteTransform.mjs +0 -26
- package/src/cliPasteTransform.mjs +0 -183
- package/src/config.d.ts +0 -36
- package/src/config.mjs +0 -197
- package/src/context/loadAgentRoles.mjs +0 -283
- package/src/context/loadPrompts.mjs +0 -324
- package/src/context/loadUserMessageContext.mjs +0 -147
- package/src/costTracker.mjs +0 -210
- package/src/env.mjs +0 -44
- package/src/main.mjs +0 -279
- package/src/mcpClient.mjs +0 -351
- package/src/mcpIntegration.mjs +0 -160
- package/src/model.d.ts +0 -109
- package/src/modelCaller.mjs +0 -32
- package/src/modelDefinition.d.ts +0 -92
- package/src/prompt.mjs +0 -138
- package/src/providers/anthropic.d.ts +0 -248
- package/src/providers/anthropic.mjs +0 -587
- package/src/providers/bedrock.d.ts +0 -249
- package/src/providers/bedrock.mjs +0 -700
- package/src/providers/gemini.d.ts +0 -208
- package/src/providers/gemini.mjs +0 -754
- package/src/providers/openai.d.ts +0 -281
- package/src/providers/openai.mjs +0 -544
- package/src/providers/openaiCompatible.d.ts +0 -147
- package/src/providers/openaiCompatible.mjs +0 -652
- package/src/providers/platform/awsSigV4.mjs +0 -184
- package/src/providers/platform/azure.mjs +0 -42
- package/src/providers/platform/bedrock.mjs +0 -78
- package/src/providers/platform/googleCloud.mjs +0 -34
- package/src/subagent.mjs +0 -265
- package/src/tmpfile.mjs +0 -27
- package/src/tool.d.ts +0 -74
- package/src/toolExecutor.mjs +0 -236
- package/src/toolInputValidator.mjs +0 -183
- package/src/toolUseApprover.mjs +0 -99
- package/src/tools/askURL.mjs +0 -209
- package/src/tools/askWeb.mjs +0 -208
- package/src/tools/compactContext.d.ts +0 -4
- package/src/tools/compactContext.mjs +0 -87
- package/src/tools/delegateToSubagent.d.ts +0 -4
- package/src/tools/delegateToSubagent.mjs +0 -48
- package/src/tools/execCommand.d.ts +0 -22
- package/src/tools/execCommand.mjs +0 -200
- package/src/tools/patchFile.d.ts +0 -4
- package/src/tools/patchFile.mjs +0 -133
- package/src/tools/reportAsSubagent.d.ts +0 -3
- package/src/tools/reportAsSubagent.mjs +0 -44
- package/src/tools/tmuxCommand.d.ts +0 -14
- package/src/tools/tmuxCommand.mjs +0 -194
- package/src/tools/writeFile.d.ts +0 -4
- package/src/tools/writeFile.mjs +0 -56
- package/src/usageStore.mjs +0 -167
- package/src/utils/evalJSONConfig.mjs +0 -72
- package/src/utils/matchValue.d.ts +0 -6
- package/src/utils/matchValue.mjs +0 -40
- package/src/utils/noThrow.mjs +0 -31
- package/src/utils/notify.mjs +0 -29
- package/src/utils/parseFileRange.mjs +0 -18
- package/src/utils/readFileRange.mjs +0 -33
- package/src/utils/retryOnError.mjs +0 -41
- package/src/voiceInput.mjs +0 -61
- package/src/voiceInputGemini.mjs +0 -105
- package/src/voiceInputOpenAI.mjs +0 -104
- package/src/voiceInputSession.mjs +0 -543
- 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
|
-
}
|
package/src/cliFormatter.mjs
DELETED
|
@@ -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
|
-
}
|