@iinm/plain-agent 1.0.0

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 (79) hide show
  1. package/.config/agents.library/code-simplifier.md +5 -0
  2. package/.config/agents.library/qa-engineer.md +74 -0
  3. package/.config/agents.library/software-architect.md +278 -0
  4. package/.config/agents.predefined/worker.md +3 -0
  5. package/.config/config.predefined.json +825 -0
  6. package/.config/prompts.library/code-review.md +8 -0
  7. package/.config/prompts.library/feature-dev.md +6 -0
  8. package/.config/prompts.predefined/shortcuts/commit-by-user.md +9 -0
  9. package/.config/prompts.predefined/shortcuts/commit.md +10 -0
  10. package/.config/prompts.predefined/shortcuts/general-question.md +6 -0
  11. package/LICENSE +21 -0
  12. package/README.md +624 -0
  13. package/bin/plain +3 -0
  14. package/bin/plain-interrupt +6 -0
  15. package/bin/plain-notify-desktop +19 -0
  16. package/bin/plain-notify-terminal-bell +3 -0
  17. package/package.json +57 -0
  18. package/sandbox/bin/plain-sandbox +972 -0
  19. package/src/agent.d.ts +48 -0
  20. package/src/agent.mjs +159 -0
  21. package/src/agentLoop.mjs +369 -0
  22. package/src/agentState.mjs +41 -0
  23. package/src/cliArgs.mjs +45 -0
  24. package/src/cliFormatter.mjs +217 -0
  25. package/src/cliInteractive.mjs +739 -0
  26. package/src/config.d.ts +48 -0
  27. package/src/config.mjs +168 -0
  28. package/src/context/consumeInterruptMessage.mjs +30 -0
  29. package/src/context/loadAgentRoles.mjs +272 -0
  30. package/src/context/loadPrompts.mjs +312 -0
  31. package/src/context/loadUserMessageContext.mjs +147 -0
  32. package/src/env.mjs +46 -0
  33. package/src/main.mjs +202 -0
  34. package/src/mcp.mjs +202 -0
  35. package/src/model.d.ts +109 -0
  36. package/src/modelCaller.mjs +29 -0
  37. package/src/modelDefinition.d.ts +73 -0
  38. package/src/prompt.mjs +128 -0
  39. package/src/providers/anthropic.d.ts +248 -0
  40. package/src/providers/anthropic.mjs +596 -0
  41. package/src/providers/gemini.d.ts +208 -0
  42. package/src/providers/gemini.mjs +752 -0
  43. package/src/providers/openai.d.ts +281 -0
  44. package/src/providers/openai.mjs +551 -0
  45. package/src/providers/openaiCompatible.d.ts +147 -0
  46. package/src/providers/openaiCompatible.mjs +658 -0
  47. package/src/providers/platform/azure.mjs +42 -0
  48. package/src/providers/platform/bedrock.mjs +74 -0
  49. package/src/providers/platform/googleCloud.mjs +34 -0
  50. package/src/subagent.mjs +247 -0
  51. package/src/tmpfile.mjs +27 -0
  52. package/src/tool.d.ts +74 -0
  53. package/src/toolExecutor.mjs +236 -0
  54. package/src/toolInputValidator.mjs +183 -0
  55. package/src/toolUseApprover.mjs +98 -0
  56. package/src/tools/askGoogle.mjs +135 -0
  57. package/src/tools/delegateToSubagent.d.ts +4 -0
  58. package/src/tools/delegateToSubagent.mjs +48 -0
  59. package/src/tools/execCommand.d.ts +22 -0
  60. package/src/tools/execCommand.mjs +200 -0
  61. package/src/tools/fetchWebPage.mjs +96 -0
  62. package/src/tools/patchFile.d.ts +4 -0
  63. package/src/tools/patchFile.mjs +96 -0
  64. package/src/tools/reportAsSubagent.d.ts +3 -0
  65. package/src/tools/reportAsSubagent.mjs +44 -0
  66. package/src/tools/tavilySearch.d.ts +6 -0
  67. package/src/tools/tavilySearch.mjs +57 -0
  68. package/src/tools/tmuxCommand.d.ts +14 -0
  69. package/src/tools/tmuxCommand.mjs +194 -0
  70. package/src/tools/writeFile.d.ts +4 -0
  71. package/src/tools/writeFile.mjs +56 -0
  72. package/src/utils/evalJSONConfig.mjs +48 -0
  73. package/src/utils/matchValue.d.ts +6 -0
  74. package/src/utils/matchValue.mjs +40 -0
  75. package/src/utils/noThrow.mjs +31 -0
  76. package/src/utils/notify.mjs +28 -0
  77. package/src/utils/parseFileRange.mjs +18 -0
  78. package/src/utils/readFileRange.mjs +33 -0
  79. package/src/utils/retryOnError.mjs +41 -0
