@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
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { AgentEventEmitter } from "./agent"
|
|
3
|
+
* @import { CallModel, MessageContentText, MessageContentImage, MessageContentToolResult, PartialMessageContent, UserMessage, MessageContentToolUse } from "./model"
|
|
4
|
+
* @import { ToolDefinition, ToolUseApprover } from "./tool"
|
|
5
|
+
* @import { SubagentManager } from "./subagent.mjs"
|
|
6
|
+
* @import { StateManager } from "./agentState.mjs"
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { styleText } from "node:util";
|
|
10
|
+
import { compactContextToolName } from "./tools/compactContext.mjs";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* If compact_context was called successfully, discard the prior conversation
|
|
14
|
+
* (keeping only the system prompt) and append the tool result as a standard
|
|
15
|
+
* user message so the model can resume from the reloaded memory file.
|
|
16
|
+
* @param {StateManager} stateManager
|
|
17
|
+
* @param {MessageContentToolUse[]} toolUseParts
|
|
18
|
+
* @param {MessageContentToolResult[]} toolResults
|
|
19
|
+
* @returns {boolean} true if compact was applied
|
|
20
|
+
*/
|
|
21
|
+
function applyCompactContextIfCalled(stateManager, toolUseParts, toolResults) {
|
|
22
|
+
const compactToolUse = toolUseParts.find(
|
|
23
|
+
(t) => t.toolName === compactContextToolName,
|
|
24
|
+
);
|
|
25
|
+
if (!compactToolUse) return false;
|
|
26
|
+
|
|
27
|
+
const compactResult = toolResults.find(
|
|
28
|
+
(r) => r.toolUseId === compactToolUse.toolUseId,
|
|
29
|
+
);
|
|
30
|
+
if (!compactResult || compactResult.isError) return false;
|
|
31
|
+
|
|
32
|
+
const systemMessage = stateManager.getMessageAt(0);
|
|
33
|
+
if (!systemMessage) return false;
|
|
34
|
+
|
|
35
|
+
stateManager.setMessages([systemMessage]);
|
|
36
|
+
stateManager.appendMessages([
|
|
37
|
+
{ role: "user", content: compactResult.content },
|
|
38
|
+
]);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @typedef {Object} PauseSignal
|
|
44
|
+
* @property {() => boolean} isPaused - Returns true if auto-approve should be paused
|
|
45
|
+
* @property {() => void} reset - Resets the paused state
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @typedef {Object} AgentLoopConfig
|
|
50
|
+
* @property {CallModel} callModel - Function to call the language model
|
|
51
|
+
* @property {StateManager} stateManager - State manager for message handling
|
|
52
|
+
* @property {ToolDefinition[]} toolDefs - Tool definitions for the model
|
|
53
|
+
* @property {import("./toolExecutor.mjs").ToolExecutor} toolExecutor - Tool executor instance
|
|
54
|
+
* @property {AgentEventEmitter} agentEventEmitter - Event emitter for agent events
|
|
55
|
+
* @property {ToolUseApprover} toolUseApprover - Tool use approval checker
|
|
56
|
+
* @property {SubagentManager} subagentManager - Subagent manager instance
|
|
57
|
+
* @property {PauseSignal} pauseSignal - Signal to pause auto-approve after current tool completes
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @typedef {ReturnType<typeof createAgentLoop>} AgentLoop
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Create an agent loop handler
|
|
66
|
+
* @param {AgentLoopConfig} config
|
|
67
|
+
*/
|
|
68
|
+
export function createAgentLoop({
|
|
69
|
+
callModel,
|
|
70
|
+
stateManager,
|
|
71
|
+
toolDefs,
|
|
72
|
+
toolExecutor,
|
|
73
|
+
agentEventEmitter,
|
|
74
|
+
toolUseApprover,
|
|
75
|
+
subagentManager,
|
|
76
|
+
pauseSignal,
|
|
77
|
+
}) {
|
|
78
|
+
const inputHandler = createInputHandler({
|
|
79
|
+
stateManager,
|
|
80
|
+
toolExecutor,
|
|
81
|
+
subagentManager,
|
|
82
|
+
toolUseApprover,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Handle user input and run the agent turn loop
|
|
87
|
+
* @param {(MessageContentText | MessageContentImage)[]} input - User input content
|
|
88
|
+
* @returns {Promise<void>}
|
|
89
|
+
*/
|
|
90
|
+
async function handleUserInput(input) {
|
|
91
|
+
pauseSignal.reset();
|
|
92
|
+
toolUseApprover.resetApprovalCount();
|
|
93
|
+
await inputHandler.handle(input);
|
|
94
|
+
await runTurnLoop();
|
|
95
|
+
agentEventEmitter.emit("turnEnd");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Run the main agent turn loop
|
|
100
|
+
* @returns {Promise<void>}
|
|
101
|
+
*/
|
|
102
|
+
async function runTurnLoop() {
|
|
103
|
+
let thinkingLoops = 0;
|
|
104
|
+
const maxThinkingLoops = 5;
|
|
105
|
+
|
|
106
|
+
while (true) {
|
|
107
|
+
// Check if auto-approve was paused by Ctrl-C during tool execution
|
|
108
|
+
if (pauseSignal.isPaused()) {
|
|
109
|
+
pauseSignal.reset();
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const modelOutput = await callModel({
|
|
114
|
+
messages: stateManager.getMessages(),
|
|
115
|
+
tools: toolDefs,
|
|
116
|
+
/**
|
|
117
|
+
* @param {PartialMessageContent} partialContent
|
|
118
|
+
*/
|
|
119
|
+
onPartialMessageContent: (partialContent) => {
|
|
120
|
+
agentEventEmitter.emit("partialMessageContent", partialContent);
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (modelOutput instanceof Error) {
|
|
125
|
+
agentEventEmitter.emit("error", modelOutput);
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const { message: assistantMessage, providerTokenUsage } = modelOutput;
|
|
130
|
+
stateManager.appendMessages([assistantMessage]);
|
|
131
|
+
if (providerTokenUsage) {
|
|
132
|
+
agentEventEmitter.emit("providerTokenUsage", providerTokenUsage);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Gemini may stop with "thinking" -> continue
|
|
136
|
+
const lastContent = assistantMessage.content.at(-1);
|
|
137
|
+
if (lastContent?.type === "thinking") {
|
|
138
|
+
thinkingLoops += 1;
|
|
139
|
+
if (thinkingLoops > maxThinkingLoops) {
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
stateManager.appendMessages([
|
|
144
|
+
{
|
|
145
|
+
role: "user",
|
|
146
|
+
content: [{ type: "text", text: "System: Continue" }],
|
|
147
|
+
},
|
|
148
|
+
]);
|
|
149
|
+
console.error(
|
|
150
|
+
styleText(
|
|
151
|
+
"yellow",
|
|
152
|
+
`\nModel is thinking. Sending "System: Continue" (Loop: ${thinkingLoops}/${maxThinkingLoops})`,
|
|
153
|
+
),
|
|
154
|
+
);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const toolUseParts = assistantMessage.content.filter(
|
|
159
|
+
(part) => part.type === "tool_use",
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// No tool use -> turn end
|
|
163
|
+
if (toolUseParts.length === 0) {
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const validation = toolExecutor.validateBatch(toolUseParts);
|
|
168
|
+
if (!validation.isValid) {
|
|
169
|
+
stateManager.appendMessages([
|
|
170
|
+
{
|
|
171
|
+
role: "user",
|
|
172
|
+
content: validation.toolResults,
|
|
173
|
+
},
|
|
174
|
+
]);
|
|
175
|
+
if (validation.errorMessage) {
|
|
176
|
+
console.error(styleText("yellow", validation.errorMessage));
|
|
177
|
+
}
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Approve tool use
|
|
182
|
+
const decisions = toolUseParts.map(toolUseApprover.isAllowedToolUse);
|
|
183
|
+
|
|
184
|
+
const hasDeniedToolUse = decisions.some((d) => d.action === "deny");
|
|
185
|
+
if (hasDeniedToolUse) {
|
|
186
|
+
/** @type {MessageContentToolResult[]} */
|
|
187
|
+
const toolResults = toolUseParts.map((toolUse, index) => {
|
|
188
|
+
const decision = decisions[index];
|
|
189
|
+
const rejectionMessage =
|
|
190
|
+
decision.action === "deny"
|
|
191
|
+
? `Tool call rejected. ${decision.reason || ""}`.trim()
|
|
192
|
+
: "Tool call rejected due to other denied tool calls";
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
type: "tool_result",
|
|
196
|
+
toolUseId: toolUse.toolUseId,
|
|
197
|
+
toolName: toolUse.toolName,
|
|
198
|
+
content: [{ type: "text", text: rejectionMessage }],
|
|
199
|
+
isError: true,
|
|
200
|
+
};
|
|
201
|
+
});
|
|
202
|
+
stateManager.appendMessages([{ role: "user", content: toolResults }]);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const isAllToolUseApproved = decisions.every((d) => d.action === "allow");
|
|
207
|
+
if (!isAllToolUseApproved) {
|
|
208
|
+
agentEventEmitter.emit("toolUseRequest");
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const executionResult = await toolExecutor.executeBatch(toolUseParts);
|
|
213
|
+
|
|
214
|
+
if (!executionResult.success) {
|
|
215
|
+
stateManager.appendMessages([
|
|
216
|
+
{
|
|
217
|
+
role: "user",
|
|
218
|
+
content: executionResult.errors,
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
role: "user",
|
|
222
|
+
content: [
|
|
223
|
+
{
|
|
224
|
+
type: "text",
|
|
225
|
+
text: executionResult.errorMessage,
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
},
|
|
229
|
+
]);
|
|
230
|
+
console.error(styleText("yellow", executionResult.errorMessage));
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const toolResults = executionResult.results;
|
|
235
|
+
|
|
236
|
+
if (
|
|
237
|
+
applyCompactContextIfCalled(stateManager, toolUseParts, toolResults)
|
|
238
|
+
) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const result = subagentManager.processToolResults(
|
|
243
|
+
toolUseParts,
|
|
244
|
+
toolResults,
|
|
245
|
+
stateManager.getMessages(),
|
|
246
|
+
);
|
|
247
|
+
stateManager.setMessages(result.messages);
|
|
248
|
+
if (result.newMessage) {
|
|
249
|
+
stateManager.appendMessages([result.newMessage]);
|
|
250
|
+
} else {
|
|
251
|
+
stateManager.appendMessages([{ role: "user", content: toolResults }]);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
handleUserInput,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* @typedef {Object} InputHandlerContext
|
|
263
|
+
* @property {StateManager} stateManager
|
|
264
|
+
* @property {import("./toolExecutor.mjs").ToolExecutor} toolExecutor
|
|
265
|
+
* @property {import("./subagent.mjs").SubagentManager} subagentManager
|
|
266
|
+
* @property {import("./tool.d.ts").ToolUseApprover} toolUseApprover
|
|
267
|
+
*/
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* @typedef {ReturnType<typeof createInputHandler>} InputHandler
|
|
271
|
+
*/
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Create an input handler.
|
|
275
|
+
*
|
|
276
|
+
* @param {InputHandlerContext} context
|
|
277
|
+
*/
|
|
278
|
+
export function createInputHandler(context) {
|
|
279
|
+
const { stateManager, toolExecutor, subagentManager, toolUseApprover } =
|
|
280
|
+
context;
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Determine input type based on current state and input.
|
|
284
|
+
* @param {UserMessage["content"]} input
|
|
285
|
+
* @returns {'toolApproval' | 'resume' | 'text'}
|
|
286
|
+
*/
|
|
287
|
+
function determineInputType(input) {
|
|
288
|
+
const lastMessage = stateManager.getMessageAt(-1);
|
|
289
|
+
|
|
290
|
+
// Check if there's a pending tool call
|
|
291
|
+
if (lastMessage?.content.some((part) => part.type === "tool_use")) {
|
|
292
|
+
return "toolApproval";
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (
|
|
296
|
+
input.length === 1 &&
|
|
297
|
+
input[0].type === "text" &&
|
|
298
|
+
input[0].text.toLowerCase() === "/resume"
|
|
299
|
+
) {
|
|
300
|
+
return "resume";
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return "text";
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Handle tool approval/rejection input.
|
|
308
|
+
* @param {UserMessage["content"]} input
|
|
309
|
+
*/
|
|
310
|
+
async function handleToolApproval(input) {
|
|
311
|
+
const lastMessage = stateManager.getMessageAt(-1);
|
|
312
|
+
if (!lastMessage) return;
|
|
313
|
+
|
|
314
|
+
/** @type {MessageContentToolUse[]} */
|
|
315
|
+
const toolUseParts = lastMessage.content.filter(
|
|
316
|
+
(part) => part.type === "tool_use",
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const isApproval =
|
|
320
|
+
input.length === 1 &&
|
|
321
|
+
input[0].type === "text" &&
|
|
322
|
+
input[0].text.toLocaleLowerCase().match(/^(yes|y|y)$/i);
|
|
323
|
+
|
|
324
|
+
if (isApproval) {
|
|
325
|
+
if (
|
|
326
|
+
/** @type {MessageContentText} */ (input[0]).text.match(/^(YES|Y)$/)
|
|
327
|
+
) {
|
|
328
|
+
for (const toolUse of toolUseParts) {
|
|
329
|
+
toolUseApprover.allowToolUse(toolUse);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const executionResult = await toolExecutor.executeBatch(toolUseParts);
|
|
334
|
+
if (!executionResult.success) {
|
|
335
|
+
stateManager.appendMessages([
|
|
336
|
+
{ role: "user", content: executionResult.errors },
|
|
337
|
+
]);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const toolResults = executionResult.results;
|
|
342
|
+
|
|
343
|
+
if (
|
|
344
|
+
applyCompactContextIfCalled(stateManager, toolUseParts, toolResults)
|
|
345
|
+
) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const result = subagentManager.processToolResults(
|
|
350
|
+
toolUseParts,
|
|
351
|
+
toolResults,
|
|
352
|
+
stateManager.getMessages(),
|
|
353
|
+
);
|
|
354
|
+
stateManager.setMessages(result.messages);
|
|
355
|
+
|
|
356
|
+
if (result.newMessage) {
|
|
357
|
+
stateManager.appendMessages([result.newMessage]);
|
|
358
|
+
} else {
|
|
359
|
+
stateManager.appendMessages([{ role: "user", content: toolResults }]);
|
|
360
|
+
}
|
|
361
|
+
} else {
|
|
362
|
+
// Rejected
|
|
363
|
+
/** @type {MessageContentToolResult[]} */
|
|
364
|
+
const toolResults = toolUseParts.map((toolUse) => ({
|
|
365
|
+
type: "tool_result",
|
|
366
|
+
toolUseId: toolUse.toolUseId,
|
|
367
|
+
toolName: toolUse.toolName,
|
|
368
|
+
content: [{ type: "text", text: "Tool call rejected" }],
|
|
369
|
+
isError: true,
|
|
370
|
+
}));
|
|
371
|
+
|
|
372
|
+
stateManager.appendMessages([
|
|
373
|
+
{ role: "user", content: toolResults },
|
|
374
|
+
{
|
|
375
|
+
role: "user",
|
|
376
|
+
content: input,
|
|
377
|
+
},
|
|
378
|
+
]);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function handleResume() {
|
|
383
|
+
// Resume the conversation stopped by unexpected error, etc.
|
|
384
|
+
// No state changes needed
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* @param {UserMessage["content"]} input
|
|
389
|
+
*/
|
|
390
|
+
async function handleText(input) {
|
|
391
|
+
stateManager.appendMessages([
|
|
392
|
+
{
|
|
393
|
+
role: "user",
|
|
394
|
+
content: input,
|
|
395
|
+
},
|
|
396
|
+
]);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
/**
|
|
401
|
+
* @param {UserMessage["content"]} input
|
|
402
|
+
*/
|
|
403
|
+
async handle(input) {
|
|
404
|
+
const inputType = determineInputType(input);
|
|
405
|
+
|
|
406
|
+
switch (inputType) {
|
|
407
|
+
case "toolApproval":
|
|
408
|
+
await handleToolApproval(input);
|
|
409
|
+
break;
|
|
410
|
+
case "resume":
|
|
411
|
+
await handleResume();
|
|
412
|
+
break;
|
|
413
|
+
case "text":
|
|
414
|
+
await handleText(input);
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { Message } from "./model"
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {ReturnType<typeof createStateManager>} StateManager
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} StateEventHandlers
|
|
11
|
+
* @property {(messages: Message[]) => void} onMessagesAppended
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Creates a state manager for message handling.
|
|
16
|
+
* @param {Message[]} initialMessages
|
|
17
|
+
* @param {StateEventHandlers} handlers
|
|
18
|
+
*/
|
|
19
|
+
export function createStateManager(initialMessages, handlers) {
|
|
20
|
+
/** @type {Message[]} */
|
|
21
|
+
let messages = [...initialMessages];
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
/** Get all messages (immutable copy) */
|
|
25
|
+
getMessages: () => [...messages],
|
|
26
|
+
|
|
27
|
+
/** Get message at specific index (supports -1 for last) */
|
|
28
|
+
getMessageAt: /** @param {number} index */ (index) => messages.at(index),
|
|
29
|
+
|
|
30
|
+
/** Append messages */
|
|
31
|
+
appendMessages: /** @param {Message[]} newMessages */ (newMessages) => {
|
|
32
|
+
messages = [...messages, ...newMessages];
|
|
33
|
+
handlers.onMessagesAppended(newMessages);
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
/** Replace all messages */
|
|
37
|
+
setMessages: /** @param {Message[]} newMessages */ (newMessages) => {
|
|
38
|
+
messages = [...newMessages];
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { loadAppConfig } from "./config.mjs";
|
|
5
|
+
import { CLAUDE_CODE_PLUGIN_DIR } from "./env.mjs";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} ClaudeCodePluginRepo
|
|
9
|
+
* @property {string} source
|
|
10
|
+
* @property {Array<{name: string, path: string, only?: string}>} plugins
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {Object} ClaudeCodePlugin
|
|
15
|
+
* @property {string} name
|
|
16
|
+
* @property {string} path
|
|
17
|
+
* @property {RegExp} [only]
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolve plugin paths from hierarchical config structure.
|
|
22
|
+
* Converts {source, plugins} to flat {name, path, only} with full paths.
|
|
23
|
+
* @param {ClaudeCodePluginRepo[]} repos
|
|
24
|
+
* @returns {ClaudeCodePlugin[]}
|
|
25
|
+
*/
|
|
26
|
+
export function resolvePluginPaths(repos) {
|
|
27
|
+
if (!repos) return [];
|
|
28
|
+
|
|
29
|
+
/** @type {ClaudeCodePlugin[]} */
|
|
30
|
+
const result = [];
|
|
31
|
+
|
|
32
|
+
for (const repo of repos) {
|
|
33
|
+
const ownerRepo = extractOwnerRepo(repo.source);
|
|
34
|
+
if (!ownerRepo) {
|
|
35
|
+
console.warn(`Invalid source URL: ${repo.source}`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const plugin of repo.plugins) {
|
|
40
|
+
// Compile only pattern to RegExp
|
|
41
|
+
let only;
|
|
42
|
+
if (plugin.only) {
|
|
43
|
+
try {
|
|
44
|
+
only = new RegExp(plugin.only);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.warn(
|
|
47
|
+
`Invalid regex pattern "${plugin.only}" for plugin "${plugin.name}":`,
|
|
48
|
+
err instanceof Error ? err.message : String(err),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
result.push({
|
|
54
|
+
name: plugin.name,
|
|
55
|
+
path: path.join(CLAUDE_CODE_PLUGIN_DIR, ownerRepo, plugin.path),
|
|
56
|
+
only,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Install Claude Code plugins by cloning repositories.
|
|
66
|
+
*/
|
|
67
|
+
export async function installClaudeCodePlugins() {
|
|
68
|
+
const { appConfig } = await loadAppConfig({ skipTrustCheck: true });
|
|
69
|
+
const repos = appConfig.claudeCodePlugins ?? [];
|
|
70
|
+
|
|
71
|
+
if (repos.length === 0) {
|
|
72
|
+
console.log("No plugins configured.");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let installed = 0;
|
|
77
|
+
let skipped = 0;
|
|
78
|
+
let failed = 0;
|
|
79
|
+
|
|
80
|
+
// Track paths for summary
|
|
81
|
+
/** @type {string[]} */
|
|
82
|
+
const installedPaths = [];
|
|
83
|
+
/** @type {string[]} */
|
|
84
|
+
const skippedPaths = [];
|
|
85
|
+
|
|
86
|
+
// Ensure plugin directory exists
|
|
87
|
+
await fs.mkdir(CLAUDE_CODE_PLUGIN_DIR, { recursive: true });
|
|
88
|
+
|
|
89
|
+
for (const repo of repos) {
|
|
90
|
+
const ownerRepo = extractOwnerRepo(repo.source);
|
|
91
|
+
if (!ownerRepo) {
|
|
92
|
+
console.error(`❌ Invalid source URL: ${repo.source}`);
|
|
93
|
+
failed++;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const destPath = path.join(CLAUDE_CODE_PLUGIN_DIR, ownerRepo);
|
|
98
|
+
|
|
99
|
+
// Check if already exists
|
|
100
|
+
const exists = await fs
|
|
101
|
+
.access(destPath)
|
|
102
|
+
.then(() => true)
|
|
103
|
+
.catch(() => false);
|
|
104
|
+
if (exists) {
|
|
105
|
+
console.log(`⏭️ Skipping ${repo.source} → ${destPath}: already installed`);
|
|
106
|
+
skippedPaths.push(destPath);
|
|
107
|
+
skipped++;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Clone the repository
|
|
112
|
+
console.log(`📥 Installing ${repo.source}...`);
|
|
113
|
+
try {
|
|
114
|
+
await new Promise((resolve, reject) => {
|
|
115
|
+
execFile(
|
|
116
|
+
"git",
|
|
117
|
+
["clone", "--depth", "1", repo.source, destPath],
|
|
118
|
+
(err) => {
|
|
119
|
+
if (err) reject(err);
|
|
120
|
+
else resolve(undefined);
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
console.log(`✅ Installed to ${destPath}`);
|
|
125
|
+
installedPaths.push(destPath);
|
|
126
|
+
installed++;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error(
|
|
129
|
+
`❌ Failed to install: ${error instanceof Error ? error.message : String(error)}`,
|
|
130
|
+
);
|
|
131
|
+
failed++;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log(
|
|
136
|
+
`\n📊 Summary: ${installed} installed, ${skipped} skipped, ${failed} failed`,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
if (installedPaths.length > 0) {
|
|
140
|
+
console.log("\nInstalled:");
|
|
141
|
+
for (const p of installedPaths) {
|
|
142
|
+
console.log(` • ${p}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (skippedPaths.length > 0) {
|
|
147
|
+
console.log("\nSkipped:");
|
|
148
|
+
for (const p of skippedPaths) {
|
|
149
|
+
console.log(` • ${p}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Extract owner/repo from source URL.
|
|
156
|
+
* @param {string} source
|
|
157
|
+
* @returns {string|null}
|
|
158
|
+
*/
|
|
159
|
+
function extractOwnerRepo(source) {
|
|
160
|
+
// Handle: https://github.com/owner/repo
|
|
161
|
+
// Handle: git@github.com:owner/repo.git
|
|
162
|
+
const match = source.match(/[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
163
|
+
return match ? match[1] : null;
|
|
164
|
+
}
|