@iinm/plain-agent 1.0.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.
Files changed (79) hide show
  1. package/.config/agents.library/code-simplifier.md +5 -0
  2. package/.config/agents.library/qa-engineer.md +74 -0
  3. package/.config/agents.library/software-architect.md +278 -0
  4. package/.config/agents.predefined/worker.md +3 -0
  5. package/.config/config.predefined.json +825 -0
  6. package/.config/prompts.library/code-review.md +8 -0
  7. package/.config/prompts.library/feature-dev.md +6 -0
  8. package/.config/prompts.predefined/shortcuts/commit-by-user.md +9 -0
  9. package/.config/prompts.predefined/shortcuts/commit.md +10 -0
  10. package/.config/prompts.predefined/shortcuts/general-question.md +6 -0
  11. package/LICENSE +21 -0
  12. package/README.md +624 -0
  13. package/bin/plain +3 -0
  14. package/bin/plain-interrupt +6 -0
  15. package/bin/plain-notify-desktop +19 -0
  16. package/bin/plain-notify-terminal-bell +3 -0
  17. package/package.json +57 -0
  18. package/sandbox/bin/plain-sandbox +972 -0
  19. package/src/agent.d.ts +48 -0
  20. package/src/agent.mjs +159 -0
  21. package/src/agentLoop.mjs +369 -0
  22. package/src/agentState.mjs +41 -0
  23. package/src/cliArgs.mjs +45 -0
  24. package/src/cliFormatter.mjs +217 -0
  25. package/src/cliInteractive.mjs +739 -0
  26. package/src/config.d.ts +48 -0
  27. package/src/config.mjs +168 -0
  28. package/src/context/consumeInterruptMessage.mjs +30 -0
  29. package/src/context/loadAgentRoles.mjs +272 -0
  30. package/src/context/loadPrompts.mjs +312 -0
  31. package/src/context/loadUserMessageContext.mjs +147 -0
  32. package/src/env.mjs +46 -0
  33. package/src/main.mjs +202 -0
  34. package/src/mcp.mjs +202 -0
  35. package/src/model.d.ts +109 -0
  36. package/src/modelCaller.mjs +29 -0
  37. package/src/modelDefinition.d.ts +73 -0
  38. package/src/prompt.mjs +128 -0
  39. package/src/providers/anthropic.d.ts +248 -0
  40. package/src/providers/anthropic.mjs +596 -0
  41. package/src/providers/gemini.d.ts +208 -0
  42. package/src/providers/gemini.mjs +752 -0
  43. package/src/providers/openai.d.ts +281 -0
  44. package/src/providers/openai.mjs +551 -0
  45. package/src/providers/openaiCompatible.d.ts +147 -0
  46. package/src/providers/openaiCompatible.mjs +658 -0
  47. package/src/providers/platform/azure.mjs +42 -0
  48. package/src/providers/platform/bedrock.mjs +74 -0
  49. package/src/providers/platform/googleCloud.mjs +34 -0
  50. package/src/subagent.mjs +247 -0
  51. package/src/tmpfile.mjs +27 -0
  52. package/src/tool.d.ts +74 -0
  53. package/src/toolExecutor.mjs +236 -0
  54. package/src/toolInputValidator.mjs +183 -0
  55. package/src/toolUseApprover.mjs +98 -0
  56. package/src/tools/askGoogle.mjs +135 -0
  57. package/src/tools/delegateToSubagent.d.ts +4 -0
  58. package/src/tools/delegateToSubagent.mjs +48 -0
  59. package/src/tools/execCommand.d.ts +22 -0
  60. package/src/tools/execCommand.mjs +200 -0
  61. package/src/tools/fetchWebPage.mjs +96 -0
  62. package/src/tools/patchFile.d.ts +4 -0
  63. package/src/tools/patchFile.mjs +96 -0
  64. package/src/tools/reportAsSubagent.d.ts +3 -0
  65. package/src/tools/reportAsSubagent.mjs +44 -0
  66. package/src/tools/tavilySearch.d.ts +6 -0
  67. package/src/tools/tavilySearch.mjs +57 -0
  68. package/src/tools/tmuxCommand.d.ts +14 -0
  69. package/src/tools/tmuxCommand.mjs +194 -0
  70. package/src/tools/writeFile.d.ts +4 -0
  71. package/src/tools/writeFile.mjs +56 -0
  72. package/src/utils/evalJSONConfig.mjs +48 -0
  73. package/src/utils/matchValue.d.ts +6 -0
  74. package/src/utils/matchValue.mjs +40 -0
  75. package/src/utils/noThrow.mjs +31 -0
  76. package/src/utils/notify.mjs +28 -0
  77. package/src/utils/parseFileRange.mjs +18 -0
  78. package/src/utils/readFileRange.mjs +33 -0
  79. package/src/utils/retryOnError.mjs +41 -0
