@semalt-ai/code 1.4.4 → 1.6.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/index.js +53 -1345
- package/lib/agent.js +81 -0
- package/lib/api.js +282 -0
- package/lib/args.js +45 -0
- package/lib/commands.js +344 -0
- package/lib/config.js +46 -0
- package/lib/constants.js +27 -0
- package/lib/context.js +71 -0
- package/lib/permissions.js +93 -0
- package/lib/prompts.js +29 -0
- package/lib/tools.js +120 -0
- package/lib/ui.js +603 -0
- package/package.json +1 -1
package/lib/commands.js
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const readline = require('readline');
|
|
5
|
+
|
|
6
|
+
const { CONFIG_PATH, DEFAULT_API_TIMEOUT_MS } = require('./constants');
|
|
7
|
+
const { getSystemPrompt } = require('./prompts');
|
|
8
|
+
|
|
9
|
+
function createCommands({
|
|
10
|
+
getConfig,
|
|
11
|
+
setConfig,
|
|
12
|
+
permissionManager,
|
|
13
|
+
ui,
|
|
14
|
+
apiClient,
|
|
15
|
+
runAgentLoop,
|
|
16
|
+
readFileContext,
|
|
17
|
+
agentExecShell,
|
|
18
|
+
}) {
|
|
19
|
+
const {
|
|
20
|
+
BOLD,
|
|
21
|
+
FG_BLUE,
|
|
22
|
+
FG_CYAN,
|
|
23
|
+
FG_DARK,
|
|
24
|
+
FG_GRAY,
|
|
25
|
+
FG_GREEN,
|
|
26
|
+
FG_RED,
|
|
27
|
+
FG_TEAL,
|
|
28
|
+
FG_YELLOW,
|
|
29
|
+
RST,
|
|
30
|
+
getCols,
|
|
31
|
+
printBanner,
|
|
32
|
+
printHelpHints,
|
|
33
|
+
printStatusBar,
|
|
34
|
+
readInteractiveInput,
|
|
35
|
+
} = ui;
|
|
36
|
+
const {
|
|
37
|
+
chatStream,
|
|
38
|
+
chatSync,
|
|
39
|
+
chooseSavedModelProfile,
|
|
40
|
+
describeModelProfile,
|
|
41
|
+
estimateTokens,
|
|
42
|
+
setActiveModelProfile,
|
|
43
|
+
} = apiClient;
|
|
44
|
+
|
|
45
|
+
async function cmdChat(opts) {
|
|
46
|
+
printBanner();
|
|
47
|
+
const cwd = process.cwd();
|
|
48
|
+
let currentModel = opts.model || getConfig().default_model;
|
|
49
|
+
let isRunningAgent = false;
|
|
50
|
+
|
|
51
|
+
printStatusBar(currentModel, cwd);
|
|
52
|
+
printHelpHints();
|
|
53
|
+
|
|
54
|
+
let messages = [{ role: 'system', content: getSystemPrompt() }];
|
|
55
|
+
const promptHistory = [];
|
|
56
|
+
const cols = getCols();
|
|
57
|
+
|
|
58
|
+
while (true) {
|
|
59
|
+
const inputResult = await readInteractiveInput(` ${FG_TEAL}${BOLD}>${RST} `, {
|
|
60
|
+
trim: false,
|
|
61
|
+
allowCursorNavigation: true,
|
|
62
|
+
history: promptHistory,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (inputResult.type === 'eof') {
|
|
66
|
+
console.log(`\n ${FG_GRAY}Goodbye!${RST}\n`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (inputResult.type === 'sigint') {
|
|
71
|
+
if (!isRunningAgent) {
|
|
72
|
+
console.log(`\n ${FG_YELLOW}Use Ctrl+D or type exit to quit.${RST}`);
|
|
73
|
+
}
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const text = (inputResult.value || '').trim();
|
|
78
|
+
if (!text) continue;
|
|
79
|
+
promptHistory.push(text);
|
|
80
|
+
|
|
81
|
+
if (['exit', 'quit', '/exit', '/quit'].includes(text.toLowerCase())) {
|
|
82
|
+
console.log(`\n ${FG_GRAY}Goodbye!${RST}\n`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (text === '/help') {
|
|
87
|
+
console.log(`
|
|
88
|
+
${FG_BLUE}${BOLD}Commands:${RST}
|
|
89
|
+
${FG_CYAN}/file <path>${RST} ${FG_GRAY}Load file or dir into context${RST}
|
|
90
|
+
${FG_CYAN}/model${RST} ${FG_GRAY}Choose saved model profile${RST}
|
|
91
|
+
${FG_CYAN}/model <name>${RST} ${FG_GRAY}Switch model manually${RST}
|
|
92
|
+
${FG_CYAN}/models${RST} ${FG_GRAY}Choose saved model profile${RST}
|
|
93
|
+
${FG_CYAN}/clear${RST} ${FG_GRAY}Clear conversation${RST}
|
|
94
|
+
${FG_CYAN}/compact${RST} ${FG_GRAY}Show token usage${RST}
|
|
95
|
+
${FG_CYAN}/shell <cmd>${RST} ${FG_GRAY}Run shell command directly${RST}
|
|
96
|
+
${FG_CYAN}!<cmd>${RST} ${FG_GRAY}Run shell command directly${RST}
|
|
97
|
+
${FG_CYAN}/approve${RST} ${FG_GRAY}Toggle auto-approve for all actions${RST}
|
|
98
|
+
${FG_CYAN}/config${RST} ${FG_GRAY}Show config${RST}
|
|
99
|
+
${FG_CYAN}exit${RST} ${FG_GRAY}Quit${RST}
|
|
100
|
+
|
|
101
|
+
${FG_DARK}The AI can execute commands — you'll be asked to approve each one.${RST}
|
|
102
|
+
`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (text.startsWith('/file ')) {
|
|
107
|
+
const fp = text.slice(6).trim();
|
|
108
|
+
const ctx = readFileContext([fp], ui);
|
|
109
|
+
if (ctx) messages.push({ role: 'user', content: `Here is the file context:\n${ctx}` });
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (text === '/model' || text === '/models') {
|
|
114
|
+
await new Promise((resolve) => {
|
|
115
|
+
const rl = readline.createInterface({
|
|
116
|
+
input: process.stdin,
|
|
117
|
+
output: process.stdout,
|
|
118
|
+
terminal: true,
|
|
119
|
+
});
|
|
120
|
+
chooseSavedModelProfile(rl, currentModel, cwd, (nextModel) => {
|
|
121
|
+
currentModel = nextModel;
|
|
122
|
+
rl.close();
|
|
123
|
+
resolve();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (text.startsWith('/model ')) {
|
|
130
|
+
currentModel = text.slice(7).trim();
|
|
131
|
+
console.log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Model → ${currentModel}${RST}`);
|
|
132
|
+
printStatusBar(currentModel, cwd);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (text === '/clear') {
|
|
137
|
+
messages = [{ role: 'system', content: getSystemPrompt() }];
|
|
138
|
+
permissionManager.clear();
|
|
139
|
+
console.log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Conversation and approvals cleared${RST}\n`);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (text === '/compact' || text === '/cost') {
|
|
144
|
+
const total = messages.reduce((sum, message) => sum + estimateTokens(message.content), 0);
|
|
145
|
+
console.log(` ${FG_GRAY}${messages.length - 1} messages · ~${total} tokens${RST}\n`);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (text === '/config') {
|
|
150
|
+
console.log(` ${FG_GRAY}${JSON.stringify(getConfig(), null, 2)}${RST}\n`);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (text === '/approve') {
|
|
155
|
+
const enabled = permissionManager.toggleAll();
|
|
156
|
+
const state = enabled ? 'ON' : 'OFF';
|
|
157
|
+
const color = enabled ? FG_GREEN : FG_RED;
|
|
158
|
+
console.log(` ${color}●${RST} ${FG_GRAY}Auto-approve: ${state}${RST}\n`);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (text.startsWith('/shell ') || text.startsWith('!')) {
|
|
163
|
+
const cmd = text.startsWith('/shell ') ? text.slice(7).trim() : text.slice(1).trim();
|
|
164
|
+
await agentExecShell(cmd);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
messages.push({ role: 'user', content: text });
|
|
169
|
+
console.log(` ${FG_DARK}${'─'.repeat(Math.min(cols, 70) - 4)}${RST}`);
|
|
170
|
+
|
|
171
|
+
isRunningAgent = true;
|
|
172
|
+
messages = await runAgentLoop(messages, currentModel);
|
|
173
|
+
isRunningAgent = false;
|
|
174
|
+
|
|
175
|
+
console.log(` ${FG_DARK}${'━'.repeat(Math.min(cols, 70) - 4)}${RST}`);
|
|
176
|
+
console.log();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function cmdCode(opts, promptArgs) {
|
|
181
|
+
if (!promptArgs.length) {
|
|
182
|
+
console.log(` ${FG_RED}Usage: semalt-code code <prompt>${RST}`);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const userPrompt = promptArgs.join(' ');
|
|
187
|
+
const context = opts.file ? readFileContext(opts.file, ui) : '';
|
|
188
|
+
const fullPrompt = context ? `Context files:\n${context}\n\nTask: ${userPrompt}` : userPrompt;
|
|
189
|
+
|
|
190
|
+
let messages = [
|
|
191
|
+
{ role: 'system', content: getSystemPrompt() },
|
|
192
|
+
{ role: 'user', content: fullPrompt },
|
|
193
|
+
];
|
|
194
|
+
messages = await runAgentLoop(messages, opts.model || getConfig().default_model);
|
|
195
|
+
console.log();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function cmdEdit(opts, filePath, instructionArgs) {
|
|
199
|
+
if (!filePath) {
|
|
200
|
+
console.log(` ${FG_RED}Usage: semalt-code edit <file> <instruction>${RST}`);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (!fs.existsSync(filePath)) {
|
|
204
|
+
console.log(` ${FG_RED}✗ File not found: ${filePath}${RST}`);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
209
|
+
const instruction = instructionArgs.join(' ');
|
|
210
|
+
|
|
211
|
+
const messages = [
|
|
212
|
+
{ role: 'system', content: 'You are Semalt.AI. Output ONLY the modified file. No explanations, no fences.' },
|
|
213
|
+
{ role: 'user', content: `File: ${filePath}\n\n\`\`\`\n${content}\n\`\`\`\n\nInstruction: ${instruction}` },
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
console.log(` ${FG_GRAY}Editing ${filePath}...${RST}`);
|
|
217
|
+
let result = await chatSync(messages, { model: opts.model });
|
|
218
|
+
|
|
219
|
+
if (result && !opts.dryRun) {
|
|
220
|
+
if (result.startsWith('```')) {
|
|
221
|
+
const lines = result.split('\n');
|
|
222
|
+
result = lines.at(-1).trim() === '```'
|
|
223
|
+
? lines.slice(1, -1).join('\n')
|
|
224
|
+
: lines.slice(1).join('\n');
|
|
225
|
+
}
|
|
226
|
+
fs.writeFileSync(filePath, result);
|
|
227
|
+
console.log(` ${FG_GREEN}✓ Saved: ${filePath}${RST}`);
|
|
228
|
+
} else if (opts.dryRun) {
|
|
229
|
+
console.log(` ${FG_YELLOW}⚠ Dry run — not modified${RST}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function cmdShell(opts, commandArgs) {
|
|
234
|
+
const command = commandArgs.join(' ');
|
|
235
|
+
if (!command) {
|
|
236
|
+
console.log(` ${FG_RED}Usage: semalt-code shell <command>${RST}`);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const result = await agentExecShell(command);
|
|
241
|
+
|
|
242
|
+
if (opts.analyze) {
|
|
243
|
+
const messages = [
|
|
244
|
+
{ role: 'system', content: 'You are Semalt.AI. Analyze the command output concisely.' },
|
|
245
|
+
{ role: 'user', content: `Command: ${command}\nExit: ${result.exit_code}\nStdout:\n${result.stdout}\nStderr:\n${result.stderr}` },
|
|
246
|
+
];
|
|
247
|
+
console.log();
|
|
248
|
+
console.log(` ${FG_TEAL}${BOLD}◆ Semalt.AI${RST}`);
|
|
249
|
+
console.log();
|
|
250
|
+
process.stdout.write(' ');
|
|
251
|
+
await chatStream(messages, { model: opts.model });
|
|
252
|
+
console.log();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function cmdModels() {
|
|
257
|
+
const config = getConfig();
|
|
258
|
+
if (!config.models.length) {
|
|
259
|
+
console.log(` ${FG_RED}✗${RST} ${FG_GRAY}No saved model profiles. Use semalt-code models add first.${RST}`);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
console.log();
|
|
264
|
+
console.log(` ${FG_TEAL}${BOLD}◆ Saved Models${RST}`);
|
|
265
|
+
console.log(` ${FG_DARK}${'─'.repeat(40)}${RST}`);
|
|
266
|
+
config.models.forEach((profile, index) => {
|
|
267
|
+
const active = profile.api_base === config.api_base &&
|
|
268
|
+
profile.api_key === config.api_key &&
|
|
269
|
+
profile.model === config.default_model;
|
|
270
|
+
const marker = active ? `${FG_GREEN}●${RST}` : `${FG_DARK}○${RST}`;
|
|
271
|
+
console.log(` ${marker} ${FG_CYAN}${index + 1}.${RST} ${describeModelProfile(profile)}`);
|
|
272
|
+
});
|
|
273
|
+
console.log();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function cmdModelsAdd() {
|
|
277
|
+
const rl = readline.createInterface({
|
|
278
|
+
input: process.stdin,
|
|
279
|
+
output: process.stdout,
|
|
280
|
+
terminal: true,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
function ask(question) {
|
|
284
|
+
return new Promise((resolve) => {
|
|
285
|
+
rl.question(question, (answer) => resolve((answer || '').trim()));
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
console.log();
|
|
290
|
+
console.log(` ${FG_TEAL}${BOLD}◆ Add Model Profile${RST}`);
|
|
291
|
+
console.log(` ${FG_DARK}${'─'.repeat(40)}${RST}`);
|
|
292
|
+
|
|
293
|
+
const apiBase = await ask(` ${FG_CYAN}API Base URL:${RST} `);
|
|
294
|
+
const apiKey = await ask(` ${FG_CYAN}API Key:${RST} `);
|
|
295
|
+
const modelId = await ask(` ${FG_CYAN}Model ID:${RST} `);
|
|
296
|
+
rl.close();
|
|
297
|
+
|
|
298
|
+
if (!apiBase || !modelId) {
|
|
299
|
+
console.log(`\n ${FG_RED}✗${RST} ${FG_GRAY}API Base URL and Model ID are required.${RST}\n`);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const config = getConfig();
|
|
304
|
+
const profile = {
|
|
305
|
+
api_base: apiBase,
|
|
306
|
+
api_key: apiKey || 'any',
|
|
307
|
+
model: modelId,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
config.models.push(profile);
|
|
311
|
+
setActiveModelProfile(profile);
|
|
312
|
+
console.log(`\n ${FG_GREEN}✓${RST} Saved model profile: ${describeModelProfile(profile)}\n`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function cmdInit(opts) {
|
|
316
|
+
const current = getConfig();
|
|
317
|
+
const cfg = {
|
|
318
|
+
api_base: opts.apiBase || 'http://127.0.0.1:8800',
|
|
319
|
+
api_key: opts.apiKey || 'any',
|
|
320
|
+
default_model: opts.defaultModel || 'default',
|
|
321
|
+
temperature: 0.7,
|
|
322
|
+
request_timeout_ms: DEFAULT_API_TIMEOUT_MS,
|
|
323
|
+
stream: true,
|
|
324
|
+
models: current.models,
|
|
325
|
+
};
|
|
326
|
+
setConfig(cfg);
|
|
327
|
+
console.log(`\n ${FG_GREEN}✓${RST} Config saved to ${CONFIG_PATH}`);
|
|
328
|
+
console.log(` ${FG_GRAY}${JSON.stringify(cfg, null, 2)}${RST}\n`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
cmdChat,
|
|
333
|
+
cmdCode,
|
|
334
|
+
cmdEdit,
|
|
335
|
+
cmdInit,
|
|
336
|
+
cmdModels,
|
|
337
|
+
cmdModelsAdd,
|
|
338
|
+
cmdShell,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
module.exports = {
|
|
343
|
+
createCommands,
|
|
344
|
+
};
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const { CONFIG_PATH, DEFAULT_CONFIG } = require('./constants');
|
|
7
|
+
|
|
8
|
+
function normalizeConfig(cfg = {}) {
|
|
9
|
+
const merged = { ...DEFAULT_CONFIG, ...cfg };
|
|
10
|
+
merged.models = Array.isArray(cfg.models)
|
|
11
|
+
? cfg.models
|
|
12
|
+
.filter((entry) => entry &&
|
|
13
|
+
typeof entry.api_base === 'string' &&
|
|
14
|
+
typeof entry.api_key === 'string' &&
|
|
15
|
+
typeof entry.model === 'string' &&
|
|
16
|
+
entry.api_base.trim() &&
|
|
17
|
+
entry.model.trim())
|
|
18
|
+
.map((entry) => ({
|
|
19
|
+
api_base: entry.api_base.trim(),
|
|
20
|
+
api_key: entry.api_key,
|
|
21
|
+
model: entry.model.trim(),
|
|
22
|
+
}))
|
|
23
|
+
: [];
|
|
24
|
+
return merged;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function loadConfig() {
|
|
28
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
29
|
+
try {
|
|
30
|
+
const data = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
31
|
+
return normalizeConfig(data);
|
|
32
|
+
} catch {}
|
|
33
|
+
}
|
|
34
|
+
return normalizeConfig();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function saveConfig(cfg) {
|
|
38
|
+
fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
|
|
39
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(normalizeConfig(cfg), null, 2));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = {
|
|
43
|
+
loadConfig,
|
|
44
|
+
normalizeConfig,
|
|
45
|
+
saveConfig,
|
|
46
|
+
};
|
package/lib/constants.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const PACKAGE_JSON = require('../package.json');
|
|
7
|
+
|
|
8
|
+
const DEFAULT_API_TIMEOUT_MS = 15 * 60 * 1000;
|
|
9
|
+
|
|
10
|
+
const DEFAULT_CONFIG = {
|
|
11
|
+
api_base: 'http://127.0.0.1:8800',
|
|
12
|
+
api_key: 'any',
|
|
13
|
+
default_model: 'default',
|
|
14
|
+
temperature: 0.7,
|
|
15
|
+
request_timeout_ms: DEFAULT_API_TIMEOUT_MS,
|
|
16
|
+
stream: true,
|
|
17
|
+
models: [],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const CONFIG_PATH = path.join(os.homedir(), '.semalt-ai', 'config.json');
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
CONFIG_PATH,
|
|
24
|
+
DEFAULT_API_TIMEOUT_MS,
|
|
25
|
+
DEFAULT_CONFIG,
|
|
26
|
+
PACKAGE_JSON,
|
|
27
|
+
};
|
package/lib/context.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
function readFileContext(filePaths, ui) {
|
|
7
|
+
const { FG_GRAY, FG_GREEN, FG_RED, RST } = ui;
|
|
8
|
+
let context = '';
|
|
9
|
+
|
|
10
|
+
for (const fp of filePaths) {
|
|
11
|
+
if (!fs.existsSync(fp)) {
|
|
12
|
+
console.log(` ${FG_RED}✗${RST} ${FG_GRAY}Not found: ${fp}${RST}`);
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const stat = fs.statSync(fp);
|
|
17
|
+
if (stat.isFile()) {
|
|
18
|
+
try {
|
|
19
|
+
const content = fs.readFileSync(fp, 'utf8');
|
|
20
|
+
context += `\n--- File: ${fp} ---\n${content}\n`;
|
|
21
|
+
console.log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Loaded ${fp} (${content.length} chars)${RST}`);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.log(` ${FG_RED}✗${RST} ${FG_GRAY}${fp}: ${error.message}${RST}`);
|
|
24
|
+
}
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (stat.isDirectory()) {
|
|
29
|
+
let count = 0;
|
|
30
|
+
function walkDir(dir) {
|
|
31
|
+
if (count >= 50) return;
|
|
32
|
+
let entries;
|
|
33
|
+
try {
|
|
34
|
+
entries = fs.readdirSync(dir).sort();
|
|
35
|
+
} catch {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
if (entry.startsWith('.')) continue;
|
|
41
|
+
const full = path.join(dir, entry);
|
|
42
|
+
let childStat;
|
|
43
|
+
try {
|
|
44
|
+
childStat = fs.statSync(full);
|
|
45
|
+
} catch {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (childStat.isFile()) {
|
|
50
|
+
try {
|
|
51
|
+
const content = fs.readFileSync(full, 'utf8').slice(0, 10000);
|
|
52
|
+
context += `\n--- File: ${full} ---\n${content}\n`;
|
|
53
|
+
count++;
|
|
54
|
+
} catch {}
|
|
55
|
+
} else if (childStat.isDirectory()) {
|
|
56
|
+
walkDir(full);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
walkDir(fp);
|
|
62
|
+
console.log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Loaded ${count} files from ${fp}${RST}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return context;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = {
|
|
70
|
+
readFileContext,
|
|
71
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function createPermissionManager(ui) {
|
|
4
|
+
const { BOLD, FG_CYAN, FG_DARK, FG_GRAY, FG_GREEN, FG_RED, FG_YELLOW, RST, readInteractiveInput } = ui;
|
|
5
|
+
const state = {
|
|
6
|
+
autoApproveShell: false,
|
|
7
|
+
autoApproveFile: false,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function askPermissionLine(actionType) {
|
|
11
|
+
return actionType === 'shell'
|
|
12
|
+
? ' 1. Yes 2. Yes, always for shell 3. No'
|
|
13
|
+
: ' 1. Yes 2. Yes, always for files 3. No';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readPermissionChoice() {
|
|
17
|
+
return readInteractiveInput(` ${FG_YELLOW}?${RST} `, {
|
|
18
|
+
allowed: ['1', '2', '3'],
|
|
19
|
+
immediate: true,
|
|
20
|
+
trim: true,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function askPermission(actionType, description) {
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
if (actionType === 'shell' && state.autoApproveShell) {
|
|
27
|
+
console.log(` ${FG_GREEN}✓${RST} ${FG_DARK}Auto-approved: ${description}${RST}`);
|
|
28
|
+
resolve(true);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (actionType === 'file' && state.autoApproveFile) {
|
|
33
|
+
console.log(` ${FG_GREEN}✓${RST} ${FG_DARK}Auto-approved: ${description}${RST}`);
|
|
34
|
+
resolve(true);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log();
|
|
39
|
+
console.log(` ${FG_YELLOW}${BOLD}⚠ Permission required${RST}`);
|
|
40
|
+
console.log(` ${FG_GRAY}${actionType}: ${description}${RST}`);
|
|
41
|
+
console.log();
|
|
42
|
+
console.log(` ${FG_CYAN}${askPermissionLine(actionType)}${RST}`);
|
|
43
|
+
console.log();
|
|
44
|
+
|
|
45
|
+
readPermissionChoice().then((result) => {
|
|
46
|
+
if (result.type === 'sigint' || result.type === 'eof') {
|
|
47
|
+
console.log(` ${FG_RED}✗${RST} ${FG_DARK}Denied${RST}`);
|
|
48
|
+
resolve(false);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const choice = (result.value || '').trim().toLowerCase();
|
|
53
|
+
if (choice === '1' || choice === 'y' || choice === 'yes') {
|
|
54
|
+
resolve(true);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (choice === '2' || choice === 'a' || choice === 'always') {
|
|
59
|
+
if (actionType === 'shell') state.autoApproveShell = true;
|
|
60
|
+
else state.autoApproveFile = true;
|
|
61
|
+
console.log(` ${FG_GREEN}✓${RST} ${FG_DARK}Auto-approve enabled for ${actionType} operations${RST}`);
|
|
62
|
+
resolve(true);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log(` ${FG_RED}✗${RST} ${FG_DARK}Denied${RST}`);
|
|
67
|
+
resolve(false);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function clear() {
|
|
73
|
+
state.autoApproveShell = false;
|
|
74
|
+
state.autoApproveFile = false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function toggleAll() {
|
|
78
|
+
state.autoApproveShell = !state.autoApproveShell;
|
|
79
|
+
state.autoApproveFile = !state.autoApproveFile;
|
|
80
|
+
return state.autoApproveShell;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
askPermission,
|
|
85
|
+
clear,
|
|
86
|
+
state,
|
|
87
|
+
toggleAll,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = {
|
|
92
|
+
createPermissionManager,
|
|
93
|
+
};
|
package/lib/prompts.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function getSystemPrompt() {
|
|
4
|
+
return `You are Semalt.AI, an expert AI coding assistant running in the user's terminal. You have the ability to execute shell commands and file operations.
|
|
5
|
+
|
|
6
|
+
IMPORTANT: You CAN execute commands on the user's system. When you need to run a command, use this exact format:
|
|
7
|
+
|
|
8
|
+
To run a shell command:
|
|
9
|
+
<exec>command here</exec>
|
|
10
|
+
|
|
11
|
+
To read a file:
|
|
12
|
+
<read_file>/path/to/file</read_file>
|
|
13
|
+
|
|
14
|
+
To write a file:
|
|
15
|
+
<write_file path="/path/to/file">file content here</write_file>
|
|
16
|
+
|
|
17
|
+
Rules:
|
|
18
|
+
- When the user asks you to do something on their system (create files, install packages, check status, etc.), USE the tools above — do NOT just print instructions.
|
|
19
|
+
- Each command will be shown to the user for approval before execution.
|
|
20
|
+
- After execution, you will receive the output and can continue working.
|
|
21
|
+
- You can chain multiple operations in one response.
|
|
22
|
+
- Be concise. Provide working solutions.
|
|
23
|
+
- Use markdown for code blocks in explanations.
|
|
24
|
+
- Current working directory: ${process.cwd()}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
getSystemPrompt,
|
|
29
|
+
};
|