@iinm/plain-agent 1.8.4 → 1.8.5
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/bin/plain +1 -1
- package/package.json +7 -5
- package/sandbox/bin/plain-sandbox +13 -0
- package/src/agent.d.ts +52 -0
- package/src/agent.mjs +204 -0
- package/src/agentLoop.mjs +419 -0
- package/src/agentState.mjs +41 -0
- package/src/claudeCodePlugin.mjs +164 -0
- package/src/cliArgs.mjs +175 -0
- package/src/cliBatch.mjs +147 -0
- package/src/cliCommands.mjs +283 -0
- package/src/cliCompleter.mjs +227 -0
- package/src/cliCost.mjs +309 -0
- package/src/cliFormatter.mjs +413 -0
- package/src/cliInteractive.mjs +529 -0
- package/src/cliInterruptTransform.mjs +51 -0
- package/src/cliMuteTransform.mjs +26 -0
- package/src/cliPasteTransform.mjs +183 -0
- package/src/config.d.ts +36 -0
- package/src/config.mjs +197 -0
- package/src/context/loadAgentRoles.mjs +294 -0
- package/src/context/loadPrompts.mjs +337 -0
- package/src/context/loadUserMessageContext.mjs +147 -0
- package/src/costTracker.mjs +210 -0
- package/src/env.mjs +44 -0
- package/src/main.mjs +281 -0
- package/src/mcpClient.mjs +351 -0
- package/src/mcpIntegration.mjs +160 -0
- package/src/model.d.ts +109 -0
- package/src/modelCaller.mjs +32 -0
- package/src/modelDefinition.d.ts +92 -0
- package/src/prompt.mjs +138 -0
- package/src/providers/anthropic.d.ts +248 -0
- package/src/providers/anthropic.mjs +587 -0
- package/src/providers/bedrock.d.ts +249 -0
- package/src/providers/bedrock.mjs +700 -0
- package/src/providers/gemini.d.ts +208 -0
- package/src/providers/gemini.mjs +754 -0
- package/src/providers/openai.d.ts +281 -0
- package/src/providers/openai.mjs +544 -0
- package/src/providers/openaiCompatible.d.ts +147 -0
- package/src/providers/openaiCompatible.mjs +652 -0
- package/src/providers/platform/awsSigV4.mjs +184 -0
- package/src/providers/platform/azure.mjs +42 -0
- package/src/providers/platform/bedrock.mjs +78 -0
- package/src/providers/platform/googleCloud.mjs +34 -0
- package/src/subagent.mjs +265 -0
- package/src/tmpfile.mjs +27 -0
- package/src/tool.d.ts +74 -0
- package/src/toolExecutor.mjs +236 -0
- package/src/toolInputValidator.mjs +183 -0
- package/src/toolUseApprover.mjs +99 -0
- package/src/tools/askURL.mjs +209 -0
- package/src/tools/askWeb.mjs +208 -0
- package/src/tools/compactContext.d.ts +4 -0
- package/src/tools/compactContext.mjs +87 -0
- package/src/tools/execCommand.d.ts +22 -0
- package/src/tools/execCommand.mjs +200 -0
- package/src/tools/patchFile.d.ts +4 -0
- package/src/tools/patchFile.mjs +133 -0
- package/src/tools/switchToMainAgent.d.ts +3 -0
- package/src/tools/switchToMainAgent.mjs +43 -0
- package/src/tools/switchToSubagent.d.ts +4 -0
- package/src/tools/switchToSubagent.mjs +59 -0
- package/src/tools/tmuxCommand.d.ts +14 -0
- package/src/tools/tmuxCommand.mjs +194 -0
- package/src/tools/writeFile.d.ts +4 -0
- package/src/tools/writeFile.mjs +56 -0
- package/src/usageStore.mjs +167 -0
- package/src/utils/evalJSONConfig.mjs +72 -0
- package/src/utils/matchValue.d.ts +6 -0
- package/src/utils/matchValue.mjs +40 -0
- package/src/utils/noThrow.mjs +31 -0
- package/src/utils/notify.mjs +29 -0
- package/src/utils/parseFileRange.mjs +18 -0
- package/src/utils/readFileRange.mjs +33 -0
- package/src/utils/retryOnError.mjs +41 -0
- package/src/voiceInput.mjs +61 -0
- package/src/voiceInputGemini.mjs +105 -0
- package/src/voiceInputOpenAI.mjs +104 -0
- package/src/voiceInputSession.mjs +543 -0
- package/src/voiceToggleKey.mjs +62 -0
- package/dist/main.mjs +0 -473
- package/dist/main.mjs.map +0 -7
package/src/cliArgs.mjs
ADDED
|
@@ -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
|
+
}
|
package/src/cliBatch.mjs
ADDED
|
@@ -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
|
+
}
|