@iinm/plain-agent 1.6.0 → 1.6.1
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/.config/config.predefined.json +6 -6
- package/package.json +4 -4
- package/src/cliCommands.mjs +270 -0
- package/src/cliCompleter.mjs +222 -0
- package/src/cliFormatter.mjs +63 -1
- package/src/cliInteractive.mjs +52 -659
- package/src/cliPasteTransform.mjs +128 -0
- package/src/context/loadAgentRoles.mjs +5 -0
- package/src/context/loadPrompts.mjs +5 -0
- package/src/prompt.mjs +7 -6
- package/src/subagent.mjs +4 -1
package/src/cliInteractive.mjs
CHANGED
|
@@ -1,164 +1,23 @@
|
|
|
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
|
-
|
|
15
|
-
formatToolUse,
|
|
13
|
+
printMessage,
|
|
16
14
|
} from "./cliFormatter.mjs";
|
|
15
|
+
import {
|
|
16
|
+
createPasteTransform,
|
|
17
|
+
resolvePastePlaceholders,
|
|
18
|
+
} from "./cliPasteTransform.mjs";
|
|
17
19
|
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";
|
|
21
20
|
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
21
|
|
|
163
22
|
const HELP_MESSAGE = [
|
|
164
23
|
"Commands:",
|
|
@@ -190,57 +49,6 @@ const HELP_MESSAGE = [
|
|
|
190
49
|
.replace(/^ {2}\/.+?(?= - )/gm, (m) => styleText("cyan", m))
|
|
191
50
|
.replace(/^ {2}.+?(?= - )/gm, (m) => styleText("blue", m));
|
|
192
51
|
|
|
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
52
|
/**
|
|
245
53
|
* @typedef {object} CliOptions
|
|
246
54
|
* @property {UserEventEmitter} userEventEmitter
|
|
@@ -275,61 +83,6 @@ export function startInteractiveSession({
|
|
|
275
83
|
subagentName: "",
|
|
276
84
|
};
|
|
277
85
|
|
|
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
86
|
const getCliPrompt = (subagentName = "") =>
|
|
334
87
|
[
|
|
335
88
|
"",
|
|
@@ -343,175 +96,6 @@ export function startInteractiveSession({
|
|
|
343
96
|
"> ",
|
|
344
97
|
].join("\n");
|
|
345
98
|
|
|
346
|
-
// Indirect reference for exit handler (assigned after confirmExit is defined)
|
|
347
|
-
let onExitRequest = () => {};
|
|
348
|
-
|
|
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
|
-
}
|
|
364
|
-
|
|
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
|
-
}
|
|
413
|
-
|
|
414
|
-
callback();
|
|
415
|
-
},
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
// Set up transformed stdin for readline
|
|
419
|
-
process.stdin.pipe(pasteTransform);
|
|
420
|
-
|
|
421
|
-
// Enable bracketed paste mode
|
|
422
|
-
if (process.stdout.isTTY) {
|
|
423
|
-
process.stdout.write("\x1b[?2004h");
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
let currentCliPrompt = getCliPrompt();
|
|
427
|
-
const cli = readline.createInterface({
|
|
428
|
-
input: pasteTransform,
|
|
429
|
-
output: process.stdout,
|
|
430
|
-
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
|
-
},
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
// Disable automatic prompt redraw on resize during agent turn
|
|
500
|
-
// @ts-expect-error - internal property
|
|
501
|
-
const originalRefreshLine = cli._refreshLine?.bind(cli);
|
|
502
|
-
if (originalRefreshLine) {
|
|
503
|
-
// @ts-expect-error - internal property
|
|
504
|
-
cli._refreshLine = (...args) => {
|
|
505
|
-
if (state.turn) {
|
|
506
|
-
originalRefreshLine(...args);
|
|
507
|
-
}
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
readline.emitKeypressEvents(process.stdin);
|
|
512
|
-
if (process.stdin.isTTY) {
|
|
513
|
-
process.stdin.setRawMode(true);
|
|
514
|
-
}
|
|
515
99
|
// Cleanup handler to disable bracketed paste mode on exit
|
|
516
100
|
const cleanup = () => {
|
|
517
101
|
if (process.stdout.isTTY) {
|
|
@@ -547,12 +131,53 @@ export function startInteractiveSession({
|
|
|
547
131
|
console.log(styleText("yellow", "\nPress Ctrl-C or Ctrl-D again to exit."));
|
|
548
132
|
};
|
|
549
133
|
|
|
550
|
-
//
|
|
551
|
-
|
|
134
|
+
// Create a transform stream to handle bracketed paste before readline
|
|
135
|
+
const pasteTransform = createPasteTransform(confirmExit);
|
|
136
|
+
|
|
137
|
+
// Set up transformed stdin for readline
|
|
138
|
+
process.stdin.pipe(pasteTransform);
|
|
139
|
+
|
|
140
|
+
// Enable bracketed paste mode
|
|
141
|
+
if (process.stdout.isTTY) {
|
|
142
|
+
process.stdout.write("\x1b[?2004h");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let currentCliPrompt = getCliPrompt();
|
|
146
|
+
/** @type {import("node:readline").Interface} */
|
|
147
|
+
const cli = readline.createInterface({
|
|
148
|
+
input: pasteTransform,
|
|
149
|
+
output: process.stdout,
|
|
150
|
+
prompt: currentCliPrompt,
|
|
151
|
+
completer: createCompleter(() => cli, claudeCodePlugins),
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Disable automatic prompt redraw on resize during agent turn
|
|
155
|
+
// @ts-expect-error - internal property
|
|
156
|
+
const originalRefreshLine = cli._refreshLine?.bind(cli);
|
|
157
|
+
if (originalRefreshLine) {
|
|
158
|
+
// @ts-expect-error - internal property
|
|
159
|
+
cli._refreshLine = (...args) => {
|
|
160
|
+
if (state.turn) {
|
|
161
|
+
originalRefreshLine(...args);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
readline.emitKeypressEvents(process.stdin);
|
|
167
|
+
if (process.stdin.isTTY) {
|
|
168
|
+
process.stdin.setRawMode(true);
|
|
169
|
+
}
|
|
552
170
|
|
|
553
171
|
// Handle readline close (e.g., stdin closed externally)
|
|
554
172
|
cli.on("close", handleExit);
|
|
555
173
|
|
|
174
|
+
const handleCommand = createCommandHandler({
|
|
175
|
+
agentCommands,
|
|
176
|
+
userEventEmitter,
|
|
177
|
+
claudeCodePlugins,
|
|
178
|
+
helpMessage: HELP_MESSAGE,
|
|
179
|
+
});
|
|
180
|
+
|
|
556
181
|
/**
|
|
557
182
|
* Process the complete user input.
|
|
558
183
|
* @param {string} input
|
|
@@ -575,182 +200,11 @@ export function startInteractiveSession({
|
|
|
575
200
|
cli.setPrompt(currentCliPrompt);
|
|
576
201
|
await consumeInterruptMessage();
|
|
577
202
|
|
|
578
|
-
|
|
579
|
-
|
|
203
|
+
const result = await handleCommand(inputTrimmed);
|
|
204
|
+
if (result === "prompt") {
|
|
580
205
|
state.turn = true;
|
|
581
206
|
cli.prompt();
|
|
582
|
-
return;
|
|
583
207
|
}
|
|
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
|
-
|
|
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
|
-
}
|
|
645
|
-
state.turn = true;
|
|
646
|
-
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
|
-
}
|
|
751
|
-
|
|
752
|
-
const messageWithContext = await loadUserMessageContext(inputTrimmed);
|
|
753
|
-
userEventEmitter.emit("userInput", messageWithContext);
|
|
754
208
|
}
|
|
755
209
|
|
|
756
210
|
cli.on("line", async (lineInput) => {
|
|
@@ -866,64 +320,3 @@ export function startInteractiveSession({
|
|
|
866
320
|
process.on("exit", cleanup);
|
|
867
321
|
process.on("SIGTERM", cleanup);
|
|
868
322
|
}
|
|
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
|
-
}
|