@iinm/plain-agent 1.8.4 → 1.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/bin/plain +1 -1
  2. package/package.json +8 -9
  3. package/sandbox/bin/plain-sandbox +13 -0
  4. package/src/agent.d.ts +52 -0
  5. package/src/agent.mjs +204 -0
  6. package/src/agentLoop.mjs +419 -0
  7. package/src/agentState.mjs +41 -0
  8. package/src/claudeCodePlugin.mjs +164 -0
  9. package/src/cliArgs.mjs +175 -0
  10. package/src/cliBatch.mjs +147 -0
  11. package/src/cliCommands.mjs +283 -0
  12. package/src/cliCompleter.mjs +227 -0
  13. package/src/cliCost.mjs +309 -0
  14. package/src/cliFormatter.mjs +518 -0
  15. package/src/cliInteractive.mjs +533 -0
  16. package/src/cliInterruptTransform.mjs +51 -0
  17. package/src/cliMuteTransform.mjs +26 -0
  18. package/src/cliPasteTransform.mjs +183 -0
  19. package/src/config.d.ts +36 -0
  20. package/src/config.mjs +197 -0
  21. package/src/context/loadAgentRoles.mjs +267 -0
  22. package/src/context/loadPrompts.mjs +303 -0
  23. package/src/context/loadUserMessageContext.mjs +147 -0
  24. package/src/costTracker.mjs +210 -0
  25. package/src/env.mjs +44 -0
  26. package/src/main.mjs +281 -0
  27. package/src/mcpClient.mjs +351 -0
  28. package/src/mcpIntegration.mjs +160 -0
  29. package/src/model.d.ts +109 -0
  30. package/src/modelCaller.mjs +32 -0
  31. package/src/modelDefinition.d.ts +92 -0
  32. package/src/prompt.mjs +138 -0
  33. package/src/providers/anthropic.d.ts +248 -0
  34. package/src/providers/anthropic.mjs +587 -0
  35. package/src/providers/bedrock.d.ts +249 -0
  36. package/src/providers/bedrock.mjs +700 -0
  37. package/src/providers/gemini.d.ts +208 -0
  38. package/src/providers/gemini.mjs +754 -0
  39. package/src/providers/openai.d.ts +281 -0
  40. package/src/providers/openai.mjs +544 -0
  41. package/src/providers/openaiCompatible.d.ts +147 -0
  42. package/src/providers/openaiCompatible.mjs +652 -0
  43. package/src/providers/platform/awsSigV4.mjs +184 -0
  44. package/src/providers/platform/azure.mjs +42 -0
  45. package/src/providers/platform/bedrock.mjs +78 -0
  46. package/src/providers/platform/googleCloud.mjs +34 -0
  47. package/src/subagent.mjs +265 -0
  48. package/src/tmpfile.mjs +27 -0
  49. package/src/tool.d.ts +74 -0
  50. package/src/toolExecutor.mjs +236 -0
  51. package/src/toolInputValidator.mjs +183 -0
  52. package/src/toolUseApprover.mjs +99 -0
  53. package/src/tools/askURL.mjs +209 -0
  54. package/src/tools/askWeb.mjs +208 -0
  55. package/src/tools/compactContext.d.ts +4 -0
  56. package/src/tools/compactContext.mjs +87 -0
  57. package/src/tools/execCommand.d.ts +22 -0
  58. package/src/tools/execCommand.mjs +200 -0
  59. package/src/tools/patchFile.d.ts +4 -0
  60. package/src/tools/patchFile.mjs +133 -0
  61. package/src/tools/switchToMainAgent.d.ts +3 -0
  62. package/src/tools/switchToMainAgent.mjs +43 -0
  63. package/src/tools/switchToSubagent.d.ts +4 -0
  64. package/src/tools/switchToSubagent.mjs +59 -0
  65. package/src/tools/tmuxCommand.d.ts +14 -0
  66. package/src/tools/tmuxCommand.mjs +194 -0
  67. package/src/tools/writeFile.d.ts +4 -0
  68. package/src/tools/writeFile.mjs +56 -0
  69. package/src/usageStore.mjs +167 -0
  70. package/src/utils/evalJSONConfig.mjs +72 -0
  71. package/src/utils/matchValue.d.ts +6 -0
  72. package/src/utils/matchValue.mjs +40 -0
  73. package/src/utils/noThrow.mjs +31 -0
  74. package/src/utils/notify.mjs +29 -0
  75. package/src/utils/parseFileRange.mjs +18 -0
  76. package/src/utils/parseFrontmatter.mjs +19 -0
  77. package/src/utils/readFileRange.mjs +33 -0
  78. package/src/utils/retryOnError.mjs +41 -0
  79. package/src/voiceInput.mjs +61 -0
  80. package/src/voiceInputGemini.mjs +105 -0
  81. package/src/voiceInputOpenAI.mjs +104 -0
  82. package/src/voiceInputSession.mjs +543 -0
  83. package/src/voiceToggleKey.mjs +62 -0
  84. package/dist/main.mjs +0 -473
  85. package/dist/main.mjs.map +0 -7
