@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.
@@ -0,0 +1,222 @@
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
+
39
+ /**
40
+ * @typedef {Object} CompletionCandidate
41
+ * @property {string} name
42
+ * @property {string} description
43
+ */
44
+
45
+ /**
46
+ * Find candidates that match the line, prioritizing prefix matches.
47
+ * @param {(string | CompletionCandidate)[]} candidates
48
+ * @param {string} line
49
+ * @param {number} queryStartIndex
50
+ * @returns {(string | CompletionCandidate)[]}
51
+ */
52
+ function findMatches(candidates, line, queryStartIndex) {
53
+ const query = line.slice(queryStartIndex);
54
+ const prefixMatches = [];
55
+ const partialMatches = [];
56
+
57
+ for (const candidate of candidates) {
58
+ const name = typeof candidate === "string" ? candidate : candidate.name;
59
+ if (name.startsWith(line)) {
60
+ prefixMatches.push(candidate);
61
+ } else if (
62
+ query.length > 0 &&
63
+ name.slice(queryStartIndex).includes(query)
64
+ ) {
65
+ partialMatches.push(candidate);
66
+ }
67
+ }
68
+
69
+ return [...prefixMatches, ...partialMatches];
70
+ }
71
+
72
+ /**
73
+ * Return the longest common prefix of the given strings.
74
+ * @param {string[]} strings
75
+ * @returns {string}
76
+ */
77
+ function commonPrefix(strings) {
78
+ if (strings.length === 0) return "";
79
+ let prefix = strings[0];
80
+ for (let i = 1; i < strings.length; i++) {
81
+ while (!strings[i].startsWith(prefix)) {
82
+ prefix = prefix.slice(0, -1);
83
+ }
84
+ }
85
+ return prefix;
86
+ }
87
+
88
+ /**
89
+ * Display completion candidates and invoke the readline callback.
90
+ *
91
+ * Node.js readline normally requires two consecutive Tab presses to show the
92
+ * candidate list. This helper lets readline handle the common-prefix
93
+ * auto-completion first, then prints the candidate list on the next tick and
94
+ * redraws the prompt so the display stays clean.
95
+ *
96
+ * @param {import("node:readline").Interface} rl
97
+ * @param {(string | CompletionCandidate)[]} candidates
98
+ * @param {string} line
99
+ * @param {(err: Error | null, result: [string[], string]) => void} callback
100
+ */
101
+ function showCompletions(rl, candidates, line, callback) {
102
+ const names = candidates.map((c) => (typeof c === "string" ? c : c.name));
103
+ if (candidates.length <= 1) {
104
+ callback(null, [names, line]);
105
+ return;
106
+ }
107
+ const prefix = commonPrefix(names);
108
+ if (prefix.length > line.length) {
109
+ // Let readline insert the common prefix.
110
+ callback(null, [[prefix], line]);
111
+ } else {
112
+ // Nothing new to insert.
113
+ callback(null, [[], line]);
114
+ }
115
+ // After readline finishes its own refresh, print the candidate list and
116
+ // redraw the prompt line. We cannot use rl.prompt(true) because its
117
+ // internal _refreshLine clears everything below the prompt start, which
118
+ // erases the candidate list we just wrote. Instead we manually re-output
119
+ // the prompt and current line content.
120
+ setTimeout(() => {
121
+ const maxLength = process.stdout.columns ?? 100;
122
+ const list = candidates
123
+ .map((c) => {
124
+ if (typeof c === "string") return c;
125
+ const nameText = c.name.padEnd(25);
126
+ const separator = " - ";
127
+ const descText = c.description;
128
+
129
+ // 画面幅に合わせて説明文をカット(色を付ける前に計算)
130
+ const availableWidth =
131
+ maxLength - nameText.length - separator.length - 3;
132
+ const displayDesc =
133
+ descText.length > availableWidth && availableWidth > 0
134
+ ? `${descText.slice(0, availableWidth)}...`
135
+ : descText;
136
+
137
+ const name = styleText("cyan", nameText);
138
+ const description = styleText("dim", displayDesc);
139
+ return `${name}${separator}${description}`;
140
+ })
141
+ .join("\r\n");
142
+ process.stdout.write(`\r\n${list}\r\n`);
143
+ process.stdout.write(`${rl.getPrompt()}${rl.line}`);
144
+ }, 0);
145
+ }
146
+
147
+ /**
148
+ * Create a completer function for readline.
149
+ *
150
+ * Because the readline.Interface instance (`cli`) is not available until after
151
+ * `readline.createInterface` returns, we accept a getter function so the
152
+ * completer can resolve the reference lazily at call time.
153
+ *
154
+ * @param {() => import("node:readline").Interface} getCliRef - A function that returns the readline Interface
155
+ * @param {ClaudeCodePlugin[] | undefined} claudeCodePlugins
156
+ * @returns {(line: string, callback: (err?: Error | null, result?: [string[], string]) => void) => void}
157
+ */
158
+ export function createCompleter(getCliRef, claudeCodePlugins) {
159
+ return (line, callback) => {
160
+ (async () => {
161
+ try {
162
+ const cli = getCliRef();
163
+ const prompts = await loadPrompts(claudeCodePlugins);
164
+ const agentRoles = await loadAgentRoles(claudeCodePlugins);
165
+
166
+ if (line.startsWith("/agents:")) {
167
+ const prefix = "/agents:";
168
+ const candidates = Array.from(agentRoles.values()).map((a) => ({
169
+ name: `${prefix}${a.id}`,
170
+ description: a.description,
171
+ }));
172
+ const hits = findMatches(candidates, line, prefix.length);
173
+
174
+ showCompletions(cli, hits, line, callback);
175
+ return;
176
+ }
177
+
178
+ if (line.startsWith("/prompts:")) {
179
+ const prefix = "/prompts:";
180
+ const candidates = Array.from(prompts.values()).map((p) => ({
181
+ name: `${prefix}${p.id}`,
182
+ description: p.description,
183
+ }));
184
+ const hits = findMatches(candidates, line, prefix.length);
185
+
186
+ showCompletions(cli, hits, line, callback);
187
+ return;
188
+ }
189
+
190
+ if (line.startsWith("/")) {
191
+ const shortcuts = Array.from(prompts.values())
192
+ .filter((p) => p.isShortcut)
193
+ .map((p) => ({
194
+ name: `/${p.id}`,
195
+ description: p.description,
196
+ }));
197
+
198
+ const allCommands = [...SLASH_COMMANDS, ...shortcuts].filter(
199
+ (cmd) => {
200
+ const name = typeof cmd === "string" ? cmd : cmd.name;
201
+ return (
202
+ name !== "/<id>" &&
203
+ (name === "/agents:" || !name.startsWith("/agents:")) &&
204
+ (name === "/prompts:" || !name.startsWith("/prompts:"))
205
+ );
206
+ },
207
+ );
208
+
209
+ const hits = findMatches(allCommands, line, 1);
210
+
211
+ showCompletions(cli, hits, line, callback);
212
+ return;
213
+ }
214
+
215
+ callback(null, [[], line]);
216
+ } catch (err) {
217
+ const error = err instanceof Error ? err : new Error(String(err));
218
+ callback(error, [[], line]);
219
+ }
220
+ })();
221
+ };
222
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @import { MessageContentToolUse, MessageContentToolResult, ProviderTokenUsage } from "./model"
2
+ * @import { Message, MessageContentToolUse, MessageContentToolResult, ProviderTokenUsage } from "./model"
3
3
  * @import { ExecCommandInput } from "./tools/execCommand"
4
4
  * @import { PatchFileInput } from "./tools/patchFile"
5
5
  * @import { WriteFileInput } from "./tools/writeFile"
@@ -266,3 +266,65 @@ export function formatCostForBatch(summary) {
266
266
  ),
267
267
  };
