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