@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.
- package/.config/agents.library/code-simplifier.md +5 -0
- package/.config/agents.library/qa-engineer.md +74 -0
- package/.config/agents.library/software-architect.md +278 -0
- package/.config/agents.predefined/worker.md +3 -0
- package/.config/config.predefined.json +825 -0
- package/.config/prompts.library/code-review.md +8 -0
- package/.config/prompts.library/feature-dev.md +6 -0
- package/.config/prompts.predefined/shortcuts/commit-by-user.md +9 -0
- package/.config/prompts.predefined/shortcuts/commit.md +10 -0
- package/.config/prompts.predefined/shortcuts/general-question.md +6 -0
- package/LICENSE +21 -0
- package/README.md +624 -0
- package/bin/plain +3 -0
- package/bin/plain-interrupt +6 -0
- package/bin/plain-notify-desktop +19 -0
- package/bin/plain-notify-terminal-bell +3 -0
- package/package.json +57 -0
- package/sandbox/bin/plain-sandbox +972 -0
- package/src/agent.d.ts +48 -0
- package/src/agent.mjs +159 -0
- package/src/agentLoop.mjs +369 -0
- package/src/agentState.mjs +41 -0
- package/src/cliArgs.mjs +45 -0
- package/src/cliFormatter.mjs +217 -0
- package/src/cliInteractive.mjs +739 -0
- package/src/config.d.ts +48 -0
- package/src/config.mjs +168 -0
- package/src/context/consumeInterruptMessage.mjs +30 -0
- package/src/context/loadAgentRoles.mjs +272 -0
- package/src/context/loadPrompts.mjs +312 -0
- package/src/context/loadUserMessageContext.mjs +147 -0
- package/src/env.mjs +46 -0
- package/src/main.mjs +202 -0
- package/src/mcp.mjs +202 -0
- package/src/model.d.ts +109 -0
- package/src/modelCaller.mjs +29 -0
- package/src/modelDefinition.d.ts +73 -0
- package/src/prompt.mjs +128 -0
- package/src/providers/anthropic.d.ts +248 -0
- package/src/providers/anthropic.mjs +596 -0
- package/src/providers/gemini.d.ts +208 -0
- package/src/providers/gemini.mjs +752 -0
- package/src/providers/openai.d.ts +281 -0
- package/src/providers/openai.mjs +551 -0
- package/src/providers/openaiCompatible.d.ts +147 -0
- package/src/providers/openaiCompatible.mjs +658 -0
- package/src/providers/platform/azure.mjs +42 -0
- package/src/providers/platform/bedrock.mjs +74 -0
- package/src/providers/platform/googleCloud.mjs +34 -0
- package/src/subagent.mjs +247 -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 +98 -0
- package/src/tools/askGoogle.mjs +135 -0
- package/src/tools/delegateToSubagent.d.ts +4 -0
- package/src/tools/delegateToSubagent.mjs +48 -0
- package/src/tools/execCommand.d.ts +22 -0
- package/src/tools/execCommand.mjs +200 -0
- package/src/tools/fetchWebPage.mjs +96 -0
- package/src/tools/patchFile.d.ts +4 -0
- package/src/tools/patchFile.mjs +96 -0
- package/src/tools/reportAsSubagent.d.ts +3 -0
- package/src/tools/reportAsSubagent.mjs +44 -0
- package/src/tools/tavilySearch.d.ts +6 -0
- package/src/tools/tavilySearch.mjs +57 -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/utils/evalJSONConfig.mjs +48 -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 +28 -0
- package/src/utils/parseFileRange.mjs +18 -0
- package/src/utils/readFileRange.mjs +33 -0
- 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
|
+
}
|