-
Notifications
You must be signed in to change notification settings - Fork 40
/
koder.mjs
executable file
·266 lines (228 loc) · 8.56 KB
/
koder.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
#!/usr/bin/env node
import fs from 'fs/promises';
import os from 'os';
import path from 'path';
import process from "process";
import { chat, MODELS, tokenCount } from './Chat.mjs';
import { exec } from 'child_process';
import { promisify } from 'util';
import kind_clean from "./kind_clean.mjs";
const execAsync = promisify(exec);
const DEPS_MODEL = "claude-3-5-sonnet-20240620"; // default model for dependency guessing
const CODE_MODEL = "claude-3-5-sonnet-20240620"; // default model for coding
// Define a structured object for the system definitions
const system = {
ts: {
koder: await fs.readFile(new URL('./koder/ts_koder.txt', import.meta.url), 'utf-8').then(content => content.trim()),
guess: await fs.readFile(new URL('./koder/ts_guess.txt', import.meta.url), 'utf-8').then(content => content.trim()),
clean: x => x,
deps: name => "ts-deps " + name + " --recursive",
},
agda: {
koder: await fs.readFile(new URL('./koder/agda_koder_2.txt', import.meta.url), 'utf-8').then(content => content.trim()),
guess: await fs.readFile(new URL('./koder/agda_guess.txt', import.meta.url), 'utf-8').then(content => content.trim()),
clean: x => x,
deps: name => "agda-deps " + name + " --recursive",
},
kind: {
koder: await fs.readFile(new URL('./koder/kind_koder_2.txt', import.meta.url), 'utf-8').then(content => content.trim()),
guess: await fs.readFile(new URL('./koder/kind_guess.txt', import.meta.url), 'utf-8').then(content => content.trim()),
clean: kind_clean,
deps: name => "kind deps " + name,
},
};
// Function to get real dependencies
async function realDependencies(file, ext) {
if (system[ext] && system[ext].deps) {
try {
const { stdout } = await execAsync(system[ext].deps(file));
if (stdout == "") {
return [];
}
return stdout.trim().split('\n').map(dep => dep.trim() + "." + ext);
} catch (error) {
//console.error(`Error getting real dependencies: ${error.message}`);
return [];
}
}
return [];
}
// Function to predict dependencies
async function predictDependencies(file, ext, context, fileContent, request) {
// Function to get all Typescript files recursively
async function getAllFiles(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
const files = await Promise.all(entries.map(async (entry) => {
const res = path.resolve(dir, entry.name);
if (entry.isDirectory()) {
const subFiles = await getAllFiles(res);
return subFiles.length > 0 ? { name: entry.name, children: subFiles } : null;
} else if (entry.name.endsWith("."+ext)) {
return { name: entry.name };
}
return null;
}));
return files.filter(file => file !== null).map(file => ({...file, name: file.name.replace(/\/_$/, '')}));
}
// Function to build a tree structure from files
function buildTree(files, prefix = '') {
let result = '';
for (const file of files) {
if (file.children) {
result += `${prefix}- ${file.name}/\n`;
result += buildTree(file.children, `${prefix} `);
} else {
result += `${prefix}- ${file.name}\n`;
}
}
return result;
}
const allFiles = (await getAllFiles("./")).filter(file => !file.name.includes('.backup') && !file.name.includes('node_modules'));
const defsTree = buildTree(allFiles);
const aiInput = [
`<FILE path="${file}">`,
fileContent.trim(),
'</FILE>',
'<TREE>',
defsTree.trim(),
'</TREE>',
//'<CONTEXT>',
//context,
//'</CONTEXT>',
'<REQUEST>',
request,
'</REQUEST>'
].join('\n').trim();
const ask = chat(DEPS_MODEL);
const res = await ask(aiInput, { system: system[ext].guess, model: DEPS_MODEL, system_cacheable: true });
console.log("");
console.log("");
//console.log(aiInput);
//console.log(res);
//process.exit();
//console.clear();
const dependenciesMatch = res.match(/<DEPENDENCIES>([\s\S]*)<\/DEPENDENCIES>/);
if (!dependenciesMatch) {
console.error("Error: AI output does not contain a valid DEPENDENCIES tag.");
return [];
}
return dependenciesMatch[1].trim().split('\n').map(dep => dep.trim());
}
// Main function to handle the refactoring process
async function main() {
// Check for correct usage and parse command-line arguments
if (process.argv.length < 3) {
console.log("Usage: koder <file> <request> [<model>]");
process.exit(1);
}
let file = process.argv[2];
let request = process.argv[3] || "";
let model = process.argv[4] || CODE_MODEL;
let ext = path.extname(file).slice(1);
// Load the local ".koder" file
let koder = "";
try { koder = await fs.readFile('./.koder', 'utf-8'); } catch (e) {};
// Initialize the chat function with the specified model
let ask = chat(model);
// Get directory and file information
let dir = process.cwd();
let fileContent;
try {
fileContent = system[ext].clean(await fs.readFile(file, 'utf-8'));
} catch (e) {
fileContent = "";
}
// If the request is empty, replace it by a default request.
if (request.trim() === '') {
request = ["Implement or update the TARGET file."].join('\n');
}
// If the file is empty, ask the AI to fill with an initial template
if (fileContent.trim() === '') {
fileContent = ["(empty file)"].join('\n');
}
// Get preducted dependencies
var pred = await predictDependencies(file, ext, koder, fileContent, request);
var pred = pred.map(dep => dep.replace(/\/_$/, ''));
// Get real dependencies
var real = await realDependencies(file, ext);
// Combine predicted and actual dependencies, removing duplicates
var deps = [...new Set([...pred, ...real])];
// Read dependency files
let depFiles = await Promise.all(deps.map(async (dep) => {
let depPath, content;
let path0 = path.join(dir, `${dep}`);
let path1 = path.join(dir, `${dep.replace(new RegExp(`\\.${ext}$`), '')}/_.${ext}`);
for (const pathToTry of [path0, path1]) {
try {
content = system[ext].clean(await fs.readFile(pathToTry, 'utf-8'));
depPath = pathToTry;
break;
} catch (err) {}
}
if (!content) {
return "";
} else {
return `<FILE path="${depPath}">\n${content}\n</FILE>`;
}
}));
//console.log(pred);
//console.log(deps);
//console.log(depFiles);
//process.exit();
// Prepare AI input
let aiInput = [
...depFiles,
`<FILE path="${file}" TARGET>`,
fileContent,
'</FILE>',
'<CONTEXT>',
koder,
'</CONTEXT>',
'<REQUEST>',
request,
'</REQUEST>'
].join('\n').trim();
// Write a .prompt file with the system + aiInput strings
await fs.mkdir(path.join(os.homedir(), '.ai'), { recursive: true });
await fs.writeFile(path.join(os.homedir(), '.ai', '.koder'), system[ext].koder + '\n\n' + aiInput, 'utf-8');
// Call the AI model
let res = await ask(aiInput, { system: system[ext].koder, model, system_cacheable: true });
console.log("\n");
// Extract all FILE tags from AI output
let fileMatches = res.matchAll(/<FILE path="([^"]+)">([\s\S]*?)<\/FILE>/g);
let filesToWrite = Array.from(fileMatches, match => ({path: match[1], content: match[2].trim()}));
if (filesToWrite.length === 0) {
console.error("Error: AI output does not contain any valid FILE tags.");
process.exit(1);
}
// Write each file
for (let fileToWrite of filesToWrite) {
let absolutePath = path.resolve(fileToWrite.path);
let currentDir = process.cwd();
// Check if the file is within the current working directory
if (!absolutePath.startsWith(currentDir)) {
console.error(`Error: Cannot write to file outside of current working directory: ${fileToWrite.path}`);
continue;
}
try {
// Create backup directory if it doesn't exist
const backupDir = path.join(os.homedir(), '.ai', '.backup');
await fs.mkdir(backupDir, { recursive: true });
// Create backup file path
const backupPath = path.join(backupDir, path.relative(currentDir, absolutePath));
// Create necessary directories for backup file
await fs.mkdir(path.dirname(backupPath), { recursive: true });
// Backup existing file if it exists
if (await fs.access(absolutePath).then(() => true).catch(() => false)) {
await fs.copyFile(absolutePath, backupPath);
}
// Write the new content
await fs.writeFile(absolutePath, fileToWrite.content, 'utf-8');
console.log(`File updated successfully: ${fileToWrite.path}`);
} catch (error) {
console.error(`Error writing file ${fileToWrite.path}: ${error.message}`);
}
}
}
// Run the main function and handle any errors
main().catch(console.error);