lovecode-ai 0.1.6 → 0.1.7
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/dist/index.js +311 -15
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6888,19 +6888,293 @@ function startTUI(props) {
|
|
|
6888
6888
|
}
|
|
6889
6889
|
|
|
6890
6890
|
// src/commands/tui.ts
|
|
6891
|
+
var TOOL_SYSTEM_PROMPT = `You are LoveCode AI, a terminal-native coding assistant with full access to the project filesystem.
|
|
6892
|
+
|
|
6893
|
+
You can read, write, edit, and search files using tool tags:
|
|
6894
|
+
|
|
6895
|
+
<tool name="read_file">
|
|
6896
|
+
path=<file path>
|
|
6897
|
+
</tool>
|
|
6898
|
+
|
|
6899
|
+
<tool name="write_file">
|
|
6900
|
+
path=<file path>
|
|
6901
|
+
content=<file content>
|
|
6902
|
+
</tool>
|
|
6903
|
+
|
|
6904
|
+
<tool name="edit_file">
|
|
6905
|
+
path=<file path>
|
|
6906
|
+
oldString=<text to replace>
|
|
6907
|
+
newString=<replacement text>
|
|
6908
|
+
</tool>
|
|
6909
|
+
|
|
6910
|
+
<tool name="create_file">
|
|
6911
|
+
path=<file path>
|
|
6912
|
+
</tool>
|
|
6913
|
+
|
|
6914
|
+
<tool name="delete_file">
|
|
6915
|
+
path=<file path>
|
|
6916
|
+
</tool>
|
|
6917
|
+
|
|
6918
|
+
<tool name="append_file">
|
|
6919
|
+
path=<file path>
|
|
6920
|
+
content=<text to append>
|
|
6921
|
+
</tool>
|
|
6922
|
+
|
|
6923
|
+
<tool name="grep_search">
|
|
6924
|
+
pattern=<regex pattern>
|
|
6925
|
+
</tool>
|
|
6926
|
+
|
|
6927
|
+
<tool name="glob_search">
|
|
6928
|
+
pattern=<glob pattern>
|
|
6929
|
+
</tool>
|
|
6930
|
+
|
|
6931
|
+
<tool name="read_dir">
|
|
6932
|
+
path=<directory path>
|
|
6933
|
+
</tool>
|
|
6934
|
+
|
|
6935
|
+
<tool name="execute_command">
|
|
6936
|
+
command=<shell command>
|
|
6937
|
+
</tool>
|
|
6938
|
+
|
|
6939
|
+
When you need to access files, use the appropriate tool tag.
|
|
6940
|
+
After getting results, continue helping the user.`;
|
|
6941
|
+
async function loadAIProvider() {
|
|
6942
|
+
try {
|
|
6943
|
+
const { loadEnv: loadEnv2 } = await import("./env-HJQWWL6N.js");
|
|
6944
|
+
const { loadConfig: loadConfig2 } = await import("./config-FJNTTKR3.js");
|
|
6945
|
+
const { resolveModel } = await import("./registry-ADSIKXA4.js");
|
|
6946
|
+
loadEnv2();
|
|
6947
|
+
const cfg = loadConfig2();
|
|
6948
|
+
if (!cfg.provider) {
|
|
6949
|
+
console.log(chalk35.yellow("No AI provider configured. Use `lovecode init` to set one up."));
|
|
6950
|
+
return null;
|
|
6951
|
+
}
|
|
6952
|
+
const resolved = resolveModel(cfg.provider);
|
|
6953
|
+
if (!resolved) {
|
|
6954
|
+
console.log(chalk35.yellow(`Provider "${cfg.provider}" not found. Use \`lovecode init\` to reconfigure.`));
|
|
6955
|
+
return null;
|
|
6956
|
+
}
|
|
6957
|
+
const provider = resolved.entry.provider;
|
|
6958
|
+
const model = resolved.model;
|
|
6959
|
+
const config = {
|
|
6960
|
+
model,
|
|
6961
|
+
temperature: cfg.model_params?.temperature ?? 0.2,
|
|
6962
|
+
maxTokens: cfg.model_params?.max_tokens ?? 4096,
|
|
6963
|
+
baseUrl: cfg.api?.base_url
|
|
6964
|
+
};
|
|
6965
|
+
return { provider, config };
|
|
6966
|
+
} catch (err) {
|
|
6967
|
+
console.log(chalk35.red(`Failed to load AI provider: ${err.message}`));
|
|
6968
|
+
return null;
|
|
6969
|
+
}
|
|
6970
|
+
}
|
|
6971
|
+
async function processToolCalls(response, workingDir) {
|
|
6972
|
+
const outputs = [];
|
|
6973
|
+
const toolRegex = /<tool\s+name="([^"]+)">\n?([\s\S]*?)<\/tool>/g;
|
|
6974
|
+
let match;
|
|
6975
|
+
while ((match = toolRegex.exec(response)) !== null) {
|
|
6976
|
+
const toolName = match[1];
|
|
6977
|
+
const argsBlock = match[2].trim();
|
|
6978
|
+
const args = {};
|
|
6979
|
+
for (const line of argsBlock.split("\n")) {
|
|
6980
|
+
const eqIdx = line.indexOf("=");
|
|
6981
|
+
if (eqIdx > 0) {
|
|
6982
|
+
const key = line.slice(0, eqIdx).trim();
|
|
6983
|
+
const value = line.slice(eqIdx + 1).trim();
|
|
6984
|
+
args[key] = value;
|
|
6985
|
+
}
|
|
6986
|
+
}
|
|
6987
|
+
try {
|
|
6988
|
+
const tool = await getTool2(toolName);
|
|
6989
|
+
if (!tool) {
|
|
6990
|
+
outputs.push(`Unknown tool: ${toolName}`);
|
|
6991
|
+
continue;
|
|
6992
|
+
}
|
|
6993
|
+
const result = await tool.execute(workingDir, args);
|
|
6994
|
+
const output = result.success ? `[${toolName}] result:
|
|
6995
|
+
${result.output.slice(0, 3e3)}` : `[${toolName}] error: ${result.error || result.output}`;
|
|
6996
|
+
outputs.push(output);
|
|
6997
|
+
} catch (err) {
|
|
6998
|
+
outputs.push(`[${toolName}] exception: ${err.message}`);
|
|
6999
|
+
}
|
|
7000
|
+
}
|
|
7001
|
+
return outputs;
|
|
7002
|
+
}
|
|
7003
|
+
async function getTool2(name) {
|
|
7004
|
+
if (name === "read_file") {
|
|
7005
|
+
const { readFileSync: readFileSync18, existsSync: existsSync16 } = await import("fs");
|
|
7006
|
+
const { resolve: resolve5 } = await import("path");
|
|
7007
|
+
return {
|
|
7008
|
+
execute: async (workingDir, args) => {
|
|
7009
|
+
const filePath = resolve5(workingDir, args.path || ".");
|
|
7010
|
+
if (!existsSync16(filePath)) return { success: false, output: "", error: `File not found: ${args.path}` };
|
|
7011
|
+
const content = readFileSync18(filePath, "utf-8");
|
|
7012
|
+
const lines = content.split("\n");
|
|
7013
|
+
const numbered = lines.map((l, i) => `${String(i + 1).padStart(4, " ")} | ${l}`).join("\n");
|
|
7014
|
+
return { success: true, output: numbered };
|
|
7015
|
+
}
|
|
7016
|
+
};
|
|
7017
|
+
}
|
|
7018
|
+
if (name === "write_file") {
|
|
7019
|
+
const { writeFileSync: writeFileSync11, mkdirSync: mkdirSync10 } = await import("fs");
|
|
7020
|
+
const { resolve: resolve5, dirname: dirname6 } = await import("path");
|
|
7021
|
+
return {
|
|
7022
|
+
execute: async (workingDir, args) => {
|
|
7023
|
+
const filePath = resolve5(workingDir, args.path || "");
|
|
7024
|
+
mkdirSync10(dirname6(filePath), { recursive: true });
|
|
7025
|
+
writeFileSync11(filePath, args.content || "", "utf-8");
|
|
7026
|
+
return { success: true, output: `Wrote ${filePath}` };
|
|
7027
|
+
}
|
|
7028
|
+
};
|
|
7029
|
+
}
|
|
7030
|
+
if (name === "edit_file") {
|
|
7031
|
+
const { readFileSync: readFileSync18, writeFileSync: writeFileSync11, existsSync: existsSync16 } = await import("fs");
|
|
7032
|
+
const { resolve: resolve5 } = await import("path");
|
|
7033
|
+
return {
|
|
7034
|
+
execute: async (workingDir, args) => {
|
|
7035
|
+
const filePath = resolve5(workingDir, args.path || "");
|
|
7036
|
+
if (!existsSync16(filePath)) return { success: false, output: "", error: `File not found: ${args.path}` };
|
|
7037
|
+
const content = readFileSync18(filePath, "utf-8");
|
|
7038
|
+
if (!args.oldString) return { success: false, output: "", error: "oldString required" };
|
|
7039
|
+
const escaped = args.oldString.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7040
|
+
const count = (content.match(new RegExp(escaped, "g")) || []).length;
|
|
7041
|
+
if (count === 0) return { success: false, output: "", error: `oldString not found in ${args.path}` };
|
|
7042
|
+
const updated = content.replaceAll(args.oldString, args.newString || "");
|
|
7043
|
+
writeFileSync11(filePath, updated, "utf-8");
|
|
7044
|
+
return { success: true, output: `Edited ${filePath} (${count} replacement${count > 1 ? "s" : ""})` };
|
|
7045
|
+
}
|
|
7046
|
+
};
|
|
7047
|
+
}
|
|
7048
|
+
if (name === "create_file") {
|
|
7049
|
+
const { writeFileSync: writeFileSync11, mkdirSync: mkdirSync10, existsSync: existsSync16 } = await import("fs");
|
|
7050
|
+
const { resolve: resolve5, dirname: dirname6 } = await import("path");
|
|
7051
|
+
return {
|
|
7052
|
+
execute: async (workingDir, args) => {
|
|
7053
|
+
const filePath = resolve5(workingDir, args.path || "");
|
|
7054
|
+
if (existsSync16(filePath)) return { success: false, output: "", error: `File exists: ${args.path}` };
|
|
7055
|
+
mkdirSync10(dirname6(filePath), { recursive: true });
|
|
7056
|
+
writeFileSync11(filePath, "", "utf-8");
|
|
7057
|
+
return { success: true, output: `Created ${filePath}` };
|
|
7058
|
+
}
|
|
7059
|
+
};
|
|
7060
|
+
}
|
|
7061
|
+
if (name === "delete_file") {
|
|
7062
|
+
const { unlinkSync: unlinkSync7, existsSync: existsSync16 } = await import("fs");
|
|
7063
|
+
const { resolve: resolve5 } = await import("path");
|
|
7064
|
+
return {
|
|
7065
|
+
execute: async (workingDir, args) => {
|
|
7066
|
+
const filePath = resolve5(workingDir, args.path || "");
|
|
7067
|
+
if (!existsSync16(filePath)) return { success: false, output: "", error: `File not found: ${args.path}` };
|
|
7068
|
+
unlinkSync7(filePath);
|
|
7069
|
+
return { success: true, output: `Deleted ${filePath}` };
|
|
7070
|
+
}
|
|
7071
|
+
};
|
|
7072
|
+
}
|
|
7073
|
+
if (name === "append_file") {
|
|
7074
|
+
const { appendFileSync: appendFileSync2, mkdirSync: mkdirSync10 } = await import("fs");
|
|
7075
|
+
const { resolve: resolve5, dirname: dirname6 } = await import("path");
|
|
7076
|
+
return {
|
|
7077
|
+
execute: async (workingDir, args) => {
|
|
7078
|
+
const filePath = resolve5(workingDir, args.path || "");
|
|
7079
|
+
mkdirSync10(dirname6(filePath), { recursive: true });
|
|
7080
|
+
appendFileSync2(filePath, (args.content || "") + "\n", "utf-8");
|
|
7081
|
+
return { success: true, output: `Appended to ${filePath}` };
|
|
7082
|
+
}
|
|
7083
|
+
};
|
|
7084
|
+
}
|
|
7085
|
+
if (name === "grep_search") {
|
|
7086
|
+
const { execSync: execSync5 } = await import("child_process");
|
|
7087
|
+
return {
|
|
7088
|
+
execute: async (workingDir, args) => {
|
|
7089
|
+
try {
|
|
7090
|
+
const pattern = args.pattern || "";
|
|
7091
|
+
const cmd = `rg -n '${pattern.replace(/'/g, "'\\''")}' 2>/dev/null || echo "(no matches)"`;
|
|
7092
|
+
const output = execSync5(cmd, { cwd: workingDir, encoding: "utf-8", maxBuffer: 1024 * 1024 });
|
|
7093
|
+
return { success: true, output: output.trim() };
|
|
7094
|
+
} catch {
|
|
7095
|
+
return { success: true, output: "(no matches)" };
|
|
7096
|
+
}
|
|
7097
|
+
}
|
|
7098
|
+
};
|
|
7099
|
+
}
|
|
7100
|
+
if (name === "glob_search") {
|
|
7101
|
+
const { execSync: execSync5 } = await import("child_process");
|
|
7102
|
+
return {
|
|
7103
|
+
execute: async (workingDir, args) => {
|
|
7104
|
+
try {
|
|
7105
|
+
const pattern = args.pattern || "*";
|
|
7106
|
+
const cmd = `ls -1 ${pattern} 2>/dev/null || echo "(no files found)"`;
|
|
7107
|
+
const output = execSync5(cmd, { cwd: workingDir, encoding: "utf-8", maxBuffer: 1024 * 1024 });
|
|
7108
|
+
return { success: true, output: output.trim() };
|
|
7109
|
+
} catch {
|
|
7110
|
+
return { success: true, output: "(no files found)" };
|
|
7111
|
+
}
|
|
7112
|
+
}
|
|
7113
|
+
};
|
|
7114
|
+
}
|
|
7115
|
+
if (name === "read_dir") {
|
|
7116
|
+
const { readdirSync: readdirSync13, existsSync: existsSync16 } = await import("fs");
|
|
7117
|
+
const { resolve: resolve5 } = await import("path");
|
|
7118
|
+
return {
|
|
7119
|
+
execute: async (workingDir, args) => {
|
|
7120
|
+
const dirPath = resolve5(workingDir, args.path || ".");
|
|
7121
|
+
if (!existsSync16(dirPath)) return { success: false, output: "", error: `Directory not found: ${args.path}` };
|
|
7122
|
+
const entries = readdirSync13(dirPath, { withFileTypes: true });
|
|
7123
|
+
const output = entries.map((e) => e.isDirectory() ? `${e.name}/` : e.name).join("\n");
|
|
7124
|
+
return { success: true, output };
|
|
7125
|
+
}
|
|
7126
|
+
};
|
|
7127
|
+
}
|
|
7128
|
+
if (name === "execute_command") {
|
|
7129
|
+
const { execSync: execSync5 } = await import("child_process");
|
|
7130
|
+
return {
|
|
7131
|
+
execute: async (workingDir, args) => {
|
|
7132
|
+
try {
|
|
7133
|
+
const command = args.command || "";
|
|
7134
|
+
const timeout = parseInt(args.timeout || "30000", 10);
|
|
7135
|
+
const output = execSync5(command, {
|
|
7136
|
+
cwd: workingDir,
|
|
7137
|
+
encoding: "utf-8",
|
|
7138
|
+
maxBuffer: 1024 * 1024,
|
|
7139
|
+
timeout
|
|
7140
|
+
});
|
|
7141
|
+
return { success: true, output: output.trim() };
|
|
7142
|
+
} catch (err) {
|
|
7143
|
+
const e = err;
|
|
7144
|
+
return { success: false, output: e.stderr || e.message, error: e.message };
|
|
7145
|
+
}
|
|
7146
|
+
}
|
|
7147
|
+
};
|
|
7148
|
+
}
|
|
7149
|
+
return null;
|
|
7150
|
+
}
|
|
6891
7151
|
async function cmdTUI(options) {
|
|
7152
|
+
const workingDir = options.dir || process.cwd();
|
|
6892
7153
|
console.log(chalk35.dim("Starting LoveCode TUI..."));
|
|
6893
7154
|
if (options.theme) {
|
|
6894
7155
|
const mod = await import("./theme-ZRZYRB2Q.js");
|
|
6895
7156
|
const names = mod.getThemeNames();
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
7157
|
+
if (names.includes(options.theme)) {
|
|
7158
|
+
mod.setTheme(options.theme);
|
|
7159
|
+
}
|
|
7160
|
+
}
|
|
7161
|
+
const ai = await loadAIProvider();
|
|
7162
|
+
if (!ai) {
|
|
7163
|
+
startTUI({
|
|
7164
|
+
projectName: "LoveCode AI",
|
|
7165
|
+
branch: "main",
|
|
7166
|
+
fileCount: 142,
|
|
7167
|
+
language: "TypeScript",
|
|
7168
|
+
framework: "Node.js",
|
|
7169
|
+
repoStatus: "clean",
|
|
7170
|
+
messages: [],
|
|
7171
|
+
onSendMessage: async () => "No AI provider configured. Run `lovecode init` to set one up.",
|
|
7172
|
+
onRunCommand: async () => "No AI provider configured."
|
|
7173
|
+
});
|
|
7174
|
+
return;
|
|
6903
7175
|
}
|
|
7176
|
+
const { provider, config } = ai;
|
|
7177
|
+
const messages = [{ role: "system", content: TOOL_SYSTEM_PROMPT }];
|
|
6904
7178
|
startTUI({
|
|
6905
7179
|
projectName: "LoveCode AI",
|
|
6906
7180
|
branch: "main",
|
|
@@ -6909,13 +7183,32 @@ async function cmdTUI(options) {
|
|
|
6909
7183
|
framework: "Node.js",
|
|
6910
7184
|
repoStatus: "clean",
|
|
6911
7185
|
messages: [],
|
|
7186
|
+
provider: provider.name,
|
|
7187
|
+
model: config.model,
|
|
6912
7188
|
onSendMessage: async (msg) => {
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
7189
|
+
messages.push({ role: "user", content: msg });
|
|
7190
|
+
for (let round = 0; round < 5; round++) {
|
|
7191
|
+
const response = await provider.chat(messages, config);
|
|
7192
|
+
messages.push({ role: "assistant", content: response });
|
|
7193
|
+
const toolOutputs = await processToolCalls(response, workingDir);
|
|
7194
|
+
if (toolOutputs.length === 0) {
|
|
7195
|
+
return response;
|
|
7196
|
+
}
|
|
7197
|
+
for (const output of toolOutputs) {
|
|
7198
|
+
messages.push({ role: "user", content: output });
|
|
7199
|
+
}
|
|
7200
|
+
}
|
|
7201
|
+
return "Max tool call rounds reached. Please try again with a simpler request.";
|
|
6916
7202
|
},
|
|
6917
|
-
onRunCommand: async (
|
|
6918
|
-
|
|
7203
|
+
onRunCommand: async (command) => {
|
|
7204
|
+
const { execSync: execSync5 } = await import("child_process");
|
|
7205
|
+
try {
|
|
7206
|
+
const output = execSync5(command, { cwd: workingDir, encoding: "utf-8", timeout: 6e4 });
|
|
7207
|
+
return output.trim();
|
|
7208
|
+
} catch (err) {
|
|
7209
|
+
const e = err;
|
|
7210
|
+
return e.stderr || e.message;
|
|
7211
|
+
}
|
|
6919
7212
|
}
|
|
6920
7213
|
});
|
|
6921
7214
|
}
|
|
@@ -6925,15 +7218,18 @@ var tuiCommand = new Command13("tui").alias("ui").description("Launch the Termin
|
|
|
6925
7218
|
Escape Enter vim normal mode
|
|
6926
7219
|
i Enter vim insert mode
|
|
6927
7220
|
j/k Scroll up/down (vim normal mode)
|
|
6928
|
-
Ctrl+N/P Next/previous pane
|
|
6929
7221
|
|
|
6930
7222
|
Slash commands:
|
|
6931
7223
|
/help Show help
|
|
6932
7224
|
/clear Clear messages
|
|
6933
|
-
/theme <n> Change theme
|
|
6934
|
-
/
|
|
7225
|
+
/theme <n> Change theme
|
|
7226
|
+
/connect Show providers
|
|
7227
|
+
/model Show AI config
|
|
7228
|
+
/export Save chat to file
|
|
6935
7229
|
/!<cmd> Run a shell command
|
|
6936
7230
|
/exit Quit TUI
|
|
7231
|
+
|
|
7232
|
+
The AI can read, write, and edit files in the project directory.
|
|
6937
7233
|
`);
|
|
6938
7234
|
|
|
6939
7235
|
// src/commands/plugin.ts
|