package/src/agent.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ import type { EventEmitter } from "node:events";
2
+ import type {
3
+ CallModel,
4
+ Message,
5
+ MessageContentImage,
6
+ MessageContentText,
7
+ PartialMessageContent,
8
+ ProviderTokenUsage,
9
+ } from "./model";
10
+ import type { Tool, ToolUseApprover } from "./tool";
11
+ import type { AgentRole } from "./context/loadAgentRoles.mjs";
12
+
13
+ export type Agent = {
14
+ userEventEmitter: UserEventEmitter;
15
+ agentEventEmitter: AgentEventEmitter;
16
+ agentCommands: AgentCommands;
17
+ };
18
+
19
+ export type AgentCommands = {
20
+ dumpMessages: () => Promise<void>;
21
+ loadMessages: () => Promise<void>;
22
+ };
23
+
24
+ type UserEventMap = {
25
+ userInput: [(MessageContentText | MessageContentImage)[]];
26
+ };
27
+
28
+ export type UserEventEmitter = EventEmitter<UserEventMap>;
29
+
30
+ type AgentEventMap = {
31
+ message: [Message];
32
+ partialMessageContent: [PartialMessageContent];
33
+ error: [Error];
34
+ toolUseRequest: [];
35
+ turnEnd: [];
36
+ providerTokenUsage: [ProviderTokenUsage];
37
+ subagentSwitched: [{ name: string } | null];
38
+ };
39
+
40
+ export type AgentEventEmitter = EventEmitter<AgentEventMap>;
41
+
42
+ export type AgentConfig = {
43
+ callModel: CallModel;
44
+ prompt: string;
45
+ tools: Tool[];
46
+ toolUseApprover: ToolUseApprover;
47
+ agentRoles: Map<string, AgentRole>;
48
+ };
package/src/agent.mjs ADDED
@@ -0,0 +1,159 @@
1
+ /**
2
+ * @import { Agent, AgentConfig, AgentEventEmitter, UserEventEmitter } from "./agent"
3
+ * @import { Tool, ToolDefinition } from "./tool"
4
+ * @import { DelegateToSubagentInput } from "./tools/delegateToSubagent"
5
+ * @import { ReportAsSubagentInput } from "./tools/reportAsSubagent"
6
+ */
7
+
8
+ import { EventEmitter } from "node:events";
9
+ import fs from "node:fs/promises";
10
+ import { createAgentLoop } from "./agentLoop.mjs";
11
+ import { createStateManager } from "./agentState.mjs";
12
+ import { MESSAGES_DUMP_FILE_PATH } from "./env.mjs";
13
+ import { createSubagentManager } from "./subagent.mjs";
14
+ import { createToolExecutor } from "./toolExecutor.mjs";
15
+ import { delegateToSubagentToolName } from "./tools/delegateToSubagent.mjs";
16
+ import { reportAsSubagentToolName } from "./tools/reportAsSubagent.mjs";
17
+
18
+ /**
19
+ * @param {AgentConfig} config
20
+ * @returns {Agent}
21
+ */
22
+ export function createAgent({
23
+ callModel,
24
+ prompt,
25
+ tools,
26
+ toolUseApprover,
27
+ agentRoles,
28
+ }) {
29
+ /** @type {UserEventEmitter} */
30
+ const userEventEmitter = new EventEmitter();
31
+ /** @type {AgentEventEmitter} */
32
+ const agentEventEmitter = new EventEmitter();
33
+
34
+ const stateManager = createStateManager(
35
+ [
36
+ {
37
+ role: "system",
38
+ content: [{ type: "text", text: prompt }],
39
+ },
40
+ ],
41
+ {
42
+ onMessagesAppended: (newMessages) => {
43
+ const lastMessage = newMessages.at(-1);
44
+ if (!lastMessage) {
45
+ return;
46
+ }
47
+ agentEventEmitter.emit("message", lastMessage);
48
+ },
49
+ },
50
+ );
51
+
52
+ const subagentManager = createSubagentManager(agentRoles, {
53
+ onSubagentSwitched: (subagent) => {
54
+ agentEventEmitter.emit("subagentSwitched", subagent);
55
+ },
56
+ });
57
+
58
+ /**
59
+ * @param {DelegateToSubagentInput} input
60
+ */
61
+ const delegateToSubagentImpl = async (input) => {
62
+ const result = subagentManager.delegateToSubagent(
63
+ input.name,
64
+ input.goal,
65
+ stateManager.getMessages().length - 1,
66
+ );
67
+ if (!result.success) {
68
+ return new Error(result.error);
69
+ }
70
+ return result.value;
71
+ };
72
+
73
+ /**
74
+ * @param {ReportAsSubagentInput} input
75
+ */
76
+ const reportAsSubagentImpl = async (input) => {
77
+ const result = await subagentManager.reportAsSubagent(input.memoryPath);
78
+ if (!result.success) {
79
+ return new Error(result.error);
80
+ }
81
+ return result.memoryContent;
82
+ };
83
+
84
+ /** @type {Map<string, Tool>} */
85
+ const toolByName = new Map();
86
+ for (const tool of tools) {
87
+ if (tool.def.name === delegateToSubagentToolName && tool.injectImpl) {
88
+ tool.injectImpl(delegateToSubagentImpl);
89
+ }
90
+ if (tool.def.name === reportAsSubagentToolName && tool.injectImpl) {
91
+ tool.injectImpl(reportAsSubagentImpl);
92
+ }
93
+ toolByName.set(tool.def.name, tool);
94
+ }
95
+
96
+ /** @type {ToolDefinition[]} */
97
+ const toolDefs = tools.map(({ def }) => def);
98
+
99
+ const toolExecutor = createToolExecutor(toolByName, {
100
+ exclusiveToolNames: [delegateToSubagentToolName, reportAsSubagentToolName],
101
+ });
102
+
103
+ async function dumpMessages() {
104
+ const filePath = MESSAGES_DUMP_FILE_PATH;
105
+ try {
106
+ await fs.writeFile(
107
+ filePath,
108
+ JSON.stringify(stateManager.getMessages(), null, 2),
109
+ );
110
+ console.log(`Messages dumped to ${filePath}`);
111
+ } catch (error) {
112
+ const message = error instanceof Error ? error.message : String(error);
113
+ console.error(`Error dumping messages: ${message}`);
114
+ }
115
+ }
116
+
117
+ async function loadMessages() {
118
+ const filePath = MESSAGES_DUMP_FILE_PATH;
119
+ try {
120
+ const data = await fs.readFile(filePath, "utf-8");
121
+ const loadedMessages = JSON.parse(data);
122
+ if (Array.isArray(loadedMessages)) {
123
+ // Keep the system message (index 0) and replace the rest
124
+ stateManager.setMessages([
125
+ stateManager.getMessageAt(0),
126
+ ...loadedMessages.slice(1),
127
+ ]);
128
+ console.log(`Messages loaded from ${filePath}`);
129
+ } else {
130
+ console.error("Error loading messages: Invalid format in file.");
131
+ }
132
+ } catch (error) {
133
+ if (error instanceof Error) {
134
+ console.error(`Error loading messages: ${error.message}`);
135
+ }
136
+ }
137
+ }
138
+
139
+ const agentLoop = createAgentLoop({
140
+ callModel,
141
+ stateManager,
142
+ toolDefs,
143
+ toolExecutor,
144
+ agentEventEmitter,
145
+ toolUseApprover,
146
+ subagentManager,
147
+ });
148
+
149
+ userEventEmitter.on("userInput", agentLoop.handleUserInput);
150
+
151
+ return {
152
+ userEventEmitter,
153
+ agentEventEmitter,
154
+ agentCommands: {
155
+ dumpMessages,
156
+ loadMessages,
157
+ },
158
+ };
159
+ }
@@ -0,0 +1,369 @@
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 { consumeInterruptMessage } from "./context/consumeInterruptMessage.mjs";
11
+
12
+ /**
13
+ * @typedef {Object} AgentLoopConfig
14
+ * @property {CallModel} callModel - Function to call the language model
15
+ * @property {StateManager} stateManager - State manager for message handling
16
+ * @property {ToolDefinition[]} toolDefs - Tool definitions for the model
17
+ * @property {import("./toolExecutor.mjs").ToolExecutor} toolExecutor - Tool executor instance
18
+ * @property {AgentEventEmitter} agentEventEmitter - Event emitter for agent events
19
+ * @property {ToolUseApprover} toolUseApprover - Tool use approval checker
20
+ * @property {SubagentManager} subagentManager - Subagent manager instance
21
+ */
22
+
23
+ /**
24
+ * @typedef {ReturnType<typeof createAgentLoop>} AgentLoop
25
+ */
26
+
27
+ /**
28
+ * Create an agent loop handler
29
+ * @param {AgentLoopConfig} config
30
+ */
31
+ export function createAgentLoop({
32
+ callModel,
33
+ stateManager,
34
+ toolDefs,
35
+ toolExecutor,
36
+ agentEventEmitter,
37
+ toolUseApprover,
38
+ subagentManager,
39
+ }) {
40
+ const inputHandler = createInputHandler({
41
+ stateManager,
42
+ toolExecutor,
43
+ subagentManager,
44
+ toolUseApprover,
45
+ });
46
+
47
+ /**
48
+ * Handle user input and run the agent turn loop
49
+ * @param {(MessageContentText | MessageContentImage)[]} input - User input content
50
+ * @returns {Promise<void>}
51
+ */
52
+ async function handleUserInput(input) {
53
+ toolUseApprover.resetApprovalCount();
54
+ await inputHandler.handle(input);
55
+ await runTurnLoop();
56
+ agentEventEmitter.emit("turnEnd");
57
+ }
58
+
59
+ /**
60
+ * Run the main agent turn loop
61
+ * @returns {Promise<void>}
62
+ */
63
+ async function runTurnLoop() {
64
+ let thinkingLoops = 0;
65
+ const maxThinkingLoops = 5;
66
+
67
+ while (true) {
68
+ const modelOutput = await callModel({
69
+ messages: stateManager.getMessages(),
70
+ tools: toolDefs,
71
+ /**
72
+ * @param {PartialMessageContent} partialContent
73
+ */
74
+ onPartialMessageContent: (partialContent) => {
75
+ agentEventEmitter.emit("partialMessageContent", partialContent);
76
+ },
77
+ });
78
+
79
+ if (modelOutput instanceof Error) {
80
+ agentEventEmitter.emit("error", modelOutput);
81
+ break;
82
+ }
83
+
84
+ const { message: assistantMessage, providerTokenUsage } = modelOutput;
85
+ stateManager.appendMessages([assistantMessage]);
86
+ agentEventEmitter.emit("providerTokenUsage", providerTokenUsage);
87
+
88
+ // Gemini may stop with "thinking" -> continue
89
+ const lastContent = assistantMessage.content.at(-1);
90
+ if (lastContent?.type === "thinking") {
91
+ thinkingLoops += 1;
92
+ if (thinkingLoops > maxThinkingLoops) {
93
+ break;
94
+ }
95
+
96
+ stateManager.appendMessages([
97
+ {
98
+ role: "user",
99
+ content: [{ type: "text", text: "System: Continue" }],
100
+ },
101
+ ]);
102
+ console.error(
103
+ styleText(
104
+ "yellow",
105
+ `\nModel is thinking. Sending "System: Continue" (Loop: ${thinkingLoops}/${maxThinkingLoops})`,
106
+ ),
107
+ );
108
+ continue;
109
+ }
110
+
111
+ const toolUseParts = assistantMessage.content.filter(
112
+ (part) => part.type === "tool_use",
113
+ );
114
+
115
+ // No tool use -> turn end
116
+ if (toolUseParts.length === 0) {
117
+ break;
118
+ }
119
+
120
+ const validation = toolExecutor.validateBatch(toolUseParts);
121
+ if (!validation.isValid) {
122
+ stateManager.appendMessages([
123
+ {
124
+ role: "user",
125
+ content: validation.toolResults,
126
+ },
127
+ ]);
128
+ if (validation.errorMessage) {
129
+ console.error(styleText("yellow", validation.errorMessage));
130
+ }
131
+ continue;
132
+ }
133
+
134
+ // Approve tool use
135
+ const decisions = toolUseParts.map(toolUseApprover.isAllowedToolUse);
136
+
137
+ const hasDeniedToolUse = decisions.some((d) => d.action === "deny");
138
+ if (hasDeniedToolUse) {
139
+ /** @type {MessageContentToolResult[]} */
140
+ const toolResults = toolUseParts.map((toolUse, index) => {
141
+ const decision = decisions[index];
142
+ const rejectionMessage =
143
+ decision.action === "deny"
144
+ ? `Tool call rejected. ${decision.reason || ""}`.trim()
145
+ : "Tool call rejected due to other denied tool calls";
146
+
147
+ return {
148
+ type: "tool_result",
149
+ toolUseId: toolUse.toolUseId,
150
+ toolName: toolUse.toolName,
151
+ content: [{ type: "text", text: rejectionMessage }],
152
+ isError: true,
153
+ };
154
+ });
155
+ stateManager.appendMessages([{ role: "user", content: toolResults }]);
156
+ continue;
157
+ }
158
+
159
+ const isAllToolUseApproved = decisions.every((d) => d.action === "allow");
160
+ if (!isAllToolUseApproved) {
161
+ agentEventEmitter.emit("toolUseRequest");
162
+ break;
163
+ }
164
+
165
+ const executionResult = await toolExecutor.executeBatch(toolUseParts);
166
+
167
+ if (!executionResult.success) {
168
+ stateManager.appendMessages([
169
+ {
170
+ role: "user",
171
+ content: executionResult.errors,
172
+ },
173
+ {
174
+ role: "user",
175
+ content: [
176
+ {
177
+ type: "text",
178
+ text: executionResult.errorMessage,
179
+ },
180
+ ],
181
+ },
182
+ ]);
183
+ console.error(styleText("yellow", executionResult.errorMessage));
184
+ continue;
185
+ }
186
+
187
+ const toolResults = executionResult.results;
188
+
189
+ const result = subagentManager.processToolResults(
190
+ toolUseParts,
191
+ toolResults,
192
+ stateManager.getMessages(),
193
+ );
194
+ stateManager.setMessages(result.messages);
195
+ if (result.newMessage) {
196
+ stateManager.appendMessages([result.newMessage]);
197
+ } else {
198
+ stateManager.appendMessages([{ role: "user", content: toolResults }]);
199
+ }
200
+
201
+ const interruptMessage = await consumeInterruptMessage();
202
+ if (interruptMessage) {
203
+ stateManager.appendMessages([
204
+ {
205
+ role: "user",
206
+ content: [{ type: "text", text: interruptMessage }],
207
+ },
208
+ ]);
209
+ }
210
+ }
211
+ }
212
+
213
+ return {
214
+ handleUserInput,
215
+ };
216
+ }
217
+
218
+ /**
219
+ * @typedef {Object} InputHandlerContext
220
+ * @property {StateManager} stateManager
221
+ * @property {import("./toolExecutor.mjs").ToolExecutor} toolExecutor
222
+ * @property {import("./subagent.mjs").SubagentManager} subagentManager
223
+ * @property {import("./tool.d.ts").ToolUseApprover} toolUseApprover
224
+ */
225
+
226
+ /**
227
+ * @typedef {ReturnType<typeof createInputHandler>} InputHandler
228
+ */
229
+
230
+ /**
231
+ * Create an input handler.
232
+ *
233
+ * @param {InputHandlerContext} context
234
+ */
235
+ export function createInputHandler(context) {
236
+ const { stateManager, toolExecutor, subagentManager, toolUseApprover } =
237
+ context;
238
+
239
+ /**
240
+ * Determine input type based on current state and input.
241
+ * @param {UserMessage["content"]} input
242
+ * @returns {'toolApproval' | 'resume' | 'text'}
243
+ */
244
+ function determineInputType(input) {
245
+ const lastMessage = stateManager.getMessageAt(-1);
246
+
247
+ // Check if there's a pending tool call
248
+ if (lastMessage?.content.some((part) => part.type === "tool_use")) {
249
+ return "toolApproval";
250
+ }
251
+
252
+ if (
253
+ input.length === 1 &&
254
+ input[0].type === "text" &&
255
+ input[0].text.toLowerCase() === "/resume"
256
+ ) {
257
+ return "resume";
258
+ }
259
+
260
+ return "text";
261
+ }
262
+
263
+ /**
264
+ * Handle tool approval/rejection input.
265
+ * @param {UserMessage["content"]} input
266
+ */
267
+ async function handleToolApproval(input) {
268
+ const lastMessage = stateManager.getMessageAt(-1);
269
+ if (!lastMessage) return;
270
+
271
+ /** @type {MessageContentToolUse[]} */
272
+ const toolUseParts = lastMessage.content.filter(
273
+ (part) => part.type === "tool_use",
274
+ );
275
+
276
+ const isApproval =
277
+ input.length === 1 &&
278
+ input[0].type === "text" &&
279
+ input[0].text.toLocaleLowerCase().match(/^(yes|y|y)$/i);
280
+
281
+ if (isApproval) {
282
+ if (
283
+ /** @type {MessageContentText} */ (input[0]).text.match(/^(YES|Y)$/)
284
+ ) {
285
+ for (const toolUse of toolUseParts) {
286
+ toolUseApprover.allowToolUse(toolUse);
287
+ }
288
+ }
289
+
290
+ const executionResult = await toolExecutor.executeBatch(toolUseParts);
291
+ if (!executionResult.success) {
292
+ stateManager.appendMessages([
293
+ { role: "user", content: executionResult.errors },
294
+ ]);
295
+ return;
296
+ }
297
+
298
+ const toolResults = executionResult.results;
299
+ const result = subagentManager.processToolResults(
300
+ toolUseParts,
301
+ toolResults,
302
+ stateManager.getMessages(),
303
+ );
304
+ stateManager.setMessages(result.messages);
305
+
306
+ if (result.newMessage) {
307
+ stateManager.appendMessages([result.newMessage]);
308
+ } else {
309
+ stateManager.appendMessages([{ role: "user", content: toolResults }]);
310
+ }
311
+ } else {
312
+ // Rejected
313
+ /** @type {MessageContentToolResult[]} */
314
+ const toolResults = toolUseParts.map((toolUse) => ({
315
+ type: "tool_result",
316
+ toolUseId: toolUse.toolUseId,
317
+ toolName: toolUse.toolName,
318
+ content: [{ type: "text", text: "Tool call rejected" }],
319
+ isError: true,
320
+ }));
321
+
322
+ stateManager.appendMessages([
323
+ { role: "user", content: toolResults },
324
+ {
325
+ role: "user",
326
+ content: input,
327
+ },
328
+ ]);
329
+ }
330
+ }
331
+
332
+ async function handleResume() {
333
+ // Resume the conversation stopped by unexpected error, etc.
334
+ // No state changes needed
335
+ }
336
+
337
+ /**
338
+ * @param {UserMessage["content"]} input
339
+ */
340
+ async function handleText(input) {
341
+ stateManager.appendMessages([
342
+ {
343
+ role: "user",
344
+ content: input,
345
+ },
346
+ ]);
347
+ }
348
+
349
+ return {
350
+ /**
351
+ * @param {UserMessage["content"]} input
352
+ */
353
+ async handle(input) {
354
+ const inputType = determineInputType(input);
355
+
356
+ switch (inputType) {
357
+ case "toolApproval":
358
+ await handleToolApproval(input);
359
+ break;
360
+ case "resume":
361
+ await handleResume();
362
+ break;
363
+ case "text":
364
+ await handleText(input);
365
+ break;
366
+ }
367
+ },
368
+ };
369
+ }
@@ -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,45 @@
1
+ /**
2
+ * @typedef {Object} CliArgs
3
+ * @property {string|null} model - Model name with variant
4
+ * @property {boolean} showHelp - Whether to show help message
5
+ */
6
+
7
+ /**
8
+ * Parse command-line arguments.
9
+ * @param {string[]} argv - process.argv or similar
10
+ * @returns {CliArgs}
11
+ */
12
+ export function parseCliArgs(argv) {
13
+ const args = argv.slice(2);
14
+ /** @type {CliArgs} */
15
+ const result = { model: null, showHelp: false };
16
+
17
+ for (let i = 0; i < args.length; i++) {
18
+ if (args[i] === "-m" && args[i + 1]) {
19
+ result.model = args[++i];
20
+ } else if (args[i] === "-h" || args[i] === "--help") {
21
+ result.showHelp = true;
22
+ }
23
+ }
24
+
25
+ return result;
26
+ }
27
+
28
+ /**
29
+ * Print help message and exit.
30
+ * @param {number} [exitCode] - Exit code (default: 0)
31
+ */
32
+ export function printHelp(exitCode = 0) {
33
+ console.log(`
34
+ Usage: agent [options]
35
+
36
+ Options:
37
+ -m <model+variant> Model to use
38
+ -h, --help Show this help message
39
+
40
+ Examples:
41
+ agent -m claude-sonnet-4-6+thinking-16k
42
+ agent -m gpt-5.4+thinking-medium
43
+ `);
44
+ process.exit(exitCode);
45
+ }