@@ -0,0 +1,739 @@
1
+ /**
2
+ * @import { Message } from "./model"
3
+ * @import { UserEventEmitter, AgentEventEmitter, AgentCommands } from "./agent"
4
+ * @import { ClaudeCodePluginConfig } from "./config"
5
+ */
6
+
7
+ import { execFileSync } from "node:child_process";
8
+ import readline from "node:readline";
9
+ import { styleText } from "node:util";
10
+ import {
11
+ formatProviderTokenUsage,
12
+ formatToolResult,
13
+ formatToolUse,
14
+ } from "./cliFormatter.mjs";
15
+ import { consumeInterruptMessage } from "./context/consumeInterruptMessage.mjs";
16
+ import { loadAgentRoles } from "./context/loadAgentRoles.mjs";
17
+ import { loadPrompts } from "./context/loadPrompts.mjs";
18
+ import { loadUserMessageContext } from "./context/loadUserMessageContext.mjs";
19
+ import { notify } from "./utils/notify.mjs";
20
+ import { parseFileRange } from "./utils/parseFileRange.mjs";
21
+ import { readFileRange } from "./utils/readFileRange.mjs";
22
+
23
+ // Define available slash commands for tab completion
24
+ const SLASH_COMMANDS = [
25
+ { name: "/help", description: "Display this help message" },
26
+ { name: "/agents", description: "List available agent roles" },
27
+ {
28
+ name: "/agents:<id>",
29
+ description:
30
+ "Delegate to an agent with the given ID (e.g., /agents:code-simplifier)",
31
+ },
32
+ { name: "/prompts", description: "List available prompts" },
33
+ {
34
+ name: "/prompts:<id>",
35
+ description:
36
+ "Invoke a prompt with the given ID (e.g., /prompts:feature-dev)",
37
+ },
38
+ {
39
+ name: "/<id>",
40
+ description:
41
+ "Shortcut for prompts in the shortcuts/ directory (e.g., /commit)",
42
+ },
43
+ { name: "/paste", description: "Paste content from clipboard" },
44
+ {
45
+ name: "/resume",
46
+ description: "Resume conversation after an LLM provider error",
47
+ },
48
+ { name: "/dump", description: "Save current messages to a JSON file" },
49
+ { name: "/load", description: "Load messages from a JSON file" },
50
+ ];
51
+
52
+ /**
53
+ * @typedef {Object} CompletionCandidate
54
+ * @property {string} name
55
+ * @property {string} description
56
+ */
57
+
58
+ /**
59
+ * Find candidates that match the line, prioritizing prefix matches.
60
+ * @param {(string | CompletionCandidate)[]} candidates
61
+ * @param {string} line
62
+ * @param {number} queryStartIndex
63
+ * @returns {(string | CompletionCandidate)[]}
64
+ */
65
+ function findMatches(candidates, line, queryStartIndex) {
66
+ const query = line.slice(queryStartIndex);
67
+ const prefixMatches = [];
68
+ const partialMatches = [];
69
+
70
+ for (const candidate of candidates) {
71
+ const name = typeof candidate === "string" ? candidate : candidate.name;
72
+ if (name.startsWith(line)) {
73
+ prefixMatches.push(candidate);
74
+ } else if (
75
+ query.length > 0 &&
76
+ name.slice(queryStartIndex).includes(query)
77
+ ) {
78
+ partialMatches.push(candidate);
79
+ }
80
+ }
81
+
82
+ return [...prefixMatches, ...partialMatches];
83
+ }
84
+
85
+ /**
86
+ * Return the longest common prefix of the given strings.
87
+ * @param {string[]} strings
88
+ * @returns {string}
89
+ */
90
+ function commonPrefix(strings) {
91
+ if (strings.length === 0) return "";
92
+ let prefix = strings[0];
93
+ for (let i = 1; i < strings.length; i++) {
94
+ while (!strings[i].startsWith(prefix)) {
95
+ prefix = prefix.slice(0, -1);
96
+ }
97
+ }
98
+ return prefix;
99
+ }
100
+
101
+ /**
102
+ * Display completion candidates and invoke the readline callback.
103
+ *
104
+ * Node.js readline normally requires two consecutive Tab presses to show the
105
+ * candidate list. This helper lets readline handle the common-prefix
106
+ * auto-completion first, then prints the candidate list on the next tick and
107
+ * redraws the prompt so the display stays clean.
108
+ *
109
+ * @param {import("node:readline").Interface} rl
110
+ * @param {(string | CompletionCandidate)[]} candidates
111
+ * @param {string} line
112
+ * @param {(err: Error | null, result: [string[], string]) => void} callback
113
+ */
114
+ function showCompletions(rl, candidates, line, callback) {
115
+ const names = candidates.map((c) => (typeof c === "string" ? c : c.name));
116
+ if (candidates.length <= 1) {
117
+ callback(null, [names, line]);
118
+ return;
119
+ }
120
+ const prefix = commonPrefix(names);
121
+ if (prefix.length > line.length) {
122
+ // Let readline insert the common prefix.
123
+ callback(null, [[prefix], line]);
124
+ } else {
125
+ // Nothing new to insert.
126
+ callback(null, [[], line]);
127
+ }
128
+ // After readline finishes its own refresh, print the candidate list and
129
+ // redraw the prompt line. We cannot use rl.prompt(true) because its
130
+ // internal _refreshLine clears everything below the prompt start, which
131
+ // erases the candidate list we just wrote. Instead we manually re-output
132
+ // the prompt and current line content.
133
+ setTimeout(() => {
134
+ const maxLength = process.stdout.columns ?? 100;
135
+ const list = candidates
136
+ .map((c) => {
137
+ if (typeof c === "string") return c;
138
+ const nameText = c.name.padEnd(25);
139
+ const separator = " - ";
140
+ const descText = c.description;
141
+
142
+ // 画面幅に合わせて説明文をカット(色を付ける前に計算)
143
+ const availableWidth =
144
+ maxLength - nameText.length - separator.length - 3;
145
+ const displayDesc =
146
+ descText.length > availableWidth && availableWidth > 0
147
+ ? `${descText.slice(0, availableWidth)}...`
148
+ : descText;
149
+
150
+ const name = styleText("cyan", nameText);
151
+ const description = styleText("dim", displayDesc);
152
+ return `${name}${separator}${description}`;
153
+ })
154
+ .join("\r\n");
155
+ process.stdout.write(`\r\n${list}\r\n`);
156
+ process.stdout.write(`${rl.getPrompt()}${rl.line}`);
157
+ }, 0);
158
+ }
159
+
160
+ const HELP_MESSAGE = [
161
+ "Commands:",
162
+ ...SLASH_COMMANDS.map(
163
+ (cmd) => ` ${cmd.name.padEnd(13)} - ${cmd.description}`,
164
+ ),
165
+ "",
166
+ "Multi-line Input Syntax:",
167
+ ' """ - Start/stop multi-line input mode',
168
+ "",
169
+ "File Input Syntax:",
170
+ " !path/to/file - Read content from a file",
171
+ " !path/to/file:N - Read line N from a file",
172
+ " !path/to/file:N-M - Read lines N to M from a file",
173
+ "",
174
+ "References (use within input content):",
175
+ " @path/to/file - Reference content from another file",
176
+ " @path/to/file:N - Reference line N from another file",
177
+ " @path/to/file:N-M - Reference lines N to M from another file",
178
+ "",
179
+ "Image Attachments (use within input content):",
180
+ " @path/to/image.png - Attach an image (png, jpg, jpeg, gif, webp)",
181
+ " @'path/with spaces.png' - Quote paths that include spaces",
182
+ " @path/with\\ spaces.png - Escape spaces with a backslash",
183
+ ]
184
+ .join("\n")
185
+ .trim()
186
+ .replace(/^[^ ].*:/gm, (m) => styleText("bold", m))
187
+ .replace(/^ {2}\/.+?(?= - )/gm, (m) => styleText("cyan", m))
188
+ .replace(/^ {2}.+?(?= - )/gm, (m) => styleText("blue", m));
189
+
190
+ /**
191
+ * @typedef {object} CliOptions
192
+ * @property {UserEventEmitter} userEventEmitter
193
+ * @property {AgentEventEmitter} agentEventEmitter
194
+ * @property {AgentCommands} agentCommands
195
+ * @property {string} sessionId
196
+ * @property {string} modelName
197
+ * @property {string} notifyCmd
198
+ * @property {boolean} sandbox
199
+ * @property {() => Promise<void>} onStop
200
+ * @property {ClaudeCodePluginConfig[]} [claudeCodePlugins]
201
+ */
202
+
203
+ /**
204
+ * @param {CliOptions} options
205
+ */
206
+ export function startInteractiveSession({
207
+ userEventEmitter,
208
+ agentEventEmitter,
209
+ agentCommands,
210
+ sessionId,
211
+ modelName,
212
+ notifyCmd,
213
+ sandbox,
214
+ onStop,
215
+ claudeCodePlugins,
216
+ }) {
217
+ /** @type {{ turn: boolean, multiLineBuffer: string[] | null, subagentName: string }} */
218
+ const state = {
219
+ turn: true,
220
+ multiLineBuffer: null,
221
+ subagentName: "",
222
+ };
223
+
224
+ /**
225
+ * @param {string} id
226
+ * @param {string} goal
227
+ * @returns {Promise<void>}
228
+ */
229
+ async function invokeAgent(id, goal) {
230
+ const agentRoles = await loadAgentRoles(claudeCodePlugins);
231
+ const agent = agentRoles.get(id);
232
+ const name = agent ? id : `custom:${id}`;
233
+ const message = `Delegate to "${name}" agent with goal: ${goal}`;
234
+
235
+ console.log(styleText("gray", "\n<agent>"));
236
+ console.log(message);
237
+ console.log(styleText("gray", "</agent>"));
238
+
239
+ userEventEmitter.emit("userInput", [{ type: "text", text: message }]);
240
+ state.turn = false;
241
+ }
242
+
243
+ /**
244
+ * @param {string} id
245
+ * @param {string} args
246
+ * @param {string} displayInvocation
247
+ * @returns {Promise<void>}
248
+ */
249
+ async function invokePrompt(id, args, displayInvocation) {
250
+ const prompts = await loadPrompts(claudeCodePlugins);
251
+ const prompt = prompts.get(id);
252
+
253
+ if (!prompt) {
254
+ console.log(styleText("red", `\nPrompt not found: ${id}`));
255
+ cli.prompt();
256
+ return;
257
+ }
258
+
259
+ const invocation = `${displayInvocation}${args ? ` ${args}` : ""}`;
260
+ const message = `System: This prompt was invoked as "${invocation}".\n\n${prompt.content}`;
261
+
262
+ console.log(styleText("gray", "\n<prompt>"));
263
+ console.log(message);
264
+ console.log(styleText("gray", "</prompt>"));
265
+
266
+ userEventEmitter.emit("userInput", [{ type: "text", text: message }]);
267
+ state.turn = false;
268
+ }
269
+
270
+ const getCliPrompt = (subagentName = "") =>
271
+ [
272
+ "",
273
+ styleText(
274
+ ["white", "bgGray"],
275
+ [
276
+ ...(subagentName ? [`[${subagentName}]`] : []),
277
+ `session: ${sessionId} | model: ${modelName} | sandbox: ${sandbox ? "on" : "off"}`,
278
+ ].join(" "),
279
+ ),
280
+ "> ",
281
+ ].join("\n");
282
+
283
+ let currentCliPrompt = getCliPrompt();
284
+ const cli = readline.createInterface({
285
+ input: process.stdin,
286
+ output: process.stdout,
287
+ prompt: currentCliPrompt,
288
+ /**
289
+ * @param {string} line
290
+ * @param {(err?: Error | null, result?: [string[], string]) => void} callback
291
+ */
292
+ completer: (line, callback) => {
293
+ (async () => {
294
+ try {
295
+ const prompts = await loadPrompts(claudeCodePlugins);
296
+ const agentRoles = await loadAgentRoles(claudeCodePlugins);
297
+
298
+ if (line.startsWith("/agents:")) {
299
+ const prefix = "/agents:";
300
+ const candidates = Array.from(agentRoles.values()).map((a) => ({
301
+ name: `${prefix}${a.id}`,
302
+ description: a.description,
303
+ }));
304
+ const hits = findMatches(candidates, line, prefix.length);
305
+
306
+ showCompletions(cli, hits, line, callback);
307
+ return;
308
+ }
309
+
310
+ if (line.startsWith("/prompts:")) {
311
+ const prefix = "/prompts:";
312
+ const candidates = Array.from(prompts.values()).map((p) => ({
313
+ name: `${prefix}${p.id}`,
314
+ description: p.description,
315
+ }));
316
+ const hits = findMatches(candidates, line, prefix.length);
317
+
318
+ showCompletions(cli, hits, line, callback);
319
+ return;
320
+ }
321
+
322
+ if (line.startsWith("/")) {
323
+ const shortcuts = Array.from(prompts.values())
324
+ .filter((p) => p.isShortcut)
325
+ .map((p) => ({
326
+ name: `/${p.id}`,
327
+ description: p.description,
328
+ }));
329
+
330
+ const allCommands = [...SLASH_COMMANDS, ...shortcuts].filter(
331
+ (cmd) => {
332
+ const name = typeof cmd === "string" ? cmd : cmd.name;
333
+ return (
334
+ name !== "/<id>" &&
335
+ (name === "/agents:" || !name.startsWith("/agent:")) &&
336
+ (name === "/prompts:" || !name.startsWith("/prompt:"))
337
+ );
338
+ },
339
+ );
340
+
341
+ const hits = findMatches(allCommands, line, 1);
342
+
343
+ showCompletions(cli, hits, line, callback);
344
+ return;
345
+ }
346
+
347
+ callback(null, [[], line]);
348
+ } catch (err) {
349
+ const error = err instanceof Error ? err : new Error(String(err));
350
+ callback(error, [[], line]);
351
+ }
352
+ })();
353
+ },
354
+ });
355
+
356
+ // Disable automatic prompt redraw on resize during agent turn
357
+ // @ts-expect-error - internal property
358
+ const originalRefreshLine = cli._refreshLine?.bind(cli);
359
+ if (originalRefreshLine) {
360
+ // @ts-expect-error - internal property
361
+ cli._refreshLine = (...args) => {
362
+ if (state.turn) {
363
+ originalRefreshLine(...args);
364
+ }
365
+ };
366
+ }
367
+
368
+ readline.emitKeypressEvents(process.stdin);
369
+ if (process.stdin.isTTY) {
370
+ process.stdin.setRawMode(true);
371
+ }
372
+
373
+ process.stdin.on("keypress", async (_, key) => {
374
+ if (key.ctrl && key.name === "c") {
375
+ await onStop();
376
+ }
377
+
378
+ if (key.ctrl && key.name === "d") {
379
+ await onStop();
380
+ }
381
+ });
382
+
383
+ /**
384
+ * Process the complete user input.
385
+ * @param {string} input
386
+ * @returns {Promise<void>}
387
+ */
388
+ async function processInput(input) {
389
+ const inputTrimmed = input.trim();
390
+
391
+ if (inputTrimmed.length === 0) {
392
+ cli.prompt();
393
+ return;
394
+ }
395
+
396
+ cli.setPrompt(currentCliPrompt);
397
+ await consumeInterruptMessage();
398
+
399
+ if (["/help", "help"].includes(inputTrimmed.toLowerCase())) {
400
+ console.log(`\n${HELP_MESSAGE}`);
401
+ cli.prompt();
402
+ return;
403
+ }
404
+
405
+ if (inputTrimmed.startsWith("!")) {
406
+ const fileRange = parseFileRange(inputTrimmed.slice(1));
407
+ if (fileRange instanceof Error) {
408
+ console.log(styleText("red", `\n${fileRange.message}`));
409
+ cli.prompt();
410
+ return;
411
+ }
412
+
413
+ const fileContent = await readFileRange(fileRange);
414
+ if (fileContent instanceof Error) {
415
+ console.log(styleText("red", `\n${fileContent.message}`));
416
+ cli.prompt();
417
+ return;
418
+ }
419
+
420
+ console.log(styleText("gray", "\n<input>"));
421
+ console.log(fileContent);
422
+ console.log(styleText("gray", "</input>"));
423
+
424
+ const messageWithContext = await loadUserMessageContext(fileContent);
425
+
426
+ userEventEmitter.emit("userInput", messageWithContext);
427
+ state.turn = false;
428
+ return;
429
+ }
430
+
431
+ if (inputTrimmed.toLowerCase() === "/dump") {
432
+ await agentCommands.dumpMessages();
433
+ cli.prompt();
434
+ return;
435
+ }
436
+
437
+ if (inputTrimmed.toLowerCase() === "/load") {
438
+ await agentCommands.loadMessages();
439
+ cli.prompt();
440
+ return;
441
+ }
442
+
443
+ if (inputTrimmed === "/agents") {
444
+ const agentRoles = await loadAgentRoles(claudeCodePlugins);
445
+
446
+ console.log(styleText("bold", "\nAvailable Agent Roles:"));
447
+ if (agentRoles.size === 0) {
448
+ console.log(" No agent roles found.");
449
+ } else {
450
+ for (const role of agentRoles.values()) {
451
+ const maxLength = process.stdout.columns ?? 100;
452
+ const line = ` ${styleText("cyan", role.id.padEnd(20))} - ${role.description}`;
453
+ console.log(
454
+ line.length > maxLength ? `${line.slice(0, maxLength)}...` : line,
455
+ );
456
+ }
457
+ }
458
+ cli.prompt();
459
+ return;
460
+ }
461
+
462
+ if (inputTrimmed.startsWith("/prompts")) {
463
+ const prompts = await loadPrompts(claudeCodePlugins);
464
+
465
+ if (inputTrimmed === "/prompts") {
466
+ console.log(styleText("bold", "\nAvailable Prompts:"));
467
+ if (prompts.size === 0) {
468
+ console.log(" No prompts found.");
469
+ } else {
470
+ for (const prompt of prompts.values()) {
471
+ const maxLength = process.stdout.columns ?? 100;
472
+ const line = ` ${styleText("cyan", prompt.id.padEnd(20))} - ${prompt.description}`;
473
+ console.log(
474
+ line.length > maxLength ? `${line.slice(0, maxLength)}...` : line,
475
+ );
476
+ }
477
+ }
478
+ cli.prompt();
479
+ return;
480
+ }
481
+
482
+ if (inputTrimmed.startsWith("/prompts:")) {
483
+ const match = inputTrimmed.match(/^\/prompts:([^ ]+)(?:\s+(.*))?$/);
484
+ if (!match) {
485
+ console.log(styleText("red", "\nInvalid prompt invocation format."));
486
+ cli.prompt();
487
+ return;
488
+ }
489
+ await invokePrompt(match[1], match[2] || "", `/prompts:${match[1]}`);
490
+ return;
491
+ }
492
+ }
493
+
494
+ if (inputTrimmed.startsWith("/agents:")) {
495
+ const match = inputTrimmed.match(/^\/agents:([^ ]+)(?:\s+(.*))?$/);
496
+ if (!match) {
497
+ console.log(styleText("red", "\nInvalid agent invocation format."));
498
+ cli.prompt();
499
+ return;
500
+ }
501
+ await invokeAgent(match[1], match[2] || "");
502
+ return;
503
+ }
504
+
505
+ if (inputTrimmed.startsWith("/paste")) {
506
+ const prompt = inputTrimmed.slice("/paste".length).trim();
507
+ let clipboard;
508
+ try {
509
+ if (process.platform === "darwin") {
510
+ clipboard = execFileSync("pbpaste", { encoding: "utf8" });
511
+ } else if (process.platform === "linux") {
512
+ clipboard = execFileSync("xsel", ["--clipboard", "--output"], {
513
+ encoding: "utf8",
514
+ });
515
+ } else {
516
+ console.log(
517
+ styleText(
518
+ "red",
519
+ `\nUnsupported platform for /paste: ${process.platform}`,
520
+ ),
521
+ );
522
+ cli.prompt();
523
+ return;
524
+ }
525
+ } catch (e) {
526
+ const errorMessage = e instanceof Error ? e.message : String(e);
527
+ console.log(
528
+ styleText(
529
+ "red",
530
+ `\nFailed to get clipboard content: ${errorMessage}`,
531
+ ),
532
+ );
533
+ cli.prompt();
534
+ return;
535
+ }
536
+
537
+ const combinedInput = prompt ? `${prompt}\n\n${clipboard}` : clipboard;
538
+
539
+ console.log(styleText("gray", "\n<paste>"));
540
+ console.log(combinedInput);
541
+ console.log(styleText("gray", "</paste>"));
542
+
543
+ const messageWithContext = await loadUserMessageContext(combinedInput);
544
+ userEventEmitter.emit("userInput", messageWithContext);
545
+ state.turn = false;
546
+ return;
547
+ }
548
+
549
+ // Handle shortcuts for prompts in shortcuts/ directory
550
+ if (inputTrimmed.startsWith("/")) {
551
+ const match = inputTrimmed.match(/^\/([^ ]+)(?:\s+(.*))?$/);
552
+ if (match) {
553
+ const id = match[1];
554
+ const prompts = await loadPrompts(claudeCodePlugins);
555
+ const prompt = prompts.get(id);
556
+
557
+ if (prompt?.isShortcut) {
558
+ await invokePrompt(id, match[2] || "", `/${id}`);
559
+ return;
560
+ }
561
+ }
562
+ }
563
+
564
+ const messageWithContext = await loadUserMessageContext(inputTrimmed);
565
+ userEventEmitter.emit("userInput", messageWithContext);
566
+ state.turn = false;
567
+ }
568
+
569
+ cli.on("line", async (lineInput) => {
570
+ if (!state.turn) {
571
+ console.warn(
572
+ styleText(
573
+ "yellow",
574
+ `\nAgent is working. Ignore input: ${lineInput.trim()}`,
575
+ ),
576
+ );
577
+ return;
578
+ }
579
+
580
+ // Handle multi-line delimiter
581
+ if (lineInput.trim() === '"""') {
582
+ if (state.multiLineBuffer === null) {
583
+ state.multiLineBuffer = [];
584
+ cli.setPrompt(styleText("gray", "... "));
585
+ cli.prompt();
586
+ return;
587
+ }
588
+
589
+ const combined = state.multiLineBuffer.join("\n");
590
+ state.multiLineBuffer = null;
591
+ cli.setPrompt(currentCliPrompt);
592
+
593
+ await processInput(combined);
594
+ return;
595
+ }
596
+
597
+ // Accumulate lines if in multi-line mode
598
+ if (state.multiLineBuffer !== null) {
599
+ state.multiLineBuffer.push(lineInput);
600
+ cli.prompt();
601
+ return;
602
+ }
603
+
604
+ await processInput(lineInput);
605
+ });
606
+
607
+ agentEventEmitter.on("partialMessageContent", (partialContent) => {
608
+ if (partialContent.position === "start") {
609
+ const subagentPrefix = state.subagentName
610
+ ? styleText("cyan", `[${state.subagentName}]\n`)
611
+ : "";
612
+ const partialContentStr = styleText("gray", `<${partialContent.type}>`);
613
+ console.log(`\n${subagentPrefix}${partialContentStr}`);
614
+ }
615
+ if (partialContent.content) {
616
+ if (partialContent.type === "tool_use") {
617
+ process.stdout.write(styleText("gray", partialContent.content));
618
+ } else {
619
+ process.stdout.write(partialContent.content);
620
+ }
621
+ }
622
+ if (partialContent.position === "stop") {
623
+ console.log(styleText("gray", `\n</${partialContent.type}>`));
624
+ }
625
+ });
626
+
627
+ agentEventEmitter.on("message", (message) => {
628
+ // Skip user message
629
+ if (state.turn) {
630
+ return;
631
+ }
632
+ printMessage(message);
633
+ });
634
+
635
+ agentEventEmitter.on("toolUseRequest", () => {
636
+ cli.setPrompt(
637
+ [
638
+ styleText(
639
+ "yellow",
640
+ "\nApprove tool calls? (y = allow once, Y = allow in this session, or feedback)",
641
+ ),
642
+ currentCliPrompt,
643
+ ].join("\n"),
644
+ );
645
+ });
646
+
647
+ agentEventEmitter.on("subagentSwitched", (subagent) => {
648
+ state.subagentName = subagent?.name ?? "";
649
+ currentCliPrompt = getCliPrompt(state.subagentName);
650
+ cli.setPrompt(currentCliPrompt);
651
+ });
652
+
653
+ agentEventEmitter.on("providerTokenUsage", (usage) => {
654
+ console.log(formatProviderTokenUsage(usage));
655
+ });
656
+
657
+ agentEventEmitter.on("error", (error) => {
658
+ console.log(
659
+ styleText(
660
+ "red",
661
+ `\nError: message=${error.message}, stack=${error.stack}`,
662
+ ),
663
+ );
664
+ });
665
+
666
+ agentEventEmitter.on("turnEnd", async () => {
667
+ const err = notify(notifyCmd);
668
+ if (err) {
669
+ console.error(
670
+ styleText("yellow", `\nNotification error: ${err.message}`),
671
+ );
672
+ }
673
+ // 暫定対応: token usageのconsole出力を確実にflushするため、次のevent loop tickまで遅延
674
+ await new Promise((resolve) => setTimeout(resolve, 0));
675
+
676
+ state.turn = true;
677
+ cli.prompt();
678
+ });
679
+
680
+ cli.prompt();
681
+ }
682
+
683
+ /**
684
+ * @param {Message} message
685
+ */
686
+ function printMessage(message) {
687
+ switch (message.role) {
688
+ case "assistant": {
689
+ // console.log(styleText("bold", "\nAgent:"));
690
+ for (const part of message.content) {
691
+ switch (part.type) {
692
+ // Note: Streamで表示するためここでは表示しない
693
+ // case "thinking":
694
+ // console.log(
695
+ // [
696
+ // styleText("blue", "<thinking>"),
697
+ // part.thinking,
698
+ // styleText("blue", "</thinking>\n"),
699
+ // ].join("\n"),
700
+ // );
701
+ // break;
702
+ // case "text":
703
+ // console.log(part.text);
704
+ // break;
705
+ case "tool_use":
706
+ console.log(styleText("bold", "\nTool call:"));
707
+ console.log(formatToolUse(part));
708
+ break;
709
+ }
710
+ }
711
+ break;
712
+ }
713
+ case "user": {
714
+ for (const part of message.content) {
715
+ switch (part.type) {
716
+ case "tool_result": {
717
+ console.log(styleText("bold", "\nTool result:"));
718
+ console.log(formatToolResult(part));
719
+ break;
720
+ }
721
+ case "text": {
722
+ console.log(styleText("bold", "\nUser:"));
723
+ console.log(part.text);
724
+ break;
725
+ }
726
+ default: {
727
+ console.log(styleText("bold", "\nUnknown Message Format:"));
728
+ console.log(JSON.stringify(part, null, 2));
729
+ }
730
+ }
731
+ }
732
+ break;
733
+ }
734
+ default: {
735
+ console.log(styleText("bold", "\nUnknown Message Format:"));
736
+ console.log(JSON.stringify(message, null, 2));
737
+ }
738
+ }
739
+ }