@@ -0,0 +1,175 @@
1
+ /**
2
+ * @typedef {HelpSubcommand | InteractiveSubcommand | BatchSubcommand | ListModelsSubcommand | InstallClaudeCodePluginsSubcommand | CostSubcommand} Subcommand
3
+ */
4
+
5
+ /**
6
+ * @typedef {{ type: 'help' }} HelpSubcommand
7
+ */
8
+
9
+ /**
10
+ * @typedef {{ type: 'interactive', config: string[], model: string | null }} InteractiveSubcommand
11
+ */
12
+
13
+ /**
14
+ * @typedef {{ type: 'batch', task: string, config: string[], model: string | null }} BatchSubcommand
15
+ */
16
+
17
+ /**
18
+ * @typedef {{ type: 'list-models' }} ListModelsSubcommand
19
+ */
20
+
21
+ /**
22
+ * @typedef {{ type: 'install-claude-code-plugins' }} InstallClaudeCodePluginsSubcommand
23
+ */
24
+
25
+ /**
26
+ * @typedef {{ type: 'cost', from: string | null, to: string | null }} CostSubcommand
27
+ */
28
+
29
+ /**
30
+ * @typedef {Object} CliArgs
31
+ * @property {Subcommand} subcommand - The subcommand to execute
32
+ */
33
+
34
+ /**
35
+ * Parse command-line arguments.
36
+ * @param {string[]} argv - process.argv or similar
37
+ * @returns {CliArgs}
38
+ */
39
+ export function parseCliArgs(argv) {
40
+ const args = argv.slice(2);
41
+ const subcommandName = args[0];
42
+
43
+ if (["-h", "--help", "help"].includes(subcommandName)) {
44
+ return {
45
+ subcommand: { type: "help" },
46
+ };
47
+ }
48
+
49
+ if (!subcommandName || subcommandName.startsWith("-")) {
50
+ // Interactive mode (default)
51
+ const config = [];
52
+ let model = null;
53
+
54
+ for (let i = 0; i < args.length; i++) {
55
+ if (args[i] === "-m" || args[i] === "--model") {
56
+ if (args[i + 1]) {
57
+ model = args[i + 1];
58
+ i++;
59
+ }
60
+ } else if (args[i] === "-c" || args[i] === "--config") {
61
+ if (args[i + 1]) {
62
+ config.push(args[i + 1]);
63
+ i++;
64
+ }
65
+ }
66
+ }
67
+
68
+ return {
69
+ subcommand: { type: "interactive", config, model },
70
+ };
71
+ }
72
+
73
+ if (subcommandName === "batch") {
74
+ const batchArgs = args.slice(1);
75
+
76
+ let task = null;
77
+ let model = null;
78
+ const config = [];
79
+
80
+ for (let i = 0; i < batchArgs.length; i++) {
81
+ if (batchArgs[i] === "-m" || batchArgs[i] === "--model") {
82
+ if (batchArgs[i + 1]) {
83
+ model = batchArgs[i + 1];
84
+ i++;
85
+ }
86
+ } else if (batchArgs[i] === "-c" || batchArgs[i] === "--config") {
87
+ if (batchArgs[i + 1]) {
88
+ config.push(batchArgs[i + 1]);
89
+ i++;
90
+ }
91
+ } else if (!batchArgs[i].startsWith("-") && !task) {
92
+ task = batchArgs[i];
93
+ }
94
+ }
95
+
96
+ return {
97
+ subcommand: { type: "batch", task: task || "", config, model },
98
+ };
99
+ }
100
+
101
+ if (subcommandName === "list-models") {
102
+ return {
103
+ subcommand: { type: "list-models" },
104
+ };
105
+ }
106
+
107
+ if (subcommandName === "install-claude-code-plugins") {
108
+ return {
109
+ subcommand: { type: "install-claude-code-plugins" },
110
+ };
111
+ }
112
+
113
+ if (subcommandName === "cost") {
114
+ const costArgs = args.slice(1);
115
+ let from = null;
116
+ let to = null;
117
+ for (let i = 0; i < costArgs.length; i++) {
118
+ if (costArgs[i] === "--from") {
119
+ if (costArgs[i + 1]) {
120
+ from = costArgs[i + 1];
121
+ i++;
122
+ }
123
+ } else if (costArgs[i] === "--to") {
124
+ if (costArgs[i + 1]) {
125
+ to = costArgs[i + 1];
126
+ i++;
127
+ }
128
+ }
129
+ }
130
+ return {
131
+ subcommand: { type: "cost", from, to },
132
+ };
133
+ }
134
+
135
+ return {
136
+ subcommand: { type: "help" },
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Print help message and exit.
142
+ * @param {number} [exitCode] - Exit code (default: 0)
143
+ */
144
+ export function printHelp(exitCode = 0) {
145
+ console.log(`
146
+ Usage: plain [options]
147
+ plain batch [options] <task>
148
+ plain cost [--from YYYY-MM-DD] [--to YYYY-MM-DD]
149
+ plain list-models
150
+ plain install-claude-code-plugins
151
+
152
+ Options:
153
+ -m, --model <model+variant> Model to use
154
+ -h, --help Show this help message
155
+ -c, --config <file> Config file to load (repeatable)
156
+
157
+ Subcommands:
158
+ batch <task> Run in batch mode with the given task instruction.
159
+ Config files are NOT auto-loaded in batch mode;
160
+ use -c to specify config files explicitly.
161
+ cost Show aggregated token cost per day for a period.
162
+ Defaults to the first day of the current month
163
+ through today.
164
+ list-models List available models
165
+ install-claude-code-plugins Install Claude Code plugins
166
+
167
+ Examples:
168
+ plain -m gpt-5.4+thinking-medium
169
+ plain batch \\
170
+ -c ~/.config/plain-agent/config.local.json \\
171
+ -c .plain-agent/config.json \\
172
+ "Add tests for ..."
173
+ `);
174
+ process.exit(exitCode);
175
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * @import { UserEventEmitter, AgentEventEmitter, AgentCommands } from "./agent"
3
+ */
4
+
5
+ import { formatCostForBatch } from "./cliFormatter.mjs";
6
+ import { appendUsageRecord, buildUsageRecord } from "./usageStore.mjs";
7
+
8
+ /**
9
+ * @typedef {object} BatchSessionOptions
10
+ * @property {UserEventEmitter} userEventEmitter
11
+ * @property {AgentEventEmitter} agentEventEmitter
12
+ * @property {AgentCommands} agentCommands
13
+ * @property {string} task - Task instruction to execute
14
+ * @property {string} sessionId
15
+ * @property {string} modelName
16
+ * @property {boolean} sandbox
17
+ * @property {Date} startTime
18
+ * @property {() => Promise<void>} onStop
19
+ */
20
+
21
+ /**
22
+ * Start a batch session and execute the task.
23
+ * Events are output as JSON Lines (1 line = 1 JSON object).
24
+ *
25
+ * @param {BatchSessionOptions} options
26
+ * @returns {Promise<void>}
27
+ */
28
+ export async function startBatchSession({
29
+ userEventEmitter,
30
+ agentEventEmitter,
31
+ agentCommands,
32
+ task,
33
+ sessionId,
34
+ modelName,
35
+ sandbox,
36
+ startTime,
37
+ onStop,
38
+ }) {
39
+ setupEventHandlers(agentEventEmitter, { sessionId, modelName, sandbox });
40
+
41
+ userEventEmitter.emit("userInput", [{ type: "text", text: task }]);
42
+
43
+ await new Promise((/** @type {(value?: void) => void} */ resolve) => {
44
+ agentEventEmitter.on("turnEnd", async () => {
45
+ const costSummary = agentCommands.getCostSummary();
46
+
47
+ outputEvent({
48
+ type: "session_end",
49
+ timestamp: new Date().toISOString(),
50
+ cost: formatCostForBatch(costSummary),
51
+ });
52
+
53
+ try {
54
+ const record = buildUsageRecord({
55
+ sessionId,
56
+ mode: "batch",
57
+ modelName,
58
+ workingDir: process.cwd(),
59
+ costSummary,
60
+ now: startTime,
61
+ });
62
+ if (record) {
63
+ await appendUsageRecord(record);
64
+ }
65
+ } catch (err) {
66
+ const message = err instanceof Error ? err.message : String(err);
67
+ outputEvent({
68
+ type: "error",
69
+ error: { message: `failed to record usage: ${message}` },
70
+ timestamp: new Date().toISOString(),
71
+ });
72
+ }
73
+
74
+ await onStop();
75
+ resolve();
76
+ });
77
+ });
78
+
79
+ process.exit(0);
80
+ }
81
+
82
+ /**
83
+ * Setup event handlers for batch mode.
84
+ * Output events as JSON Lines.
85
+ *
86
+ * @param {AgentEventEmitter} agentEventEmitter
87
+ * @param {{ sessionId: string, modelName: string, sandbox: boolean }} meta
88
+ */
89
+ function setupEventHandlers(
90
+ agentEventEmitter,
91
+ { sessionId, modelName, sandbox },
92
+ ) {
93
+ outputEvent({
94
+ type: "session_start",
95
+ sessionId,
96
+ modelName,
97
+ sandbox,
98
+ timestamp: new Date().toISOString(),
99
+ });
100
+
101
+ agentEventEmitter.on("message", (message) => {
102
+ outputEvent({
103
+ type: "message",
104
+ message,
105
+ timestamp: new Date().toISOString(),
106
+ });
107
+ });
108
+
109
+ agentEventEmitter.on("error", (error) => {
110
+ outputEvent({
111
+ type: "error",
112
+ error: {
113
+ message: error.message,
114
+ stack: error.stack,
115
+ },
116
+ timestamp: new Date().toISOString(),
117
+ });
118
+
119
+ process.exit(1);
120
+ });
121
+
122
+ agentEventEmitter.on("subagentSwitched", (subagent) => {
123
+ outputEvent({
124
+ type: "subagent_switched",
125
+ subagent,
126
+ timestamp: new Date().toISOString(),
127
+ });
128
+ });
129
+
130
+ agentEventEmitter.on("providerTokenUsage", (usage) => {
131
+ outputEvent({
132
+ type: "token_usage",
133
+ usage,
134
+ timestamp: new Date().toISOString(),
135
+ });
136
+ });
137
+ }
138
+
139
+ /**
140
+ * Output an event as JSON Lines format.
141
+ * Each event is a single line of JSON.
142
+ *
143
+ * @param {object} event
144
+ */
145
+ function outputEvent(event) {
146
+ console.log(JSON.stringify(event));
147
+ }
@@ -0,0 +1,283 @@
1
+ /**
2
+ * @import { UserEventEmitter, AgentCommands } from "./agent"
3
+ * @import { ClaudeCodePlugin } from "./claudeCodePlugin.mjs"
4
+ */
5
+
6
+ import { execFileSync } from "node:child_process";
7
+ import { styleText } from "node:util";
8
+ import { formatCostSummary } from "./cliFormatter.mjs";
9
+ import { loadAgentRoles } from "./context/loadAgentRoles.mjs";
10
+ import { loadPrompts } from "./context/loadPrompts.mjs";
11
+ import { loadUserMessageContext } from "./context/loadUserMessageContext.mjs";
12
+ import { CLAUDE_CODE_COMPATIBILITY_NOTES } from "./prompt.mjs";
13
+ import { parseFileRange } from "./utils/parseFileRange.mjs";
14
+ import { readFileRange } from "./utils/readFileRange.mjs";
15
+
16
+ /**
17
+ * @typedef {"prompt" | "continue"} CommandResult
18
+ * - "prompt": return control to prompt (state.turn = true; cli.prompt())
19
+ * - "continue": agent is now running, do nothing
20
+ */
21
+
22
+ /**
23
+ * @typedef {object} CommandHandlerDeps
24
+ * @property {AgentCommands} agentCommands
25
+ * @property {UserEventEmitter} userEventEmitter
26
+ * @property {ClaudeCodePlugin[] | undefined} claudeCodePlugins
27
+ * @property {string} helpMessage
28
+ */
29
+
30
+ /**
31
+ * Create command handler function for processing slash commands.
32
+ *
33
+ * @param {CommandHandlerDeps} deps
34
+ * @returns {(input: string) => Promise<CommandResult>}
35
+ */
36
+ export function createCommandHandler({
37
+ agentCommands,
38
+ userEventEmitter,
39
+ claudeCodePlugins,
40
+ helpMessage,
41
+ }) {
42
+ /**
43
+ * Invoke an agent with the given id and goal.
44
+ * @param {string} id
45
+ * @param {string} goal
46
+ * @returns {Promise<CommandResult>}
47
+ */
48
+ async function invokeAgent(id, goal) {
49
+ const agentRoles = await loadAgentRoles(claudeCodePlugins);
50
+ const agent = agentRoles.get(id);
51
+ const name = agent ? id : `custom:${id}`;
52
+
53
+ const [goalTextContent, ...goalImages] = await loadUserMessageContext(goal);
54
+ const goalText =
55
+ goalTextContent?.type === "text" ? goalTextContent.text : goal;
56
+
57
+ const messageText = `Switch to "${name}" subagent with goal: ${goalText}`;
58
+ userEventEmitter.emit("userInput", [
59
+ { type: "text", text: messageText },
60
+ ...goalImages,
61
+ ]);
62
+ return "continue";
63
+ }
64
+
65
+ /**
66
+ * Invoke a prompt with the given id, args, and display invocation.
67
+ * @param {string} id
68
+ * @param {string} args
69
+ * @param {string} displayInvocation
70
+ * @returns {Promise<CommandResult>}
71
+ */
72
+ async function invokePrompt(id, args, displayInvocation) {
73
+ const prompts = await loadPrompts(claudeCodePlugins);
74
+ const prompt = prompts.get(id);
75
+
76
+ if (!prompt) {
77
+ console.log(styleText("red", `\nPrompt not found: ${id}`));
78
+ return "prompt";
79
+ }
80
+
81
+ const [argsTextContent, ...argsImages] = args
82
+ ? await loadUserMessageContext(args)
83
+ : [];
84
+ const argsText =
85
+ argsTextContent?.type === "text" ? argsTextContent.text : args;
86
+
87
+ const invocation = `${displayInvocation}${argsText ? ` ${argsText}` : ""}`;
88
+ const promptContent = prompt.claudeOriginated
89
+ ? `${prompt.content}\n\n---\n\n${CLAUDE_CODE_COMPATIBILITY_NOTES}`
90
+ : prompt.content;
91
+ const message = prompt.isSkill
92
+ ? `System: This prompt was invoked as "${invocation}".\nPrompt path: ${prompt.filePath}\n\n${promptContent}`
93
+ : `System: This prompt was invoked as "${invocation}".\n\n${promptContent}`;
94
+
95
+ userEventEmitter.emit("userInput", [
96
+ { type: "text", text: message },
97
+ ...argsImages,
98
+ ]);
99
+ return "continue";
100
+ }
101
+
102
+ /**
103
+ * Handle a complete user input string and return a CommandResult.
104
+ * @param {string} inputTrimmed
105
+ * @returns {Promise<CommandResult>}
106
+ */
107
+ return async function handleCommand(inputTrimmed) {
108
+ // /help or help
109
+ if (["/help", "help"].includes(inputTrimmed.toLowerCase())) {
110
+ console.log(`\n${helpMessage}`);
111
+ return "prompt";
112
+ }
113
+
114
+ // !path — read file content and emit as user input
115
+ if (inputTrimmed.startsWith("!")) {
116
+ const fileRange = parseFileRange(inputTrimmed.slice(1));
117
+ if (fileRange instanceof Error) {
118
+ console.log(styleText("red", `\n${fileRange.message}`));
119
+ return "prompt";
120
+ }
121
+
122
+ const fileContent = await readFileRange(fileRange);
123
+ if (fileContent instanceof Error) {
124
+ console.log(styleText("red", `\n${fileContent.message}`));
125
+ return "prompt";
126
+ }
127
+
128
+ const messageWithContext = await loadUserMessageContext(fileContent);
129
+ userEventEmitter.emit("userInput", messageWithContext);
130
+ return "continue";
131
+ }
132
+
133
+ // /dump
134
+ if (inputTrimmed.toLowerCase() === "/dump") {
135
+ await agentCommands.dumpMessages();
136
+ return "prompt";
137
+ }
138
+
139
+ // /load
140
+ if (inputTrimmed.toLowerCase() === "/load") {
141
+ await agentCommands.loadMessages();
142
+ return "prompt";
143
+ }
144
+
145
+ // /cost
146
+ if (inputTrimmed.toLowerCase() === "/cost") {
147
+ const summary = agentCommands.getCostSummary();
148
+ console.log(formatCostSummary(summary));
149
+ return "prompt";
150
+ }
151
+
152
+ // /compact
153
+ if (inputTrimmed.toLowerCase() === "/compact") {
154
+ const message = [
155
+ 'System: This prompt was invoked as "/compact".',
156
+ "",
157
+ "Compact the conversation context:",
158
+ "1. Update the memory file for the current task so it fully captures the task overview, progress, decisions, and next steps in a self-contained way.",
159
+ '2. Then call the "compact_context" tool alone with that memory file path and a brief reason.',
160
+ ].join("\n");
161
+ userEventEmitter.emit("userInput", [{ type: "text", text: message }]);
162
+ return "continue";
163
+ }
164
+
165
+ // /agents or /agents:id
166
+ if (inputTrimmed === "/agents") {
167
+ const agentRoles = await loadAgentRoles(claudeCodePlugins);
168
+
169
+ console.log(styleText("bold", "\nAvailable Agent Roles:"));
170
+ if (agentRoles.size === 0) {
171
+ console.log(" No agent roles found.");
172
+ } else {
173
+ for (const role of agentRoles.values()) {
174
+ const maxLength = process.stdout.columns ?? 100;
175
+ const line = ` ${styleText("cyan", role.id.padEnd(20))} - ${role.description}`;
176
+ console.log(
177
+ line.length > maxLength ? `${line.slice(0, maxLength)}...` : line,
178
+ );
179
+ }
180
+ }
181
+ return "prompt";
182
+ }
183
+
184
+ if (inputTrimmed.startsWith("/agents:")) {
185
+ const match = inputTrimmed.match(/^\/agents:([^ ]+)(?:\s+(.*))?$/s);
186
+ if (!match) {
187
+ console.log(styleText("red", "\nInvalid agent invocation format."));
188
+ return "prompt";
189
+ }
190
+ return await invokeAgent(match[1], match[2] || "");
191
+ }
192
+
193
+ // /prompts or /prompts:id
194
+ if (inputTrimmed.startsWith("/prompts")) {
195
+ const prompts = await loadPrompts(claudeCodePlugins);
196
+
197
+ if (inputTrimmed === "/prompts") {
198
+ console.log(styleText("bold", "\nAvailable Prompts:"));
199
+ if (prompts.size === 0) {
200
+ console.log(" No prompts found.");
201
+ } else {
202
+ for (const prompt of prompts.values()) {
203
+ const maxLength = process.stdout.columns ?? 100;
204
+ const line = ` ${styleText("cyan", prompt.id.padEnd(20))} - ${prompt.description}`;
205
+ console.log(
206
+ line.length > maxLength ? `${line.slice(0, maxLength)}...` : line,
207
+ );
208
+ }
209
+ }
210
+ return "prompt";
211
+ }
212
+
213
+ if (inputTrimmed.startsWith("/prompts:")) {
214
+ const match = inputTrimmed.match(/^\/prompts:([^ ]+)(?:\s+(.*))?$/s);
215
+ if (!match) {
216
+ console.log(styleText("red", "\nInvalid prompt invocation format."));
217
+ return "prompt";
218
+ }
219
+ return await invokePrompt(
220
+ match[1],
221
+ match[2] || "",
222
+ `/prompts:${match[1]}`,
223
+ );
224
+ }
225
+ }
226
+
227
+ // /paste — read clipboard and emit as user input
228
+ if (inputTrimmed.startsWith("/paste")) {
229
+ const prompt = inputTrimmed.slice("/paste".length).trim();
230
+ let clipboard;
231
+ try {
232
+ if (process.platform === "darwin") {
233
+ clipboard = execFileSync("pbpaste", { encoding: "utf8" });
234
+ } else if (process.platform === "linux") {
235
+ clipboard = execFileSync("xsel", ["--clipboard", "--output"], {
236
+ encoding: "utf8",
237
+ });
238
+ } else {
239
+ console.log(
240
+ styleText(
241
+ "red",
242
+ `\nUnsupported platform for /paste: ${process.platform}`,
243
+ ),
244
+ );
245
+ return "prompt";
246
+ }
247
+ } catch (e) {
248
+ const errorMessage = e instanceof Error ? e.message : String(e);
249
+ console.log(
250
+ styleText(
251
+ "red",
252
+ `\nFailed to get clipboard content: ${errorMessage}`,
253
+ ),
254
+ );
255
+ return "prompt";
256
+ }
257
+
258
+ const combinedInput = prompt ? `${prompt}\n\n${clipboard}` : clipboard;
259
+ const messageWithContext = await loadUserMessageContext(combinedInput);
260
+ userEventEmitter.emit("userInput", messageWithContext);
261
+ return "continue";
262
+ }
263
+
264
+ // /<id> — shortcut for prompts in shortcuts/ directory
265
+ if (inputTrimmed.startsWith("/")) {
266
+ const match = inputTrimmed.match(/^\/([^ ]+)(?:\s+(.*))?$/);
267
+ if (match) {
268
+ const id = match[1];
269
+ const prompts = await loadPrompts(claudeCodePlugins);
270
+ const prompt = prompts.get(id);
271
+
272
+ if (prompt?.isShortcut) {
273
+ return await invokePrompt(id, match[2] || "", `/${id}`);
274
+ }
275
+ }
276
+ }
277
+
278
+ // Default: emit as plain user input
279
+ const messageWithContext = await loadUserMessageContext(inputTrimmed);
280
+ userEventEmitter.emit("userInput", messageWithContext);
281
+ return "continue";
282
+ };
283
+ }