@iinm/plain-agent 1.6.0 → 1.7.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.
@@ -1,164 +1,22 @@
1
1
  /**
2
- * @import { Message } from "./model"
3
2
  * @import { UserEventEmitter, AgentEventEmitter, AgentCommands } from "./agent"
4
3
  * @import { ClaudeCodePlugin } from "./claudeCodePlugin.mjs"
5
4
  */
6
5
 
7
- import { execFileSync } from "node:child_process";
8
6
  import readline from "node:readline";
9
- import { Transform } from "node:stream";
10
7
  import { styleText } from "node:util";
8
+ import { createCommandHandler } from "./cliCommands.mjs";
9
+ import { createCompleter, SLASH_COMMANDS } from "./cliCompleter.mjs";
11
10
  import {
12
11
  formatCostSummary,
13
12
  formatProviderTokenUsage,
14
- formatToolResult,
15
- formatToolUse,
13
+ printMessage,
16
14
  } from "./cliFormatter.mjs";
17
- import { consumeInterruptMessage } from "./context/consumeInterruptMessage.mjs";
18
- import { loadAgentRoles } from "./context/loadAgentRoles.mjs";
19
- import { loadPrompts } from "./context/loadPrompts.mjs";
20
- import { loadUserMessageContext } from "./context/loadUserMessageContext.mjs";
15
+ import {
16
+ createPasteTransform,
17
+ resolvePastePlaceholders,
18
+ } from "./cliPasteTransform.mjs";
21
19
  import { notify } from "./utils/notify.mjs";
22
- import { parseFileRange } from "./utils/parseFileRange.mjs";
23
- import { readFileRange } from "./utils/readFileRange.mjs";
24
-
25
- // Define available slash commands for tab completion
26
- const SLASH_COMMANDS = [
27
- { name: "/help", description: "Display this help message" },
28
- { name: "/agents", description: "List available agent roles" },
29
- {
30
- name: "/agents:<id>",
31
- description:
32
- "Delegate to an agent with the given ID (e.g., /agents:code-simplifier)",
33
- },
34
- { name: "/prompts", description: "List available prompts" },
35
- {
36
- name: "/prompts:<id>",
37
- description:
38
- "Invoke a prompt with the given ID (e.g., /prompts:feature-dev)",
39
- },
40
- {
41
- name: "/<id>",
42
- description:
43
- "Shortcut for prompts in the shortcuts/ directory (e.g., /commit)",
44
- },
45
- { name: "/paste", description: "Paste content from clipboard" },
46
- {
47
- name: "/resume",
48
- description: "Resume conversation after an LLM provider error",
49
- },
50
- { name: "/dump", description: "Save current messages to a JSON file" },
51
- { name: "/load", description: "Load messages from a JSON file" },
52
- { name: "/cost", description: "Display session cost and token usage" },
53
- ];
54
-
55
- /**
56
- * @typedef {Object} CompletionCandidate
57
- * @property {string} name
58
- * @property {string} description
59
- */
60
-
61
- /**
62
- * Find candidates that match the line, prioritizing prefix matches.
63
- * @param {(string | CompletionCandidate)[]} candidates
64
- * @param {string} line
65
- * @param {number} queryStartIndex
66
- * @returns {(string | CompletionCandidate)[]}
67
- */
68
- function findMatches(candidates, line, queryStartIndex) {
69
- const query = line.slice(queryStartIndex);
70
- const prefixMatches = [];
71
- const partialMatches = [];
72
-
73
- for (const candidate of candidates) {
74
- const name = typeof candidate === "string" ? candidate : candidate.name;
75
- if (name.startsWith(line)) {
76
- prefixMatches.push(candidate);
77
- } else if (
78
- query.length > 0 &&
79
- name.slice(queryStartIndex).includes(query)
80
- ) {
81
- partialMatches.push(candidate);
82
- }
83
- }
84
-
85
- return [...prefixMatches, ...partialMatches];
86
- }
87
-
88
- /**
89
- * Return the longest common prefix of the given strings.
90
- * @param {string[]} strings
91
- * @returns {string}
92
- */
93
- function commonPrefix(strings) {
94
- if (strings.length === 0) return "";
95
- let prefix = strings[0];
96
- for (let i = 1; i < strings.length; i++) {
97
- while (!strings[i].startsWith(prefix)) {
98
- prefix = prefix.slice(0, -1);
99
- }
100
- }
101
- return prefix;
102
- }
103
-
104
- /**
105
- * Display completion candidates and invoke the readline callback.
106
- *
107
- * Node.js readline normally requires two consecutive Tab presses to show the
108
- * candidate list. This helper lets readline handle the common-prefix
109
- * auto-completion first, then prints the candidate list on the next tick and
110
- * redraws the prompt so the display stays clean.
111
- *
112
- * @param {import("node:readline").Interface} rl
113
- * @param {(string | CompletionCandidate)[]} candidates
114
- * @param {string} line
115
- * @param {(err: Error | null, result: [string[], string]) => void} callback
116
- */
117
- function showCompletions(rl, candidates, line, callback) {
118
- const names = candidates.map((c) => (typeof c === "string" ? c : c.name));
119
- if (candidates.length <= 1) {
120
- callback(null, [names, line]);
121
- return;
122
- }
123
- const prefix = commonPrefix(names);
124
- if (prefix.length > line.length) {
125
- // Let readline insert the common prefix.
126
- callback(null, [[prefix], line]);
127
- } else {
128
- // Nothing new to insert.
129
- callback(null, [[], line]);
130
- }
131
- // After readline finishes its own refresh, print the candidate list and
132
- // redraw the prompt line. We cannot use rl.prompt(true) because its
133
- // internal _refreshLine clears everything below the prompt start, which
134
- // erases the candidate list we just wrote. Instead we manually re-output
135
- // the prompt and current line content.
136
- setTimeout(() => {
137
- const maxLength = process.stdout.columns ?? 100;
138
- const list = candidates
139
- .map((c) => {
140
- if (typeof c === "string") return c;
141
- const nameText = c.name.padEnd(25);
142
- const separator = " - ";
143
- const descText = c.description;
144
-
145
- // 画面幅に合わせて説明文をカット(色を付ける前に計算)
146
- const availableWidth =
147
- maxLength - nameText.length - separator.length - 3;
148
- const displayDesc =
149
- descText.length > availableWidth && availableWidth > 0
150
- ? `${descText.slice(0, availableWidth)}...`
151
- : descText;
152
-
153
- const name = styleText("cyan", nameText);
154
- const description = styleText("dim", displayDesc);
155
- return `${name}${separator}${description}`;
156
- })
157
- .join("\r\n");
158
- process.stdout.write(`\r\n${list}\r\n`);
159
- process.stdout.write(`${rl.getPrompt()}${rl.line}`);
160
- }, 0);
161
- }
162
20
 
