@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.
Files changed (85) hide show
  1. package/bin/plain +1 -1
  2. package/package.json +8 -9
  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 +518 -0
  15. package/src/cliInteractive.mjs +533 -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 +267 -0
  22. package/src/context/loadPrompts.mjs +303 -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/parseFrontmatter.mjs +19 -0
  77. package/src/utils/readFileRange.mjs +33 -0
  78. package/src/utils/retryOnError.mjs +41 -0
  79. package/src/voiceInput.mjs +61 -0
  80. package/src/voiceInputGemini.mjs +105 -0
  81. package/src/voiceInputOpenAI.mjs +104 -0
  82. package/src/voiceInputSession.mjs +543 -0
  83. package/src/voiceToggleKey.mjs +62 -0
  84. package/dist/main.mjs +0 -473
  85. package/dist/main.mjs.map +0 -7
@@ -0,0 +1,227 @@
1
+ /**
2
+ * @import { ClaudeCodePlugin } from "./claudeCodePlugin.mjs"
3
+ */
4
+
5
+ import { styleText } from "node:util";
6
+ import { loadAgentRoles } from "./context/loadAgentRoles.mjs";
7
+ import { loadPrompts } from "./context/loadPrompts.mjs";
8
+
9
+ // Define available slash commands for tab completion
10
+ export const SLASH_COMMANDS = [
11
+ { name: "/help", description: "Display this help message" },
12
+ { name: "/agents", description: "List available agent roles" },
13
+ {
14
+ name: "/agents:<id>",
15
+ description:
16
+ "Delegate to an agent with the given ID (e.g., /agents:code-simplifier)",
17
+ },
18
+ { name: "/prompts", description: "List available prompts" },
19
+ {
20
+ name: "/prompts:<id>",
21
+ description:
22
+ "Invoke a prompt with the given ID (e.g., /prompts:feature-dev)",
23
+ },
24
+ {
25
+ name: "/<id>",
26
+ description:
27
+ "Shortcut for prompts in the shortcuts/ directory (e.g., /commit)",
28
+ },
29
+ { name: "/paste", description: "Paste content from clipboard" },
30
+ {
31
+ name: "/resume",
32
+ description: "Resume conversation after an LLM provider error",
33
+ },
34
+ { name: "/dump", description: "Save current messages to a JSON file" },
35
+ { name: "/load", description: "Load messages from a JSON file" },
36
+ { name: "/cost", description: "Display session cost and token usage" },
37
+ {
38
+ name: "/compact",
39
+ description:
40
+ "Ask the agent to compact the context by reloading from a memory file",
41
+ },
42
+ ];
43
+
44
+ /**
45
+ * @typedef {Object} CompletionCandidate
46
+ * @property {string} name
47
+ * @property {string} description
48
+ */
49
+
50
+ /**
51
+ * Find candidates that match the line, prioritizing prefix matches.
52
+ * @param {(string | CompletionCandidate)[]} candidates
53
+ * @param {string} line
54
+ * @param {number} queryStartIndex
55
+ * @returns {(string | CompletionCandidate)[]}
56
+ */
57
+ function findMatches(candidates, line, queryStartIndex) {
58
+ const query = line.slice(queryStartIndex);
59
+ const prefixMatches = [];
60
+ const partialMatches = [];
61
+
62
+ for (const candidate of candidates) {
63
+ const name = typeof candidate === "string" ? candidate : candidate.name;
64
+ if (name.startsWith(line)) {
65
+ prefixMatches.push(candidate);
66
+ } else if (
67
+ query.length > 0 &&
68
+ name.slice(queryStartIndex).includes(query)
69
+ ) {
70
+ partialMatches.push(candidate);
71
+ }
72
+ }
73
+
74
+ return [...prefixMatches, ...partialMatches];
75
+ }
76
+
77
+ /**
78
+ * Return the longest common prefix of the given strings.
79
+ * @param {string[]} strings
80
+ * @returns {string}
81
+ */
82
+ function commonPrefix(strings) {
83
+ if (strings.length === 0) return "";
84
+ let prefix = strings[0];
85
+ for (let i = 1; i < strings.length; i++) {
86
+ while (!strings[i].startsWith(prefix)) {
87
+ prefix = prefix.slice(0, -1);
88
+ }
89
+ }
90
+ return prefix;
91
+ }
92
+
93
+ /**
94
+ * Display completion candidates and invoke the readline callback.
95
+ *
96
+ * Node.js readline normally requires two consecutive Tab presses to show the
97
+ * candidate list. This helper lets readline handle the common-prefix
98
+ * auto-completion first, then prints the candidate list on the next tick and
99
+ * redraws the prompt so the display stays clean.
100
+ *
101
+ * @param {import("node:readline").Interface} rl
102
+ * @param {(string | CompletionCandidate)[]} candidates
103
+ * @param {string} line
104
+ * @param {(err: Error | null, result: [string[], string]) => void} callback
105
+ */
106
+ function showCompletions(rl, candidates, line, callback) {
107
+ const names = candidates.map((c) => (typeof c === "string" ? c : c.name));
108
+ if (candidates.length <= 1) {
109
+ callback(null, [names, line]);
110
+ return;
111
+ }
112
+ const prefix = commonPrefix(names);
113
+ if (prefix.length > line.length) {
114
+ // Let readline insert the common prefix.
115
+ callback(null, [[prefix], line]);
116
+ } else {
117
+ // Nothing new to insert.
118
+ callback(null, [[], line]);
119
+ }
120
+ // After readline finishes its own refresh, print the candidate list and
121
+ // redraw the prompt line. We cannot use rl.prompt(true) because its
122
+ // internal _refreshLine clears everything below the prompt start, which
123
+ // erases the candidate list we just wrote. Instead we manually re-output
124
+ // the prompt and current line content.
125
+ setTimeout(() => {
126
+ const maxLength = process.stdout.columns ?? 100;
127
+ const list = candidates
128
+ .map((c) => {
129
+ if (typeof c === "string") return c;
130
+ const nameText = c.name.padEnd(25);
131
+ const separator = " - ";
132
+ const descText = c.description;
133
+
134
+ // 画面幅に合わせて説明文をカット(色を付ける前に計算)
135
+ const availableWidth =
136
+ maxLength - nameText.length - separator.length - 3;
137
+ const displayDesc =
138
+ descText.length > availableWidth && availableWidth > 0
139
+ ? `${descText.slice(0, availableWidth)}...`
140
+ : descText;
141
+
142
+ const name = styleText("cyan", nameText);
143
+ const description = styleText("dim", displayDesc);
144
+ return `${name}${separator}${description}`;
145
+ })
146
+ .join("\r\n");
147
+ process.stdout.write(`\r\n${list}\r\n`);
148
+ process.stdout.write(`${rl.getPrompt()}${rl.line}`);
149
+ }, 0);
150
+ }
151
+
152
+ /**
153
+ * Create a completer function for readline.
154
+ *
155
+ * Because the readline.Interface instance (`cli`) is not available until after
156
+ * `readline.createInterface` returns, we accept a getter function so the
157
+ * completer can resolve the reference lazily at call time.
158
+ *
159
+ * @param {() => import("node:readline").Interface} getCliRef - A function that returns the readline Interface
160
+ * @param {ClaudeCodePlugin[] | undefined} claudeCodePlugins
161
+ * @returns {(line: string, callback: (err?: Error | null, result?: [string[], string]) => void) => void}
162
+ */
163
+ export function createCompleter(getCliRef, claudeCodePlugins) {
164
+ return (line, callback) => {
165
+ (async () => {
166
+ try {
167
+ const cli = getCliRef();
168
+ const prompts = await loadPrompts(claudeCodePlugins);
169
+ const agentRoles = await loadAgentRoles(claudeCodePlugins);
170
+
171
+ if (line.startsWith("/agents:")) {
172
+ const prefix = "/agents:";
173
+ const candidates = Array.from(agentRoles.values()).map((a) => ({
174
+ name: `${prefix}${a.id}`,
175
+ description: a.description,
176
+ }));
177
+ const hits = findMatches(candidates, line, prefix.length);
178
+
179
+ showCompletions(cli, hits, line, callback);
180
+ return;
181
+ }
182
+
183
+ if (line.startsWith("/prompts:")) {
184
+ const prefix = "/prompts:";
185
+ const candidates = Array.from(prompts.values()).map((p) => ({
186
+ name: `${prefix}${p.id}`,
187
+ description: p.description,
188
+ }));
189
+ const hits = findMatches(candidates, line, prefix.length);
190
+
191
+ showCompletions(cli, hits, line, callback);
192
+ return;
193
+ }
194
+
195
+ if (line.startsWith("/")) {
196
+ const shortcuts = Array.from(prompts.values())
197
+ .filter((p) => p.isShortcut)
198
+ .map((p) => ({
199
+ name: `/${p.id}`,
200
+ description: p.description,
201
+ }));
202
+
203
+ const allCommands = [...SLASH_COMMANDS, ...shortcuts].filter(
204
+ (cmd) => {
205
+ const name = typeof cmd === "string" ? cmd : cmd.name;
206
+ return (
207
+ name !== "/<id>" &&
208
+ (name === "/agents:" || !name.startsWith("/agents:")) &&
209
+ (name === "/prompts:" || !name.startsWith("/prompts:"))
210
+ );
211
+ },
212
+ );
213
+
214
+ const hits = findMatches(allCommands, line, 1);
215
+
216
+ showCompletions(cli, hits, line, callback);
217
+ return;
218
+ }
219
+
220
+ callback(null, [[], line]);
221
+ } catch (err) {
222
+ const error = err instanceof Error ? err : new Error(String(err));
223
+ callback(error, [[], line]);
224
+ }
225
+ })();
226
+ };
227
+ }
@@ -0,0 +1,309 @@
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
+ }