268
268
  }
269
+
270
+ /**
271
+ * Print a message to the console.
272
+ * @param {Message} message
273
+ */
274
+ export function printMessage(message) {
275
+ switch (message.role) {
276
+ case "assistant": {
277
+ // console.log(styleText("bold", "\nAgent:"));
278
+ for (const part of message.content) {
279
+ switch (part.type) {
280
+ // Note: Streamで表示するためここでは表示しない
281
+ // case "thinking":
282
+ // console.log(
283
+ // [
284
+ // styleText("blue", "<thinking>"),
285
+ // part.thinking,
286
+ // styleText("blue", "</thinking>\n"),
287
+ // ].join("\n"),
288
+ // );
289
+ // break;
290
+ // case "text":
291
+ // console.log(part.text);
292
+ // break;
293
+ case "tool_use":
294
+ console.log(styleText("bold", "\nTool call:"));
295
+ console.log(formatToolUse(part));
296
+ break;
297
+ }
298
+ }
299
+ break;
300
+ }
301
+ case "user": {
302
+ for (const part of message.content) {
303
+ switch (part.type) {
304
+ case "tool_result": {
305
+ console.log(styleText("bold", "\nTool result:"));
306
+ console.log(formatToolResult(part));
307
+ break;
308
+ }
309
+ case "text": {
310
+ console.log(styleText("bold", "\nUser:"));
311
+ console.log(part.text);
312
+ break;
313
+ }
314
+ case "image": {
315
+ break;
316
+ }
317
+ default: {
318
+ console.log(styleText("bold", "\nUnknown Message Format:"));
319
+ console.log(JSON.stringify(part, null, 2));
320
+ }
321
+ }
322
+ }
323
+ break;
324
+ }
325
+ default: {
326
+ console.log(styleText("bold", "\nUnknown Message Format:"));
327
+ console.log(JSON.stringify(message, null, 2));
328
+ }
329
+ }
330
+ }