163
21
  const HELP_MESSAGE = [
164
22
  "Commands:",
@@ -190,57 +48,6 @@ const HELP_MESSAGE = [
190
48
  .replace(/^ {2}\/.+?(?= - )/gm, (m) => styleText("cyan", m))
191
49
  .replace(/^ {2}.+?(?= - )/gm, (m) => styleText("blue", m));
192
50
 
193
- // Bracketed paste mode sequences
194
- const BRACKETED_PASTE_START = "\x1b[200~";
195
- const BRACKETED_PASTE_END = "\x1b[201~";
196
-
197
- // Store for pasted content
198
- const pastedContentStore = new Map();
199
-
200
- /**
201
- * Generate a short hash for paste reference
202
- * @param {string} content
203
- * @returns {string}
204
- */
205
- function generatePasteHash(content) {
206
- let hash = 0;
207
- for (let i = 0; i < content.length; i++) {
208
- const char = content.charCodeAt(i);
209
- hash = (hash << 5) - hash + char;
210
- hash = hash & hash; // Convert to 32bit integer
211
- }
212
- return Math.abs(hash).toString(16).padStart(6, "0").slice(0, 6);
213
- }
214
-
215
- /**
216
- * Resolve paste placeholders and append context tags
217
- * @param {string} input
218
- * @returns {string}
219
- */
220
- function resolvePastePlaceholders(input) {
221
- /** @type {string[]} */
222
- const contexts = [];
223
-
224
- // Collect paste content for context tags while keeping placeholders
225
- const text = input.replace(/\[pasted#([a-f0-9]{6})\]/g, (match, hash) => {
226
- const content = pastedContentStore.get(hash);
227
- if (content !== undefined) {
228
- pastedContentStore.delete(hash); // Clean up after use
229
- contexts.push(
230
- `<context location="pasted#${hash}">\n${content}\n</context>`,
231
- );
232
- }
233
- return match; // Keep placeholder in text
234
- });
235
-
236
- // Append contexts to the end of input
237
- if (contexts.length > 0) {
238
- return [text, ...contexts].join("\n\n");
239
- }
240
-
241
- return text;
242
- }
243
-
244
51
  /**
245
52
  * @typedef {object} CliOptions
246
53
  * @property {UserEventEmitter} userEventEmitter
@@ -275,61 +82,6 @@ export function startInteractiveSession({
275
82
  subagentName: "",
276
83
  };
277
84
 
278
- /**
279
- * @param {string} id
280
- * @param {string} goal
281
- * @returns {Promise<void>}
282
- */
283
- async function invokeAgent(id, goal) {
284
- const agentRoles = await loadAgentRoles(claudeCodePlugins);
285
- const agent = agentRoles.get(id);
286
- const name = agent ? id : `custom:${id}`;
287
-
288
- const [goalTextContent, ...goalImages] = await loadUserMessageContext(goal);
289
- const goalText =
290
- goalTextContent?.type === "text" ? goalTextContent.text : goal;
291
-
292
- const messageText = `Delegate to "${name}" agent with goal: ${goalText}`;
293
- userEventEmitter.emit("userInput", [
294
- { type: "text", text: messageText },
295
- ...goalImages,
296
- ]);
297
- }
298
-
299
- /**
300
- * @param {string} id
301
- * @param {string} args
302
- * @param {string} displayInvocation
303
- * @returns {Promise<void>}
304
- */
305
- async function invokePrompt(id, args, displayInvocation) {
306
- const prompts = await loadPrompts(claudeCodePlugins);
307
- const prompt = prompts.get(id);
308
-
309
- if (!prompt) {
310
- console.log(styleText("red", `\nPrompt not found: ${id}`));
311
- state.turn = true;
312
- cli.prompt();
313
- return;
314
- }
315
-
316
- const [argsTextContent, ...argsImages] = args
317
- ? await loadUserMessageContext(args)
318
- : [];
319
- const argsText =
320
- argsTextContent?.type === "text" ? argsTextContent.text : args;
321
-
322
- const invocation = `${displayInvocation}${argsText ? ` ${argsText}` : ""}`;
323
- const message = prompt.isSkill
324
- ? `System: This prompt was invoked as "${invocation}".\nPrompt path: ${prompt.filePath}\n\n${prompt.content}`
325
- : `System: This prompt was invoked as "${invocation}".\n\n${prompt.content}`;
326
-
327
- userEventEmitter.emit("userInput", [
328
- { type: "text", text: message },
329
- ...argsImages,
330
- ]);
331
- }
332
-
333
85
  const getCliPrompt = (subagentName = "") =>
334
86
  [
335
87
  "",
@@ -343,77 +95,55 @@ export function startInteractiveSession({
343
95
  "> ",
344
96
  ].join("\n");
345
97
 
346
- // Indirect reference for exit handler (assigned after confirmExit is defined)
347
- let onExitRequest = () => {};
98
+ // Cleanup handler to disable bracketed paste mode on exit
99
+ const cleanup = () => {
100
+ if (process.stdout.isTTY) {
101
+ process.stdout.write("\x1b[?2004l");
102
+ }
103
+ };
348
104
 
349
- // Create a transform stream to handle bracketed paste before readline
350
- let inPasteMode = false;
351
- let pasteBuffer = "";
352
-
353
- const pasteTransform = new Transform({
354
- transform(chunk, _encoding, callback) {
355
- let data = chunk.toString("utf8");
356
-
357
- // Handle Ctrl-C and Ctrl-D
358
- if (data.includes("\x03") || data.includes("\x04")) {
359
- // Ctrl-C / Ctrl-D: request exit (handled by confirmExit)
360
- onExitRequest();
361
- callback();
362
- return;
363
- }
105
+ // Handle exit signals
106
+ let isExiting = false;
107
+ const handleExit = async () => {
108
+ if (isExiting) return;
109
+ isExiting = true;
364
110
 
365
- while (data.length > 0) {
366
- if (inPasteMode) {
367
- const endIdx = data.indexOf(BRACKETED_PASTE_END);
368
- if (endIdx !== -1) {
369
- // End of paste
370
- pasteBuffer += data.slice(0, endIdx);
371
- data = data.slice(endIdx + BRACKETED_PASTE_END.length);
372
- inPasteMode = false;
373
-
374
- // Handle paste content
375
- if (pasteBuffer) {
376
- // Remove trailing newline for single-line paste detection
377
- const trimmedPaste = pasteBuffer.replace(/\n$/, "");
378
-
379
- // For single-line paste, insert directly without placeholder
380
- if (!trimmedPaste.includes("\n")) {
381
- this.push(trimmedPaste);
382
- } else {
383
- // For multi-line paste, use placeholder
384
- const hash = generatePasteHash(pasteBuffer);
385
- pastedContentStore.set(hash, pasteBuffer);
386
- this.push(`[pasted#${hash}] `);
387
- }
388
- }
389
- pasteBuffer = "";
390
- } else {
391
- // Still in paste mode
392
- pasteBuffer += data;
393
- data = "";
394
- }
395
- } else {
396
- const startIdx = data.indexOf(BRACKETED_PASTE_START);
397
- if (startIdx !== -1) {
398
- // Start of paste
399
- // Output any data before the paste
400
- if (startIdx > 0) {
401
- this.push(data.slice(0, startIdx));
402
- }
403
- data = data.slice(startIdx + BRACKETED_PASTE_START.length);
404
- inPasteMode = true;
405
- pasteBuffer = "";
406
- } else {
407
- // Normal data
408
- this.push(data);
409
- data = "";
410
- }
411
- }
412
- }
111
+ cleanup();
112
+ const summary = agentCommands.getCostSummary();
113
+ console.log();
114
+ console.log(formatCostSummary(summary));
115
+ await onStop();
116
+ process.exit(0);
117
+ };
413
118
 
414
- callback();
415
- },
416
- });
119
+ // Double-press exit confirmation
120
+ let lastExitAttempt = 0;
121
+ const EXIT_CONFIRM_TIMEOUT = 1500;
122
+
123
+ const handleCtrlC = () => {
124
+ // If agent is running, pause auto-approve instead of exiting
125
+ if (!state.turn) {
126
+ agentCommands.pauseAutoApprove();
127
+ console.log(
128
+ styleText(
129
+ "yellow",
130
+ "\n⚠ Ctrl-C: Auto-approve paused. Finishing current tool...",
131
+ ),
132
+ );
133
+ return;
134
+ }
135
+
136
+ const now = Date.now();
137
+ if (now - lastExitAttempt < EXIT_CONFIRM_TIMEOUT) {
138
+ handleExit();
139
+ return;
140
+ }
141
+ lastExitAttempt = now;
142
+ console.log(styleText("yellow", "\nPress Ctrl-C or Ctrl-D again to exit."));
143
+ };
144
+
145
+ // Create a transform stream to handle bracketed paste before readline
146
+ const pasteTransform = createPasteTransform(handleCtrlC);
417
147
 
418
148
  // Set up transformed stdin for readline
419
149
  process.stdin.pipe(pasteTransform);
@@ -424,76 +154,12 @@ export function startInteractiveSession({
424
154
  }
425
155
 
426
156
  let currentCliPrompt = getCliPrompt();
157
+ /** @type {import("node:readline").Interface} */
427
158
  const cli = readline.createInterface({
428
159
  input: pasteTransform,
429
160
  output: process.stdout,
430
161
  prompt: currentCliPrompt,
431
- /**
432
- * @param {string} line
433
- * @param {(err?: Error | null, result?: [string[], string]) => void} callback
434
- */
435
- completer: (line, callback) => {
436
- (async () => {
437
- try {
438
- const prompts = await loadPrompts(claudeCodePlugins);
439
- const agentRoles = await loadAgentRoles(claudeCodePlugins);
440
-
441
- if (line.startsWith("/agents:")) {
442
- const prefix = "/agents:";
443
- const candidates = Array.from(agentRoles.values()).map((a) => ({
444
- name: `${prefix}${a.id}`,
445
- description: a.description,
446
- }));
447
- const hits = findMatches(candidates, line, prefix.length);
448
-
449
- showCompletions(cli, hits, line, callback);
450
- return;
451
- }
452
-
453
- if (line.startsWith("/prompts:")) {
454
- const prefix = "/prompts:";
455
- const candidates = Array.from(prompts.values()).map((p) => ({
456
- name: `${prefix}${p.id}`,
457
- description: p.description,
458
- }));
459
- const hits = findMatches(candidates, line, prefix.length);
460
-
461
- showCompletions(cli, hits, line, callback);
462
- return;
463
- }
464
-
465
- if (line.startsWith("/")) {
466
- const shortcuts = Array.from(prompts.values())
467
- .filter((p) => p.isShortcut)
468
- .map((p) => ({
469
- name: `/${p.id}`,
470
- description: p.description,
471
- }));
472
-
473
- const allCommands = [...SLASH_COMMANDS, ...shortcuts].filter(
474
- (cmd) => {
475
- const name = typeof cmd === "string" ? cmd : cmd.name;
476
- return (
477
- name !== "/<id>" &&
478
- (name === "/agents:" || !name.startsWith("/agent:")) &&
479
- (name === "/prompts:" || !name.startsWith("/prompt:"))
480
- );
481
- },
482
- );
483
-
484
- const hits = findMatches(allCommands, line, 1);
485
-
486
- showCompletions(cli, hits, line, callback);
487
- return;
488
- }
489
-
490
- callback(null, [[], line]);
491
- } catch (err) {
492
- const error = err instanceof Error ? err : new Error(String(err));
493
- callback(error, [[], line]);
494
- }
495
- })();
496
- },
162
+ completer: createCompleter(() => cli, claudeCodePlugins),
497
163
  });
498
164
 
499
165
  // Disable automatic prompt redraw on resize during agent turn
@@ -512,47 +178,17 @@ export function startInteractiveSession({
512
178
  if (process.stdin.isTTY) {
513
179
  process.stdin.setRawMode(true);
514
180
  }
515
- // Cleanup handler to disable bracketed paste mode on exit
516
- const cleanup = () => {
517
- if (process.stdout.isTTY) {
518
- process.stdout.write("\x1b[?2004l");
519
- }
520
- };
521
-
522
- // Handle exit signals
523
- let isExiting = false;
524
- const handleExit = async () => {
525
- if (isExiting) return;
526
- isExiting = true;
527
-
528
- cleanup();
529
- const summary = agentCommands.getCostSummary();
530
- console.log();
531
- console.log(formatCostSummary(summary));
532
- await onStop();
533
- process.exit(0);
534
- };
535
-
536
- // Double-press exit confirmation
537
- let lastExitAttempt = 0;
538
- const EXIT_CONFIRM_TIMEOUT = 1500;
539
-
540
- const confirmExit = () => {
541
- const now = Date.now();
542
- if (now - lastExitAttempt < EXIT_CONFIRM_TIMEOUT) {
543
- handleExit();
544
- return;
545
- }
546
- lastExitAttempt = now;
547
- console.log(styleText("yellow", "\nPress Ctrl-C or Ctrl-D again to exit."));
548
- };
549
-
550
- // Wire up exit request handler for Ctrl-C / Ctrl-D
551
- onExitRequest = confirmExit;
552
181
 
553
182
  // Handle readline close (e.g., stdin closed externally)
554
183
  cli.on("close", handleExit);
555
184
 
185
+ const handleCommand = createCommandHandler({
186
+ agentCommands,
187
+ userEventEmitter,
188
+ claudeCodePlugins,
189
+ helpMessage: HELP_MESSAGE,
190
+ });
191
+
556
192
  /**
557
193
  * Process the complete user input.
558
194
  * @param {string} input
@@ -573,184 +209,12 @@ export function startInteractiveSession({
573
209
  }
574
210
 
575
211
  cli.setPrompt(currentCliPrompt);
576
- await consumeInterruptMessage();
577
-
578
- if (["/help", "help"].includes(inputTrimmed.toLowerCase())) {
579
- console.log(`\n${HELP_MESSAGE}`);
580
- state.turn = true;
581
- cli.prompt();
582
- return;
583
- }
584
-
585
- if (inputTrimmed.startsWith("!")) {
586
- const fileRange = parseFileRange(inputTrimmed.slice(1));
587
- if (fileRange instanceof Error) {
588
- console.log(styleText("red", `\n${fileRange.message}`));
589
- state.turn = true;
590
- cli.prompt();
591
- return;
592
- }
593
-
594
- const fileContent = await readFileRange(fileRange);
595
- if (fileContent instanceof Error) {
596
- console.log(styleText("red", `\n${fileContent.message}`));
597
- state.turn = true;
598
- cli.prompt();
599
- return;
600
- }
601
-
602
- const messageWithContext = await loadUserMessageContext(fileContent);
603
-
604
- userEventEmitter.emit("userInput", messageWithContext);
605
- return;
606
- }
607
-
608
- if (inputTrimmed.toLowerCase() === "/dump") {
609
- await agentCommands.dumpMessages();
610
- state.turn = true;
611
- cli.prompt();
612
- return;
613
- }
614
-
615
- if (inputTrimmed.toLowerCase() === "/load") {
616
- await agentCommands.loadMessages();
617
- state.turn = true;
618
- cli.prompt();
619
- return;
620
- }
621
-
622
- if (inputTrimmed.toLowerCase() === "/cost") {
623
- const summary = agentCommands.getCostSummary();
624
- console.log(formatCostSummary(summary));
625
- state.turn = true;
626
- cli.prompt();
627
- return;
628
- }
629
-
630
- if (inputTrimmed === "/agents") {
631
- const agentRoles = await loadAgentRoles(claudeCodePlugins);
632
212
 
633
- console.log(styleText("bold", "\nAvailable Agent Roles:"));
634
- if (agentRoles.size === 0) {
635
- console.log(" No agent roles found.");
636
- } else {
637
- for (const role of agentRoles.values()) {
638
- const maxLength = process.stdout.columns ?? 100;
639
- const line = ` ${styleText("cyan", role.id.padEnd(20))} - ${role.description}`;
640
- console.log(
641
- line.length > maxLength ? `${line.slice(0, maxLength)}...` : line,
642
- );
643
- }
644
- }
213
+ const result = await handleCommand(inputTrimmed);
214
+ if (result === "prompt") {
645
215
  state.turn = true;
646
216
  cli.prompt();
647
- return;
648
- }
649
-
650
- if (inputTrimmed.startsWith("/prompts")) {
651
- const prompts = await loadPrompts(claudeCodePlugins);
652
-
653
- if (inputTrimmed === "/prompts") {
654
- console.log(styleText("bold", "\nAvailable Prompts:"));
655
- if (prompts.size === 0) {
656
- console.log(" No prompts found.");
657
- } else {
658
- for (const prompt of prompts.values()) {
659
- const maxLength = process.stdout.columns ?? 100;
660
- const line = ` ${styleText("cyan", prompt.id.padEnd(20))} - ${prompt.description}`;
661
- console.log(
662
- line.length > maxLength ? `${line.slice(0, maxLength)}...` : line,
663
- );
664
- }
665
- }
666
- state.turn = true;
667
- cli.prompt();
668
- return;
669
- }
670
-
671
- if (inputTrimmed.startsWith("/prompts:")) {
672
- const match = inputTrimmed.match(/^\/prompts:([^ ]+)(?:\s+(.*))?$/s);
673
- if (!match) {
674
- console.log(styleText("red", "\nInvalid prompt invocation format."));
675
- state.turn = true;
676
- cli.prompt();
677
- return;
678
- }
679
- await invokePrompt(match[1], match[2] || "", `/prompts:${match[1]}`);
680
- return;
681
- }
682
- }
683
-
684
- if (inputTrimmed.startsWith("/agents:")) {
685
- const match = inputTrimmed.match(/^\/agents:([^ ]+)(?:\s+(.*))?$/s);
686
- if (!match) {
687
- console.log(styleText("red", "\nInvalid agent invocation format."));
688
- state.turn = true;
689
- cli.prompt();
690
- return;
691
- }
692
- await invokeAgent(match[1], match[2] || "");
693
- return;
694
- }
695
-
696
- if (inputTrimmed.startsWith("/paste")) {
697
- const prompt = inputTrimmed.slice("/paste".length).trim();
698
- let clipboard;
699
- try {
700
- if (process.platform === "darwin") {
701
- clipboard = execFileSync("pbpaste", { encoding: "utf8" });
702
- } else if (process.platform === "linux") {
703
- clipboard = execFileSync("xsel", ["--clipboard", "--output"], {
704
- encoding: "utf8",
705
- });
706
- } else {
707
- console.log(
708
- styleText(
709
- "red",
710
- `\nUnsupported platform for /paste: ${process.platform}`,
711
- ),
712
- );
713
- state.turn = true;
714
- cli.prompt();
715
- return;
716
- }
717
- } catch (e) {
718
- const errorMessage = e instanceof Error ? e.message : String(e);
719
- console.log(
720
- styleText(
721
- "red",
722
- `\nFailed to get clipboard content: ${errorMessage}`,
723
- ),
724
- );
725
- state.turn = true;
726
- cli.prompt();
727
- return;
728
- }
729
-
730
- const combinedInput = prompt ? `${prompt}\n\n${clipboard}` : clipboard;
731
-
732
- const messageWithContext = await loadUserMessageContext(combinedInput);
733
- userEventEmitter.emit("userInput", messageWithContext);
734
- return;
735
- }
736
-
737
- // Handle shortcuts for prompts in shortcuts/ directory
738
- if (inputTrimmed.startsWith("/")) {
739
- const match = inputTrimmed.match(/^\/([^ ]+)(?:\s+(.*))?$/);
740
- if (match) {
741
- const id = match[1];
742
- const prompts = await loadPrompts(claudeCodePlugins);
743
- const prompt = prompts.get(id);
744
-
745
- if (prompt?.isShortcut) {
746
- await invokePrompt(id, match[2] || "", `/${id}`);
747
- return;
748
- }
749
- }
750
217
  }
751
-
752
- const messageWithContext = await loadUserMessageContext(inputTrimmed);
753
- userEventEmitter.emit("userInput", messageWithContext);
754
218
  }
755
219
 
756
220
  cli.on("line", async (lineInput) => {
@@ -866,64 +330,3 @@ export function startInteractiveSession({
866
330
  process.on("exit", cleanup);
867
331
  process.on("SIGTERM", cleanup);
868
332
  }
869
-
870
- /**
871
- * @param {Message} message
872
- */
873
- function printMessage(message) {
874
- switch (message.role) {
875
- case "assistant": {
876
- // console.log(styleText("bold", "\nAgent:"));
877
- for (const part of message.content) {
878
- switch (part.type) {
879
- // Note: Streamで表示するためここでは表示しない
880
- // case "thinking":
881
- // console.log(
882
- // [
883
- // styleText("blue", "<thinking>"),
884
- // part.thinking,
885
- // styleText("blue", "</thinking>\n"),
886
- // ].join("\n"),
887
- // );
888
- // break;
889
- // case "text":
890
- // console.log(part.text);
891
- // break;
892
- case "tool_use":
893
- console.log(styleText("bold", "\nTool call:"));
894
- console.log(formatToolUse(part));
895
- break;
896
- }
897
- }
898
- break;
899
- }
900
- case "user": {
901
- for (const part of message.content) {
902
- switch (part.type) {
903
- case "tool_result": {
904
- console.log(styleText("bold", "\nTool result:"));
905
- console.log(formatToolResult(part));
906
- break;
907
- }
908
- case "text": {
909
- console.log(styleText("bold", "\nUser:"));
910
- console.log(part.text);
911
- break;
912
- }
913
- case "image": {
914
- break;
915
- }
916
- default: {
917
- console.log(styleText("bold", "\nUnknown Message Format:"));
918
- console.log(JSON.stringify(part, null, 2));
919
- }
920
- }
921
- }
922
- break;
923
- }
924
- default: {
925
- console.log(styleText("bold", "\nUnknown Message Format:"));
926
- console.log(JSON.stringify(message, null, 2));
927
- }
928
- }
929
- }