@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
package/bin/plain CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import("../dist/main.mjs");
3
+ import("../src/main.mjs");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iinm/plain-agent",
3
- "version": "1.8.4",
3
+ "version": "1.8.6",
4
4
  "description": "A lightweight CLI-based coding agent",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -17,27 +17,26 @@
17
17
  "bin",
18
18
  "sandbox/bin",
19
19
  "config",
20
- "dist"
20
+ "src/**/*.mjs",
21
+ "src/**/*.d.ts",
22
+ "!src/**/*.test.mjs",
23
+ "!src/**/*.test.*.mjs",
24
+ "!src/**/*.playground.mjs",
25
+ "!src/**/test/"
21
26
  ],
22
27
  "engines": {
23
28
  "node": ">=22"
24
29
  },
25
30
  "scripts": {
26
- "build": "esbuild src/main.mjs --bundle --platform=node --target=node22 --outfile=dist/main.mjs --minify --sourcemap --format=esm --banner:js='import { createRequire } from \"node:module\";const require=createRequire(import.meta.url);'",
27
- "prepublishOnly": "npm run build",
28
31
  "check": "npm run lint && tsc && npm run test",
29
32
  "test": "node --test",
30
33
  "lint": "npx @biomejs/biome check",
31
34
  "fix": "npx @biomejs/biome check --fix"
32
35
  },
33
- "dependencies": {
34
- "diff": "^8.0.4",
35
- "yaml": "^2.8.3"
36
- },
36
+ "dependencies": {},
37
37
  "devDependencies": {
38
38
  "@biomejs/biome": "^2.4.12",
39
39
  "@types/node": "^22.19.17",
40
- "esbuild": "^0.28.0",
41
40
  "typescript": "^6.0.2"
42
41
  }
43
42
  }
