banana-code 1.2.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/LICENSE +21 -0
- package/README.md +246 -0
- package/banana.js +5464 -0
- package/lib/agenticRunner.js +1884 -0
- package/lib/borderRenderer.js +41 -0
- package/lib/commandRunner.js +205 -0
- package/lib/completer.js +286 -0
- package/lib/config.js +301 -0
- package/lib/contextBuilder.js +324 -0
- package/lib/diffViewer.js +295 -0
- package/lib/fileManager.js +224 -0
- package/lib/historyManager.js +124 -0
- package/lib/hookManager.js +1143 -0
- package/lib/imageHandler.js +268 -0
- package/lib/inlineComplete.js +192 -0
- package/lib/interactivePicker.js +254 -0
- package/lib/lmStudio.js +226 -0
- package/lib/markdownRenderer.js +423 -0
- package/lib/mcpClient.js +288 -0
- package/lib/modelRegistry.js +350 -0
- package/lib/monkeyModels.js +97 -0
- package/lib/oauthOpenAI.js +167 -0
- package/lib/parser.js +134 -0
- package/lib/promptManager.js +96 -0
- package/lib/providerClients.js +1014 -0
- package/lib/providerManager.js +130 -0
- package/lib/providerStore.js +413 -0
- package/lib/statusBar.js +283 -0
- package/lib/streamHandler.js +306 -0
- package/lib/subAgentManager.js +406 -0
- package/lib/tokenCounter.js +132 -0
- package/lib/visionAnalyzer.js +163 -0
- package/lib/watcher.js +138 -0
- package/models.json +57 -0
- package/package.json +42 -0
- package/prompts/base.md +23 -0
- package/prompts/code-agent-glm.md +16 -0
- package/prompts/code-agent-gptoss.md +25 -0
- package/prompts/code-agent-nemotron.md +17 -0
- package/prompts/code-agent-qwen.md +20 -0
- package/prompts/code-agent.md +70 -0
- package/prompts/plan.md +44 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monkey Models API Client for Banana Code CLI
|
|
3
|
+
* Cloud provider wrapping OpenAICompatibleClient.
|
|
4
|
+
*
|
|
5
|
+
* Base URL: https://monkey-models-production.up.railway.app
|
|
6
|
+
* Tiers: silverback, mandrill, gibbon, tamarin
|
|
7
|
+
* Auth: Bearer token in Authorization header
|
|
8
|
+
* Vision: image_url content blocks (server proxies to Gemini Flash)
|
|
9
|
+
* The server handles personality injection, so no system prompts needed.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { OpenAICompatibleClient } = require('./providerClients');
|
|
13
|
+
|
|
14
|
+
const MONKEY_MODELS_URL = 'https://monkey-models-production.up.railway.app';
|
|
15
|
+
// Default token for Banana Code's own Monkey Models server.
|
|
16
|
+
// This is a shared service token (not a user secret). Users can override via env var.
|
|
17
|
+
const MONKEY_MODELS_DEFAULT_TOKEN = '086399eca157e4ad2fc0fecfb254da1118d226ac53371757267388b23bd10fa6';
|
|
18
|
+
const MONKEY_MODELS_TOKEN = process.env.BANANA_MONKEY_TOKEN || MONKEY_MODELS_DEFAULT_TOKEN;
|
|
19
|
+
|
|
20
|
+
const TIERS = ['silverback', 'mandrill', 'gibbon', 'tamarin'];
|
|
21
|
+
|
|
22
|
+
class MonkeyModelsClient extends OpenAICompatibleClient {
|
|
23
|
+
constructor(options = {}) {
|
|
24
|
+
super({
|
|
25
|
+
label: 'Monkey Models',
|
|
26
|
+
baseUrl: options.baseUrl || MONKEY_MODELS_URL,
|
|
27
|
+
bearerToken: options.token || MONKEY_MODELS_TOKEN,
|
|
28
|
+
...options
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Sanitizes error messages to prevent leaking upstream provider details (like OpenRouter).
|
|
34
|
+
* @param {string} errorText The raw error text from the provider.
|
|
35
|
+
* @returns {string} A sanitized error message.
|
|
36
|
+
*/
|
|
37
|
+
_sanitizeError(errorText) {
|
|
38
|
+
if (!errorText) return '';
|
|
39
|
+
// If it looks like it contains OpenRouter or specific provider JSON/strings,
|
|
40
|
+
// we want to strip that out and just provide a clean error.
|
|
41
|
+
// The user's error shows: "OpenRouter 429: {\"error\":{\"message\":\"Provider returned error\"...}}"
|
|
42
|
+
|
|
43
|
+
// Check if it's an error that contains JSON or common provider markers
|
|
44
|
+
if (errorText.includes('OpenRouter') || errorText.includes('provider_name') || errorText.includes('is_byok')) {
|
|
45
|
+
return 'Service is temporarily unavailable or rate-limited. Please try again shortly.';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return errorText;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Overrides _request to sanitize error messages for Monkey Models.
|
|
53
|
+
*/
|
|
54
|
+
async _request(path, body, signal) {
|
|
55
|
+
try {
|
|
56
|
+
return await super._request(path, body, signal);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
if (err.message.startsWith('Monkey Models error')) {
|
|
59
|
+
// Extract the part after the prefix
|
|
60
|
+
const parts = err.message.split(': ');
|
|
61
|
+
if (parts.length > 1) {
|
|
62
|
+
const originalError = parts.slice(1).join(': ');
|
|
63
|
+
const sanitized = this._sanitizeError(originalError);
|
|
64
|
+
throw new Error(`Monkey Models error: ${sanitized}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* GET /health - no auth required.
|
|
73
|
+
* Returns parsed JSON on success, null on failure.
|
|
74
|
+
*/
|
|
75
|
+
async healthCheck() {
|
|
76
|
+
try {
|
|
77
|
+
const response = await fetch(`${this.baseUrl}/health`, {
|
|
78
|
+
signal: AbortSignal.timeout(5000)
|
|
79
|
+
});
|
|
80
|
+
if (!response.ok) return null;
|
|
81
|
+
return await response.json();
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check connectivity via health endpoint first, then fall back to parent.
|
|
89
|
+
*/
|
|
90
|
+
async isConnected(options = {}) {
|
|
91
|
+
const health = await this.healthCheck();
|
|
92
|
+
if (health) return true;
|
|
93
|
+
return super.isConnected(options);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = { MonkeyModelsClient, MONKEY_MODELS_URL, TIERS };
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
const OPENAI_AUTH_ISSUER = 'https://auth.openai.com';
|
|
2
|
+
const OPENAI_CODEX_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
3
|
+
|
|
4
|
+
function trimSlash(url) {
|
|
5
|
+
return (url || '').replace(/\/+$/, '');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function sleep(ms) {
|
|
9
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parseInterval(value) {
|
|
13
|
+
const parsed = Number.parseInt(String(value || ''), 10);
|
|
14
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 5;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function readError(response, prefix) {
|
|
18
|
+
const text = await response.text().catch(() => '');
|
|
19
|
+
throw new Error(`${prefix} (${response.status}): ${text || response.statusText}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function requestDeviceCode(options = {}) {
|
|
23
|
+
const issuer = trimSlash(options.issuer || OPENAI_AUTH_ISSUER);
|
|
24
|
+
const clientId = options.clientId || OPENAI_CODEX_CLIENT_ID;
|
|
25
|
+
const response = await fetch(`${issuer}/api/accounts/deviceauth/usercode`, {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: { 'Content-Type': 'application/json' },
|
|
28
|
+
body: JSON.stringify({ client_id: clientId })
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
await readError(response, 'OpenAI device code request failed');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const data = await response.json();
|
|
36
|
+
return {
|
|
37
|
+
issuer,
|
|
38
|
+
clientId,
|
|
39
|
+
verificationUrl: `${issuer}/codex/device`,
|
|
40
|
+
userCode: data.user_code || data.usercode,
|
|
41
|
+
deviceAuthId: data.device_auth_id,
|
|
42
|
+
interval: parseInterval(data.interval)
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function pollForDeviceAuthorization(deviceCode, options = {}) {
|
|
47
|
+
const timeoutMs = options.timeoutMs || 15 * 60 * 1000;
|
|
48
|
+
const started = Date.now();
|
|
49
|
+
const issuer = trimSlash(deviceCode.issuer || OPENAI_AUTH_ISSUER);
|
|
50
|
+
const pollUrl = `${issuer}/api/accounts/deviceauth/token`;
|
|
51
|
+
|
|
52
|
+
while (Date.now() - started < timeoutMs) {
|
|
53
|
+
const response = await fetch(pollUrl, {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: { 'Content-Type': 'application/json' },
|
|
56
|
+
body: JSON.stringify({
|
|
57
|
+
device_auth_id: deviceCode.deviceAuthId,
|
|
58
|
+
user_code: deviceCode.userCode
|
|
59
|
+
})
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (response.ok) {
|
|
63
|
+
return await response.json();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (response.status === 403 || response.status === 404) {
|
|
67
|
+
await sleep((deviceCode.interval || 5) * 1000);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
await readError(response, 'OpenAI device authorization failed');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
throw new Error('OpenAI device authorization timed out after 15 minutes');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function exchangeAuthorizationCode(options) {
|
|
78
|
+
const issuer = trimSlash(options.issuer || OPENAI_AUTH_ISSUER);
|
|
79
|
+
const clientId = options.clientId || OPENAI_CODEX_CLIENT_ID;
|
|
80
|
+
const redirectUri = options.redirectUri || `${issuer}/deviceauth/callback`;
|
|
81
|
+
|
|
82
|
+
const form = new URLSearchParams({
|
|
83
|
+
grant_type: 'authorization_code',
|
|
84
|
+
code: options.authorizationCode,
|
|
85
|
+
redirect_uri: redirectUri,
|
|
86
|
+
client_id: clientId,
|
|
87
|
+
code_verifier: options.codeVerifier
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const response = await fetch(`${issuer}/oauth/token`, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
93
|
+
body: form.toString()
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
await readError(response, 'OpenAI authorization-code exchange failed');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return await response.json();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function refreshOpenAIToken(options) {
|
|
104
|
+
const issuer = trimSlash(options.issuer || OPENAI_AUTH_ISSUER);
|
|
105
|
+
const clientId = options.clientId || OPENAI_CODEX_CLIENT_ID;
|
|
106
|
+
const form = new URLSearchParams({
|
|
107
|
+
client_id: clientId,
|
|
108
|
+
grant_type: 'refresh_token',
|
|
109
|
+
refresh_token: options.refreshToken
|
|
110
|
+
});
|
|
111
|
+
const response = await fetch(`${issuer}/oauth/token`, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
114
|
+
body: form.toString()
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
await readError(response, 'OpenAI token refresh failed');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return await response.json();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function buildTokenRecord(tokenPayload) {
|
|
125
|
+
const expiresIn = Number(tokenPayload.expires_in);
|
|
126
|
+
const expiresAt = Number.isFinite(expiresIn)
|
|
127
|
+
? new Date(Date.now() + (expiresIn * 1000)).toISOString()
|
|
128
|
+
: null;
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
accessToken: tokenPayload.access_token,
|
|
132
|
+
refreshToken: tokenPayload.refresh_token,
|
|
133
|
+
idToken: tokenPayload.id_token || null,
|
|
134
|
+
expiresAt
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isTokenExpired(expiresAt, skewSeconds = 60) {
|
|
139
|
+
if (!expiresAt) return false;
|
|
140
|
+
const expiry = new Date(expiresAt).getTime();
|
|
141
|
+
if (!Number.isFinite(expiry)) return false;
|
|
142
|
+
return Date.now() >= (expiry - skewSeconds * 1000);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function completeDeviceCodeLogin(deviceCode, options = {}) {
|
|
146
|
+
const authorized = await pollForDeviceAuthorization(deviceCode, options);
|
|
147
|
+
const tokens = await exchangeAuthorizationCode({
|
|
148
|
+
issuer: deviceCode.issuer,
|
|
149
|
+
clientId: deviceCode.clientId,
|
|
150
|
+
authorizationCode: authorized.authorization_code,
|
|
151
|
+
codeVerifier: authorized.code_verifier,
|
|
152
|
+
redirectUri: `${trimSlash(deviceCode.issuer)}/deviceauth/callback`
|
|
153
|
+
});
|
|
154
|
+
return buildTokenRecord(tokens);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = {
|
|
158
|
+
OPENAI_AUTH_ISSUER,
|
|
159
|
+
OPENAI_CODEX_CLIENT_ID,
|
|
160
|
+
requestDeviceCode,
|
|
161
|
+
pollForDeviceAuthorization,
|
|
162
|
+
exchangeAuthorizationCode,
|
|
163
|
+
refreshOpenAIToken,
|
|
164
|
+
completeDeviceCodeLogin,
|
|
165
|
+
buildTokenRecord,
|
|
166
|
+
isTokenExpired
|
|
167
|
+
};
|
package/lib/parser.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser - Extract file operations from AI responses
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parse AI response for file operations
|
|
7
|
+
*
|
|
8
|
+
* Expected format:
|
|
9
|
+
* <file_operation>
|
|
10
|
+
* <action>create|edit|delete</action>
|
|
11
|
+
* <path>relative/path/to/file.ts</path>
|
|
12
|
+
* <content>
|
|
13
|
+
* ... file content ...
|
|
14
|
+
* </content>
|
|
15
|
+
* </file_operation>
|
|
16
|
+
*
|
|
17
|
+
* Also handles common local model variations:
|
|
18
|
+
* - <fileoperation> (no underscore)
|
|
19
|
+
* - <file-operation> (hyphen)
|
|
20
|
+
* - <parameter=action>create</parameter> (Nemotron-style)
|
|
21
|
+
* - <parameter=path>...</parameter> (Nemotron-style)
|
|
22
|
+
*/
|
|
23
|
+
function parseFileOperations(response) {
|
|
24
|
+
const operations = [];
|
|
25
|
+
|
|
26
|
+
// First, strip markdown code fences that might wrap the XML blocks
|
|
27
|
+
let cleanedResponse = response
|
|
28
|
+
.replace(/```(?:xml|html|text)?\s*\n?(<(?:file[-_]?operation)>)/gi, '$1')
|
|
29
|
+
.replace(/(<\/(?:file[-_]?operation)>)\s*\n?```/gi, '$1');
|
|
30
|
+
|
|
31
|
+
// Match file_operation blocks - tolerate missing underscore, hyphen, etc.
|
|
32
|
+
const operationRegex = /<file[-_]?operation>([\s\S]*?)<\/file[-_]?operation>/gi;
|
|
33
|
+
let match;
|
|
34
|
+
|
|
35
|
+
while ((match = operationRegex.exec(cleanedResponse)) !== null) {
|
|
36
|
+
const block = match[1];
|
|
37
|
+
|
|
38
|
+
// Extract action - standard format OR Nemotron <parameter=action> format
|
|
39
|
+
const actionMatch = block.match(/<action>(create|edit|delete)<\/action>/i)
|
|
40
|
+
|| block.match(/<parameter\s*=\s*"?action"?\s*>\s*(create|edit|delete)\s*<\/parameter>/i);
|
|
41
|
+
if (!actionMatch) continue;
|
|
42
|
+
|
|
43
|
+
// Extract path - standard format OR Nemotron <parameter=path> format
|
|
44
|
+
const pathMatch = block.match(/<path>([^<]+)<\/path>/)
|
|
45
|
+
|| block.match(/<parameter\s*=\s*"?path"?\s*>\s*([^<]+?)\s*<\/parameter>/i);
|
|
46
|
+
if (!pathMatch) continue;
|
|
47
|
+
|
|
48
|
+
// Extract content - standard format OR Nemotron <parameter=content> format
|
|
49
|
+
const contentMatch = block.match(/<content>([\s\S]*?)<\/content>/)
|
|
50
|
+
|| block.match(/<parameter\s*=\s*"?content"?\s*>([\s\S]*?)<\/parameter>/i);
|
|
51
|
+
|
|
52
|
+
operations.push({
|
|
53
|
+
action: actionMatch[1].toLowerCase(),
|
|
54
|
+
path: pathMatch[1].trim(),
|
|
55
|
+
content: contentMatch ? contentMatch[1].trim() : null
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return operations;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parse AI response for shell commands
|
|
64
|
+
*
|
|
65
|
+
* Expected format:
|
|
66
|
+
* <run_command>
|
|
67
|
+
* npm install axios
|
|
68
|
+
* </run_command>
|
|
69
|
+
*/
|
|
70
|
+
function parseCommands(response) {
|
|
71
|
+
const commands = [];
|
|
72
|
+
|
|
73
|
+
// Strip markdown code fences that might wrap the XML blocks
|
|
74
|
+
let cleanedResponse = response
|
|
75
|
+
.replace(/```(?:bash|shell|sh|text)?\s*\n?(<run[-_]?command>)/gi, '$1')
|
|
76
|
+
.replace(/(<\/run[-_]?command>)\s*\n?```/gi, '$1');
|
|
77
|
+
|
|
78
|
+
const commandRegex = /<run[-_]?command>([\s\S]*?)<\/run[-_]?command>/gi;
|
|
79
|
+
let match;
|
|
80
|
+
|
|
81
|
+
while ((match = commandRegex.exec(cleanedResponse)) !== null) {
|
|
82
|
+
const command = match[1].trim();
|
|
83
|
+
if (command) {
|
|
84
|
+
commands.push(command);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return commands;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Extract the explanation text (everything not in special blocks)
|
|
93
|
+
*/
|
|
94
|
+
function extractExplanation(response) {
|
|
95
|
+
let text = response;
|
|
96
|
+
|
|
97
|
+
// Remove file operation blocks (including model variants)
|
|
98
|
+
text = text.replace(/<file[-_]?operation>[\s\S]*?<\/file[-_]?operation>/gi, '');
|
|
99
|
+
|
|
100
|
+
// Remove command blocks (including model variants)
|
|
101
|
+
text = text.replace(/<run[-_]?command>[\s\S]*?<\/run[-_]?command>/gi, '');
|
|
102
|
+
|
|
103
|
+
// Clean up excess whitespace
|
|
104
|
+
text = text.replace(/\n{3,}/g, '\n\n').trim();
|
|
105
|
+
|
|
106
|
+
return text;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Parse a complete AI response
|
|
111
|
+
*/
|
|
112
|
+
function parseResponse(response) {
|
|
113
|
+
return {
|
|
114
|
+
explanation: extractExplanation(response),
|
|
115
|
+
fileOperations: parseFileOperations(response),
|
|
116
|
+
commands: parseCommands(response)
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if response contains any actionable items
|
|
122
|
+
*/
|
|
123
|
+
function hasActions(response) {
|
|
124
|
+
const parsed = parseResponse(response);
|
|
125
|
+
return parsed.fileOperations.length > 0 || parsed.commands.length > 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = {
|
|
129
|
+
parseFileOperations,
|
|
130
|
+
parseCommands,
|
|
131
|
+
extractExplanation,
|
|
132
|
+
parseResponse,
|
|
133
|
+
hasActions
|
|
134
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt Manager for Banana Code
|
|
3
|
+
* Loads system prompts from the prompts/ directory.
|
|
4
|
+
* Drop any .md file into prompts/ and it becomes available via /prompt <name>.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
class PromptManager {
|
|
11
|
+
constructor(promptsDir) {
|
|
12
|
+
this.promptsDir = promptsDir;
|
|
13
|
+
this.prompts = {};
|
|
14
|
+
this.load();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
load() {
|
|
18
|
+
this.prompts = {};
|
|
19
|
+
try {
|
|
20
|
+
const files = fs.readdirSync(this.promptsDir).filter(f => f.endsWith('.md'));
|
|
21
|
+
for (const file of files) {
|
|
22
|
+
const name = path.basename(file, '.md');
|
|
23
|
+
this.prompts[name] = fs.readFileSync(path.join(this.promptsDir, file), 'utf-8');
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
// No prompts directory or can't read - that's ok
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get a prompt by name. Falls back to 'base' if not found.
|
|
32
|
+
*
|
|
33
|
+
* For code-agent-* prompts, auto-assembles: base.md + code-agent.md + code-agent-{model}.md
|
|
34
|
+
* This eliminates duplication - model files only need their unique quirks.
|
|
35
|
+
*
|
|
36
|
+
* Injects dynamic OS detection to replace the static ## OS block.
|
|
37
|
+
*/
|
|
38
|
+
get(name) {
|
|
39
|
+
let prompt;
|
|
40
|
+
|
|
41
|
+
if (name.startsWith('code-agent-') && this.prompts[name]) {
|
|
42
|
+
// Assemble: base + code-agent (shared tool instructions) + model-specific quirks
|
|
43
|
+
const parts = [];
|
|
44
|
+
if (this.prompts['base']) parts.push(this.prompts['base']);
|
|
45
|
+
if (this.prompts['code-agent']) parts.push(this.prompts['code-agent']);
|
|
46
|
+
parts.push(this.prompts[name]);
|
|
47
|
+
prompt = parts.join('\n\n');
|
|
48
|
+
} else if (name === 'code-agent' && this.prompts['code-agent']) {
|
|
49
|
+
// code-agent alone: base + code-agent
|
|
50
|
+
const parts = [];
|
|
51
|
+
if (this.prompts['base']) parts.push(this.prompts['base']);
|
|
52
|
+
parts.push(this.prompts['code-agent']);
|
|
53
|
+
prompt = parts.join('\n\n');
|
|
54
|
+
} else {
|
|
55
|
+
prompt = this.prompts[name] || this.prompts['base'] || '';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Dynamic OS detection
|
|
59
|
+
const osLine = process.platform === 'win32'
|
|
60
|
+
? 'Windows. Use PowerShell/cmd syntax for shell commands. No bash, no `ls`, no `grep`. Use `dir`, `Get-ChildItem`, `findstr`, etc.'
|
|
61
|
+
: process.platform === 'darwin'
|
|
62
|
+
? 'macOS. Use bash/zsh syntax for shell commands. No PowerShell, no `dir`, no `findstr`. Use `ls`, `find`, `grep`, etc.'
|
|
63
|
+
: 'Linux. Use bash syntax for shell commands. No PowerShell, no `dir`, no `findstr`. Use `ls`, `find`, `grep`, etc.';
|
|
64
|
+
prompt = prompt.replace(/^## OS\n\n.+$/gm, `## OS\n\n${osLine}`);
|
|
65
|
+
prompt = prompt.replace(/^OS: Windows\..+$/gm, `OS: ${osLine}`);
|
|
66
|
+
if (process.platform !== 'win32') {
|
|
67
|
+
prompt = prompt.replace(/PowerShell syntax only/g, 'bash/zsh syntax');
|
|
68
|
+
prompt = prompt.replace(/PowerShell syntax only, not bash/g, 'bash/zsh syntax');
|
|
69
|
+
prompt = prompt.replace(/\(PowerShell syntax\)/g, '(bash/zsh syntax)');
|
|
70
|
+
}
|
|
71
|
+
return prompt;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* List available prompt names
|
|
76
|
+
*/
|
|
77
|
+
list() {
|
|
78
|
+
return Object.keys(this.prompts);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if a prompt exists
|
|
83
|
+
*/
|
|
84
|
+
has(name) {
|
|
85
|
+
return name in this.prompts;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Reload prompts from disk (useful if user drops a new .md file in)
|
|
90
|
+
*/
|
|
91
|
+
reload() {
|
|
92
|
+
this.load();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = PromptManager;
|