@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,160 @@
1
+ /**
2
+ * @import { StructuredToolResultContent, Tool, ToolImplementation } from "./tool";
3
+ * @import { MCPServerConfig } from "./config";
4
+ */
5
+
6
+ import { mkdir } from "node:fs/promises";
7
+ import path from "node:path";
8
+ import { AGENT_PROJECT_METADATA_DIR } from "./env.mjs";
9
+ import { createMCPClient } from "./mcpClient.mjs";
10
+ import { writeTmpFile } from "./tmpfile.mjs";
11
+ import { noThrow } from "./utils/noThrow.mjs";
12
+
13
+ /** @typedef {import("./mcpClient.mjs").MCPClient} MCPClient */
14
+
15
+ const OUTPUT_MAX_LENGTH = 1024 * 8;
16
+
17
+ /**
18
+ * @typedef {Object} SetupMCPServrResult
19
+ * @property {Tool[]} tools
20
+ * @property {string} stderrLogPath
21
+ * @property {() => Promise<void>} cleanup
22
+ */
23
+
24
+ /**
25
+ * @param {string} serverName
26
+ * @param {MCPServerConfig} serverConfig
27
+ * @returns {Promise<SetupMCPServrResult>}
28
+ */
29
+ export async function setupMCPServer(serverName, serverConfig) {
30
+ const { options, ...params } = serverConfig;
31
+
32
+ // Ensure log directory exists and open stderr log file
33
+ const logDir = path.join(AGENT_PROJECT_METADATA_DIR, "logs");
34
+ await mkdir(logDir, { recursive: true });
35
+ const logPath = path.join(logDir, `mcp--${serverName}.stderr`);
36
+
37
+ const client = await createMCPClient({
38
+ ...params,
39
+ stderr: logPath,
40
+ });
41
+
42
+ const tools = (await createMCPTools(serverName, client)).filter(
43
+ (tool) =>
44
+ !options?.enabledTools ||
45
+ options.enabledTools.find((enabledToolName) =>
46
+ tool.def.name.endsWith(`__${enabledToolName}`),
47
+ ),
48
+ );
49
+
50
+ return {
51
+ tools,
52
+ stderrLogPath: logPath,
53
+ cleanup: async () => {
54
+ await client.close();
55
+ },
56
+ };
57
+ }
58
+
59
+ /**
60
+ * @param {string} serverName
61
+ * @param {MCPClient} client
62
+ * @returns {Promise<Tool[]>}
63
+ */
64
+ async function createMCPTools(serverName, client) {
65
+ const { tools: mcpTools } = await client.listTools();
66
+ /** @type {Tool[]} */
67
+ const tools = mcpTools
68
+ .filter((tool) => {
69
+ // Remove unsupported tools
70
+ return ![""].includes(tool.name);
71
+ })
72
+ .map((tool) => {
73
+ return {
74
+ def: {
75
+ name: `mcp__${serverName}__${tool.name}`,
76
+ description: tool.description || `${tool.name} tool`,
77
+ inputSchema: tool.inputSchema,
78
+ },
79
+
80
+ /** @type {ToolImplementation} */
81
+ impl: async (input) =>
82
+ noThrow(async () => {
83
+ const result = await client.callTool({
84
+ name: tool.name,
85
+ arguments: input,
86
+ });
87
+
88
+ const resultStringRaw = JSON.stringify(result, null, 2);
89
+
90
+ /** @type {StructuredToolResultContent[]} */
91
+ const contentParts = [];
92
+ /** @type {string[]} */
93
+ const contentStrings = [];
94
+ let contentContainsImage = false;
95
+ if (Array.isArray(result.content)) {
96
+ for (const part of result.content) {
97
+ if ("text" in part && typeof part.text === "string") {
98
+ contentParts.push({
99
+ type: "text",
100
+ text: part.text,
101
+ });
102
+ contentStrings.push(part.text);
103
+ } else if (
104
+ part.type === "image" &&
105
+ typeof part.mimeType === "string" &&
106
+ typeof part.data === "string"
107
+ ) {
108
+ contentParts.push({
109
+ type: "image",
110
+ data: part.data,
111
+ mimeType: part.mimeType,
112
+ });
113
+ contentContainsImage = true;
114
+ } else {
115
+ console.error(
116
+ `Unsupported content part from MCP: ${JSON.stringify(part)}`,
117
+ );
118
+ }
119
+ }
120
+ }
121
+
122
+ if (contentContainsImage) {
123
+ return contentParts;
124
+ }
125
+
126
+ const resultString = contentStrings.join("\n\n") || resultStringRaw;
127
+
128
+ /** @type {string} */
129
+ let formmatted = resultString;
130
+ let fileExtension = "txt";
131
+
132
+ try {
133
+ const parsed = JSON.parse(resultString);
134
+ formmatted = JSON.stringify(parsed, null, 2);
135
+ fileExtension = "json";
136
+ } catch {
137
+ // not JSON
138
+ }
139
+
140
+ if (formmatted.length <= OUTPUT_MAX_LENGTH) {
141
+ return formmatted;
142
+ }
143
+
144
+ const filePath = await writeTmpFile(
145
+ formmatted,
146
+ tool.name,
147
+ fileExtension,
148
+ );
149
+ const lineCount = formmatted.split("\n").length;
150
+
151
+ return [
152
+ `Content is large (${resultString.length} characters, ${lineCount} lines) and saved to ${filePath}`,
153
+ "Use exec_command tool to find relevant parts.",
154
+ ].join("\n");
155
+ }),
156
+ };
157
+ });
158
+
159
+ return tools;
160
+ }
package/src/model.d.ts ADDED
@@ -0,0 +1,109 @@
1
+ import type { ToolDefinition } from "./tool";
2
+
3
+ export type CallModel = (input: ModelInput) => Promise<ModelOutput | Error>;
4
+
5
+ export type ModelInput = {
6
+ messages: Message[];
7
+ tools?: ToolDefinition[];
8
+ onPartialMessageContent?: (partialContent: PartialMessageContent) => void;
9
+ };
10
+
11
+ export type ModelOutput = {
12
+ message: Message;
13
+ providerTokenUsage?: ProviderTokenUsage;
14
+ };
15
+
16
+ export type ProviderTokenUsage = Record<
17
+ string,
18
+ number | string | Record<string, number>
19
+ >;
20
+
21
+ export type PartialMessageContent = {
22
+ type: string;
23
+ position: "start" | "stop" | "delta";
24
+ content?: string;
25
+ };
26
+
27
+ export type Message = SystemMessage | UserMessage | AssistantMessage;
28
+
29
+ export type SystemMessage = {
30
+ role: "system";
31
+ content: MessageContentText[];
32
+ };
33
+
34
+ export type UserMessage = {
35
+ role: "user";
36
+ content: (
37
+ | MessageContentText
38
+ | MessageContentImage
39
+ | MessageContentToolResult
40
+ )[];
41
+ };
42
+
43
+ export type AssistantMessage = {
44
+ role: "assistant";
45
+ content: (
46
+ | MessageContentThinking
47
+ | MessageContentRedactedThinking
48
+ | MessageContentText
49
+ | MessageContentToolUse
50
+ )[];
51
+ provider?: MessageContentProvider;
52
+ };
53
+
54
+ export type MessageContentThinking = {
55
+ type: "thinking";
56
+ thinking: string;
57
+ provider?: MessageContentProvider;
58
+ };
59
+
60
+ export type MessageContentRedactedThinking = {
61
+ type: "redacted_thinking";
62
+ provider?: MessageContentProvider;
63
+ };
64
+
65
+ export type MessageContentText = {
66
+ type: "text";
67
+ text: string;
68
+ provider?: MessageContentProvider;
69
+ };
70
+
71
+ export type MessageContentImage = {
72
+ type: "image";
73
+
74
+ // base64 encoded image data
75
+ data: string;
76
+
77
+ // e.g., image/jpeg
78
+ mimeType: string;
79
+ };
80
+
81
+ export type MessageContentToolUse = {
82
+ type: "tool_use";
83
+ toolUseId: string;
84
+ toolName: string;
85
+ input: Record<string, unknown>;
86
+ provider?: MessageContentProvider;
87
+ };
88
+
89
+ export type MessageContentToolResult = {
90
+ type: "tool_result";
91
+ toolUseId: string;
92
+ toolName: string;
93
+ content: (MessageContentText | MessageContentImage)[];
94
+ isError?: boolean;
95
+ };
96
+
97
+ export type MessageContentProvider = {
98
+ /**
99
+ * Raw source data from the provider
100
+ * (original message, response, output items, etc.)
101
+ */
102
+ source?: unknown;
103
+
104
+ /**
105
+ * Provider-specific fields that are directly merged
106
+ * into the content part sent to the provider API.
107
+ */
108
+ fields?: Record<string, unknown>;
109
+ };
@@ -0,0 +1,32 @@
1
+ import { callAnthropicModel } from "./providers/anthropic.mjs";
2
+ import { callBedrockConverseModel } from "./providers/bedrock.mjs";
3
+ import { createCacheEnabledGeminiModelCaller } from "./providers/gemini.mjs";
4
+ import { callOpenAIModel } from "./providers/openai.mjs";
5
+ import { callOpenAICompatibleModel } from "./providers/openaiCompatible.mjs";
6
+
7
+ /**
8
+ * @param {import("./modelDefinition").ModelDefinition} modelDef
9
+ * @returns {import("./model").CallModel}
10
+ */
11
+ export function createModelCaller(modelDef) {
12
+ const { platform, model } = modelDef;
13
+
14
+ switch (model.format) {
15
+ case "anthropic":
16
+ return (input) => callAnthropicModel(platform, model.config, input);
17
+ case "gemini": {
18
+ const modelCaller = createCacheEnabledGeminiModelCaller(
19
+ platform,
20
+ model.config,
21
+ );
22
+ return (input) => modelCaller(model.config, input);
23
+ }
24
+ case "openai-responses":
25
+ return (input) => callOpenAIModel(platform, model.config, input);
26
+ case "openai-messages":
27
+ return (input) =>
28
+ callOpenAICompatibleModel(platform, model.config, input);
29
+ case "bedrock-converse":
30
+ return (input) => callBedrockConverseModel(platform, model.config, input);
31
+ }
32
+ }
@@ -0,0 +1,92 @@
1
+ import { AnthropicModelConfig } from "./providers/anthropic";
2
+ import { BedrockConverseModelConfig } from "./providers/bedrock";
3
+ import { GeminiModelConfig } from "./providers/gemini";
4
+ import { OpenAIModelConfig } from "./providers/openai";
5
+ import { OpenAICompatibleModelConfig } from "./providers/openaiCompatible";
6
+
7
+ export type ModelDefinition = {
8
+ name: string;
9
+ variant: string;
10
+ platform: PlatformConfig;
11
+ model: ModelConfig;
12
+ cost?: CostConfig;
13
+ };
14
+
15
+ export type PlatformConfig =
16
+ | {
17
+ name: "anthropic";
18
+ variant: string;
19
+ baseURL: string;
20
+ customHeaders?: Record<string, string>;
21
+ apiKey: string;
22
+ }
23
+ | {
24
+ name: "gemini";
25
+ variant: string;
26
+ baseURL: string;
27
+ customHeaders?: Record<string, string>;
28
+ apiKey: string;
29
+ }
30
+ | {
31
+ name: "openai";
32
+ variant: string;
33
+ baseURL: string;
34
+ customHeaders?: Record<string, string>;
35
+ apiKey: string;
36
+ }
37
+ | {
38
+ name: "openai-compatible";
39
+ variant: string;
40
+ baseURL: string;
41
+ customHeaders?: Record<string, string>;
42
+ apiKey: string;
43
+ }
44
+ | {
45
+ name: "azure";
46
+ variant: string;
47
+ baseURL: string;
48
+ customHeaders?: Record<string, string>;
49
+ azureConfigDir?: string;
50
+ }
51
+ | {
52
+ name: "bedrock";
53
+ variant: string;
54
+ baseURL: string;
55
+ customHeaders?: Record<string, string>;
56
+ awsProfile: string;
57
+ }
58
+ | {
59
+ name: "vertex-ai";
60
+ variant: string;
61
+ baseURL: string;
62
+ customHeaders?: Record<string, string>;
63
+ account?: string;
64
+ };
65
+
66
+ export type ModelConfig =
67
+ | {
68
+ format: "anthropic";
69
+ config: AnthropicModelConfig;
70
+ }
71
+ | {
72
+ format: "gemini";
73
+ config: GeminiModelConfig;
74
+ }
75
+ | {
76
+ format: "openai-responses";
77
+ config: OpenAIModelConfig;
78
+ }
79
+ | {
80
+ format: "openai-messages";
81
+ config: OpenAICompatibleModelConfig;
82
+ }
83
+ | {
84
+ format: "bedrock-converse";
85
+ config: BedrockConverseModelConfig;
86
+ };
87
+
88
+ export type CostConfig = {
89
+ currency: string;
90
+ unit: string;
91
+ costs: Record<string, number>;
92
+ };
package/src/prompt.mjs ADDED
@@ -0,0 +1,138 @@
1
+ /**
2
+ * @typedef {object} PromptConfig
3
+ * @property {string} username
4
+ * @property {string} modelName
5
+ * @property {string} workingDir - The current working directory.
6
+ * @property {string} today - Today's date in YYYY-MM-DD format.
7
+ * @property {string} sessionId
8
+ * @property {string} tmuxSessionId
9
+ * @property {string} projectMetadataDir - The directory where memory files are stored.
10
+ * @property {Map<string, import('./context/loadAgentRoles.mjs').AgentRole>} agentRoles - Available agent roles.
11
+ * @property {{filePath: string, description: string}[]} skills
12
+ */
13
+
14
+ /**
15
+ * @param {PromptConfig} config
16
+ * @returns {string}
17
+ */
18
+ export function createPrompt({
19
+ username,
20
+ modelName,
21
+ sessionId,
22
+ today,
23
+ tmuxSessionId,
24
+ workingDir,
25
+ projectMetadataDir,
26
+ agentRoles,
27
+ skills,
28
+ }) {
29
+ const agentRoleDescriptions = Array.from(agentRoles.entries())
30
+ .map(([id, role]) => {
31
+ const desc =
32
+ role.description.length > 100
33
+ ? `${role.description.substring(0, 100)}...`
34
+ : role.description;
35
+ return `- ${id}: ${desc}`;
36
+ })
37
+ .join("\n");
38
+
39
+ const skillDescriptions = skills
40
+ .map((skill) => {
41
+ const desc =
42
+ skill.description.length > 100
43
+ ? `${skill.description.substring(0, 100)}...`
44
+ : skill.description;
45
+ return `- ${skill.filePath}\n ${desc}`;
46
+ })
47
+ .join("\n");
48
+
49
+ return `
50
+ # Communication Style
51
+
52
+ - Respond in the user's language.
53
+ - Address the user by their name, rather than "user".
54
+ - Use emojis sparingly to highlight key points.
55
+
56
+ # Memory Files
57
+
58
+ - Create/Update memory files after creating/updating a plan, completing milestones, encountering issues, or making decisions.
59
+ - Update existing task memory when continuing the same task.
60
+ - Write the memory content in the user's language.
61
+ - Ensure self-containment: The file must be standalone. A reader should fully understand the task context, logic and progress without any other references.
62
+
63
+ Memory files should include:
64
+ - Task overview: What the task is, why it's being done, requirements and constraints
65
+ - Context: Relevant documentation, source files, commands, and resources referenced
66
+ - Progress tracking: Completed milestones with evidence, current status, and next steps
67
+ - Decision records: Important decisions made, alternatives considered, and rationale
68
+ - Findings and learnings: Key discoveries, challenges encountered, and solutions applied
69
+ - Future considerations: Known limitations, potential improvements, and follow-up items
70
+
71
+ # Tools
72
+
73
+ Call multiple tools at once when they don't depend on each other's results.
74
+
75
+ ## exec_command
76
+
77
+ - Use relative paths.
78
+ - Avoid bash -c unless pipes (|) or redirection (>, <) are required.
79
+
80
+ Examples:
81
+ - List directories or find files: fd [".", "./", "--max-depth", "3", "--type", "d", "--hidden"]
82
+ - Search for strings: rg ["--heading", "--line-number", "pattern", "./"]
83
+ - Read specific line ranges (max 200 lines): sed ["-n", "1,200p", "file.txt"]
84
+ - Manage GitHub issues and PRs:
85
+ Get PR details: gh ["pr", "view", "123", "--json", "title,body,url"]
86
+ Get PR comment: gh ["api", "--method", "GET", "repos/<owner>/<repo>/pulls/comments/<id>", "--jq", "{user: .user.login, path: .path, line: .line, body: .body}"]
87
+
88
+ ## tmux_command
89
+
90
+ - Only use when the user explicitly requests it.
91
+ - Create a new session with the given tmux session id.
92
+ - Use relative paths.
93
+
94
+ Examples:
95
+ - Start session: new-session ["-d", "-s", "<tmux-session-id>"]
96
+ - Detect window number to send keys: list-windows ["-t", "<tmux-session-id>"]
97
+ - Get output of window before sending keys: capture-pane ["-p", "-t", "<tmux-session-id>:<window>"]
98
+ - Send key to session: send-keys ["-t", "<tmux-session-id>:<window>", "echo hello", "Enter"]
99
+ - Delete line: send-keys ["-t", "<tmux-session-id>:<window>", "C-a", "C-k"]
100
+
101
+ # Project Rules and Skills
102
+
103
+ Discover and apply project-specific rules and reusable skills.
104
+
105
+ ## AGENTS.md (falling back to CLAUDE.md if not found): Project-specific rules, conventions, and commands.
106
+
107
+ Find: fd ["^(AGENTS|CLAUDE)\\.md$", "./", "--hidden", "--max-depth", "5"]
108
+ Read from the project root to the directory you're working in: ./AGENTS.md → dir/AGENTS.md → dir/subdir/AGENTS.md
109
+ Apply rules when working in that directory
110
+
111
+ ## SKILL.md: Reusable workflows with specialized knowledge
112
+
113
+ If skill matches task: read full file and apply the workflow
114
+
115
+ ${skillDescriptions}
116
+
117
+ # Environment
118
+
119
+ - User name: ${username}
120
+ - Your model name: ${modelName}
121
+ - Current working directory: ${workingDir}
122
+ - Today's date: ${today}
123
+ - Session id: ${sessionId}
124
+ - Tmux session id: ${tmuxSessionId}
125
+ - Memory file path: ${projectMetadataDir}/memory/${sessionId}--<kebab-case-title>.md
126
+
127
+ Available subagents:
128
+ ${agentRoleDescriptions}
129
+ - custom:<role-name>: Use this for ad-hoc roles not listed above (e.g., custom:explore, custom:plan).
130
+ `.trim();
131
+ }
132
+
133
+ export const CLAUDE_CODE_COMPATIBILITY_NOTES = `# Environment Constraints
134
+
135
+ - Use memory file to manage todo list.
136
+ - Subagents cannot run in parallel. Switch to them one at a time.
137
+ - Use AGENTS.md instead when CLAUDE.md is absent.
138
+ - If instructed to use "haiku agent", "sonnet agent", or "opus agent", use "worker" instead.`;