@@ -731,6 +731,14 @@ setup_container_firewall() {
731
731
  # allow established connections
732
732
  iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
733
733
  iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
734
+
735
+ # allow localhost on IPv6
736
+ ip6tables -A INPUT -i lo -j ACCEPT
737
+ ip6tables -A OUTPUT -o lo -j ACCEPT
738
+
739
+ # allow established connections on IPv6
740
+ ip6tables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
741
+ ip6tables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
734
742
 
735
743
  # allow given addresses
736
744
  for address in "${addresses[@]}"; do
@@ -750,6 +758,11 @@ setup_container_firewall() {
750
758
  fi
751
759
  done
752
760
  iptables -A OUTPUT -p tcp -m set --match-set allow_list dst,dst -j ACCEPT
761
+
762
+ # Reject unauthorized TCP connections immediately with RST
763
+ # instead of silently dropping and causing timeout
764
+ iptables -A OUTPUT -p tcp -j REJECT --reject-with tcp-reset
765
+ ip6tables -A OUTPUT -p tcp -j REJECT --reject-with tcp-reset
753
766
  }
754
767
 
755
768
  setup_container_user() {
package/src/agent.d.ts ADDED
@@ -0,0 +1,52 @@
1
+ import type { EventEmitter } from "node:events";
2
+ import type { AgentRole } from "./context/loadAgentRoles.mjs";
3
+ import type { CostConfig, CostSummary } from "./costTracker.mjs";
4
+ import type {
5
+ CallModel,
6
+ Message,
7
+ MessageContentImage,
8
+ MessageContentText,
9
+ PartialMessageContent,
10
+ ProviderTokenUsage,
11
+ } from "./model";
12
+ import type { Tool, ToolUseApprover } from "./tool";
13
+
14
+ export type Agent = {
15
+ userEventEmitter: UserEventEmitter;
16
+ agentEventEmitter: AgentEventEmitter;
17
+ agentCommands: AgentCommands;
18
+ };
19
+
20
+ export type AgentCommands = {
21
+ dumpMessages: () => Promise<void>;
22
+ loadMessages: () => Promise<void>;
23
+ getCostSummary: () => CostSummary;
24
+ pauseAutoApprove: () => void;
25
+ };
26
+
27
+ type UserEventMap = {
28
+ userInput: [(MessageContentText | MessageContentImage)[]];
29
+ };
30
+
31
+ export type UserEventEmitter = EventEmitter<UserEventMap>;
32
+
33
+ type AgentEventMap = {
34
+ message: [Message];
35
+ partialMessageContent: [PartialMessageContent];
36
+ error: [Error];
37
+ toolUseRequest: [];
38
+ turnEnd: [];
39
+ providerTokenUsage: [ProviderTokenUsage];
40
+ subagentSwitched: [{ name: string } | null];
41
+ };
42
+
43
+ export type AgentEventEmitter = EventEmitter<AgentEventMap>;
44
+
45
+ export type AgentConfig = {
46
+ callModel: CallModel;
47
+ prompt: string;
48
+ tools: Tool[];
49
+ toolUseApprover: ToolUseApprover;
50
+ agentRoles: Map<string, AgentRole>;
51
+ modelCostConfig?: CostConfig;
52
+ };
package/src/agent.mjs ADDED
@@ -0,0 +1,204 @@
1
+ /**
2
+ * @import { Agent, AgentConfig, AgentEventEmitter, UserEventEmitter } from "./agent"
3
+ * @import { Tool, ToolDefinition } from "./tool"
4
+ * @import { CompactContextInput } from "./tools/compactContext"
5
+ * @import { SwitchToSubagentInput } from "./tools/switchToSubagent"
6
+ * @import { SwitchToMainAgentInput } from "./tools/switchToMainAgent"
7
+ */
8
+
9
+ import { EventEmitter } from "node:events";
10
+ import fs from "node:fs/promises";
11
+ import { createAgentLoop } from "./agentLoop.mjs";
12
+ import { createStateManager } from "./agentState.mjs";
13
+ import { createCostTracker } from "./costTracker.mjs";
14
+ import { MESSAGES_DUMP_FILE_PATH } from "./env.mjs";
15
+ import { createSubagentManager } from "./subagent.mjs";
16
+ import { createToolExecutor } from "./toolExecutor.mjs";
17
+ import {
18
+ compactContextToolName,
19
+ readMemoryForCompaction,
20
+ } from "./tools/compactContext.mjs";
21
+ import { switchToMainAgentToolName } from "./tools/switchToMainAgent.mjs";
22
+ import { switchToSubagentToolName } from "./tools/switchToSubagent.mjs";
23
+
24
+ /**
25
+ * @param {AgentConfig} config
26
+ * @returns {Agent}
27
+ */
28
+ export function createAgent({
29
+ callModel,
30
+ prompt,
31
+ tools,
32
+ toolUseApprover,
33
+ agentRoles,
34
+ modelCostConfig,
35
+ }) {
36
+ /** @type {UserEventEmitter} */
37
+ const userEventEmitter = new EventEmitter();
38
+ /** @type {AgentEventEmitter} */
39
+ const agentEventEmitter = new EventEmitter();
40
+
41
+ const costTracker = createCostTracker(modelCostConfig);
42
+
43
+ agentEventEmitter.on("providerTokenUsage", (usage) => {
44
+ costTracker.recordUsage(usage);
45
+ });
46
+
47
+ const stateManager = createStateManager(
48
+ [
49
+ {
50
+ role: "system",
51
+ content: [{ type: "text", text: prompt }],
52
+ },
53
+ ],
54
+ {
55
+ onMessagesAppended: (newMessages) => {
56
+ const lastMessage = newMessages.at(-1);
57
+ if (!lastMessage) {
58
+ return;
59
+ }
60
+ agentEventEmitter.emit("message", lastMessage);
61
+ },
62
+ },
63
+ );
64
+
65
+ const subagentManager = createSubagentManager(agentRoles, {
66
+ onSubagentSwitched: (subagent) => {
67
+ agentEventEmitter.emit("subagentSwitched", subagent);
68
+ },
69
+ });
70
+
71
+ /**
72
+ * @param {SwitchToSubagentInput} input
73
+ */
74
+ const switchToSubagentImpl = async (input) => {
75
+ const result = subagentManager.switchToSubagent(
76
+ input.name,
77
+ input.goal,
78
+ stateManager.getMessages().length - 1,
79
+ );
80
+ if (!result.success) {
81
+ return new Error(result.error);
82
+ }
83
+ return result.value;
84
+ };
85
+
86
+ /**
87
+ * @param {SwitchToMainAgentInput} input
88
+ */
89
+ const switchToMainAgentImpl = async (input) => {
90
+ const result = await subagentManager.switchToMainAgent(input.memoryPath);
91
+ if (!result.success) {
92
+ return new Error(result.error);
93
+ }
94
+ return result.memoryContent;
95
+ };
96
+
97
+ /**
98
+ * @param {Record<string, unknown>} rawInput
99
+ */
100
+ const compactContextImpl = async (rawInput) => {
101
+ if (subagentManager.isSubagentActive()) {
102
+ return new Error(
103
+ "compact_context cannot be used while running as a subagent. " +
104
+ "Call switch_to_main_agent to return to the main agent first.",
105
+ );
106
+ }
107
+ const input = /** @type {CompactContextInput} */ (rawInput);
108
+ return await readMemoryForCompaction(input);
109
+ };
110
+
111
+ /** @type {Map<string, Tool>} */
112
+ const toolByName = new Map();
113
+ for (const tool of tools) {
114
+ if (tool.def.name === switchToSubagentToolName && tool.injectImpl) {
115
+ tool.injectImpl(switchToSubagentImpl);
116
+ }
117
+ if (tool.def.name === switchToMainAgentToolName && tool.injectImpl) {
118
+ tool.injectImpl(switchToMainAgentImpl);
119
+ }
120
+ if (tool.def.name === compactContextToolName && tool.injectImpl) {
121
+ tool.injectImpl(compactContextImpl);
122
+ }
123
+ toolByName.set(tool.def.name, tool);
124
+ }
125
+
126
+ /** @type {ToolDefinition[]} */
127
+ const toolDefs = tools.map(({ def }) => def);
128
+
129
+ const toolExecutor = createToolExecutor(toolByName, {
130
+ exclusiveToolNames: [switchToSubagentToolName, switchToMainAgentToolName],
131
+ });
132
+
133
+ async function dumpMessages() {
134
+ const filePath = MESSAGES_DUMP_FILE_PATH;
135
+ try {
136
+ await fs.writeFile(
137
+ filePath,
138
+ JSON.stringify(stateManager.getMessages(), null, 2),
139
+ );
140
+ console.log(`Messages dumped to ${filePath}`);
141
+ } catch (error) {
142
+ const message = error instanceof Error ? error.message : String(error);
143
+ console.error(`Error dumping messages: ${message}`);
144
+ }
145
+ }
146
+
147
+ async function loadMessages() {
148
+ const filePath = MESSAGES_DUMP_FILE_PATH;
149
+ try {
150
+ const data = await fs.readFile(filePath, "utf-8");
151
+ const loadedMessages = JSON.parse(data);
152
+ if (Array.isArray(loadedMessages)) {
153
+ // Keep the system message (index 0) and replace the rest
154
+ stateManager.setMessages([
155
+ stateManager.getMessageAt(0),
156
+ ...loadedMessages.slice(1),
157
+ ]);
158
+ console.log(`Messages loaded from ${filePath}`);
159
+ } else {
160
+ console.error("Error loading messages: Invalid format in file.");
161
+ }
162
+ } catch (error) {
163
+ if (error instanceof Error) {
164
+ console.error(`Error loading messages: ${error.message}`);
165
+ }
166
+ }
167
+ }
168
+
169
+ // Pause signal: set by Ctrl-C during agent execution, checked after each tool batch completes
170
+ let paused = false;
171
+ /** @type {import("./agentLoop.mjs").PauseSignal} */
172
+ const pauseSignal = {
173
+ isPaused: () => paused,
174
+ reset: () => {
175
+ paused = false;
176
+ },
177
+ };
178
+
179
+ const agentLoop = createAgentLoop({
180
+ callModel,
181
+ stateManager,
182
+ toolDefs,
183
+ toolExecutor,
184
+ agentEventEmitter,
185
+ toolUseApprover,
186
+ subagentManager,
187
+ pauseSignal,
188
+ });
189
+
190
+ userEventEmitter.on("userInput", agentLoop.handleUserInput);
191
+
192
+ return {
193
+ userEventEmitter,
194
+ agentEventEmitter,
195
+ agentCommands: {
196
+ dumpMessages,
197
+ loadMessages,
198
+ getCostSummary: () => costTracker.calculateCost(),
199
+ pauseAutoApprove: () => {
200
+ paused = true;
201
+ },
202
+ },
203
+ };
204
+ }