@bniladridas/cursor 0.1.18 → 0.1.19
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/.github/workflows/release.yml +1 -0
- package/AGENTS.md +66 -36
- package/CMakeLists.txt +54 -10
- package/DESIGN.md +775 -0
- package/README.md +2 -2
- package/cli.js +26 -3
- package/include/agent.h +55 -6
- package/include/services/ai_service.h +3 -0
- package/include/services/git_service.h +4 -0
- package/include/utils/platform.h +2 -2
- package/include/utils/ui.h +19 -0
- package/install.js +1 -0
- package/install.sh +2 -1
- package/package.json +1 -1
- package/release/checksums.txt +4 -4
- package/release/cursor-linux/cursor_v0.1.19_linux_amd64.tar.gz +0 -0
- package/release/cursor-macos/cursor-0.1.19.arm64_sequoia.bottle.tar.gz +0 -0
- package/release/cursor-macos/cursor_v0.1.19_darwin_arm64.tar.gz +0 -0
- package/release/cursor-windows/cursor__windows_amd64.zip +0 -0
- package/src/agent.cpp +1129 -157
- package/src/main.cpp +4 -1
- package/src/services/ai_service.cpp +7 -23
- package/src/services/git_service.cpp +45 -1
- package/src/services/web_service.cpp +7 -3
- package/src/utils/ui.cpp +94 -8
- package/src/utils/version.cpp +48 -15
- package/tests/e2e/run_local_batch.sh +131 -0
- package/tests/main_test.cpp +163 -0
- package/release/cursor-linux/cursor_v0.1.18_linux_amd64.tar.gz +0 -0
- package/release/cursor-macos/cursor-0.1.18.arm64_sequoia.bottle.tar.gz +0 -0
- package/release/cursor-macos/cursor_v0.1.18_darwin_arm64.tar.gz +0 -0
package/src/main.cpp
CHANGED
|
@@ -62,7 +62,10 @@ int main(int argc, char *argv[]) {
|
|
|
62
62
|
Utils::Config::load_environment();
|
|
63
63
|
Utils::UI::print_logo();
|
|
64
64
|
|
|
65
|
-
std::string latest
|
|
65
|
+
std::string latest;
|
|
66
|
+
if (!Utils::Config::has_env_var("CURSOR_SKIP_UPDATE_CHECK")) {
|
|
67
|
+
latest = Version::check_update();
|
|
68
|
+
}
|
|
66
69
|
if (!latest.empty()) {
|
|
67
70
|
std::cout << Utils::Color::YELLOW
|
|
68
71
|
<< "Update available: v" << Version::get_version() << " -> v"
|
|
@@ -166,34 +166,18 @@ nlohmann::json AIService::create_payload(const std::string &user_input,
|
|
|
166
166
|
return create_standard_payload("deepseek-chat", user_input, system_prompt);
|
|
167
167
|
case Core::AgentMode::MODE_OPENAI: // OpenAI
|
|
168
168
|
return create_standard_payload("gpt-4", user_input, system_prompt);
|
|
169
|
-
case Core::AgentMode::MODE_LLAMA_3B: //
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
case Core::AgentMode::MODE_LLAMA_LATEST: // Llama latest (local)
|
|
177
|
-
return {{"model", "llama3.2:latest"},
|
|
178
|
-
{"stream", false},
|
|
179
|
-
{"messages",
|
|
180
|
-
{{{"role", "system"}, {"content", system_prompt}},
|
|
181
|
-
{{"role", "user"}, {"content", user_input}}}}};
|
|
182
|
-
|
|
183
|
-
case Core::AgentMode::MODE_LLAMA_31: // Llama 3.1 (local)
|
|
184
|
-
return {{"model", "llama3.1:latest"},
|
|
185
|
-
{"stream", false},
|
|
186
|
-
{"messages",
|
|
187
|
-
{{{"role", "system"}, {"content", system_prompt}},
|
|
188
|
-
{{"role", "user"}, {"content", user_input}}}}};
|
|
189
|
-
|
|
190
|
-
default: // Fallback to Llama 3B
|
|
191
|
-
return {{"model", "llama3.2:3b"},
|
|
169
|
+
case Core::AgentMode::MODE_LLAMA_3B: // Local Ollama models
|
|
170
|
+
case Core::AgentMode::MODE_LLAMA_LATEST:
|
|
171
|
+
case Core::AgentMode::MODE_LLAMA_31:
|
|
172
|
+
default: {
|
|
173
|
+
std::string model = model_name_.empty() ? "llama3.2:3b" : model_name_;
|
|
174
|
+
return {{"model", model},
|
|
192
175
|
{"stream", false},
|
|
193
176
|
{"messages",
|
|
194
177
|
{{{"role", "system"}, {"content", system_prompt}},
|
|
195
178
|
{{"role", "user"}, {"content", user_input}}}}};
|
|
196
179
|
}
|
|
180
|
+
}
|
|
197
181
|
}
|
|
198
182
|
|
|
199
183
|
std::string AIService::parse_cerebras_stream(const std::string &response) {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
#include "utils/platform.h"
|
|
3
3
|
#include "utils/validation.h"
|
|
4
4
|
#include <filesystem>
|
|
5
|
+
#include <unordered_set>
|
|
5
6
|
#include <fstream>
|
|
6
7
|
#include <regex>
|
|
7
8
|
#include <sstream>
|
|
@@ -67,6 +68,50 @@ std::string GitService::get_git_status(const std::string &path) {
|
|
|
67
68
|
return result.empty() ? "Working directory clean" : result;
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
std::vector<std::string>
|
|
72
|
+
GitService::get_working_tree_changed_files(const std::string &path) {
|
|
73
|
+
std::vector<std::string> files;
|
|
74
|
+
|
|
75
|
+
if (!is_git_repository(path)) {
|
|
76
|
+
return files;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
auto collect_files = [&](const std::string &command) {
|
|
80
|
+
FILE *pipe = Utils::Platform::open_process(command, "r");
|
|
81
|
+
if (!pipe) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
char buffer[256];
|
|
86
|
+
while (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
|
|
87
|
+
std::string file(buffer);
|
|
88
|
+
if (!file.empty() && file.back() == '\n') {
|
|
89
|
+
file.pop_back();
|
|
90
|
+
}
|
|
91
|
+
if (!file.empty()) {
|
|
92
|
+
files.push_back(file);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
Utils::Platform::close_process(pipe);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
std::string base = "cd \"" + path + "\" && ";
|
|
99
|
+
collect_files(base + "git diff --name-only --cached");
|
|
100
|
+
collect_files(base + "git diff --name-only");
|
|
101
|
+
collect_files(base + "git ls-files --others --exclude-standard");
|
|
102
|
+
|
|
103
|
+
std::vector<std::string> unique_files;
|
|
104
|
+
std::unordered_set<std::string> seen;
|
|
105
|
+
for (const auto &file : files) {
|
|
106
|
+
if (!seen.count(file)) {
|
|
107
|
+
seen.insert(file);
|
|
108
|
+
unique_files.push_back(file);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return unique_files;
|
|
113
|
+
}
|
|
114
|
+
|
|
70
115
|
std::vector<std::string> GitService::get_changed_files(const std::string &path,
|
|
71
116
|
int days) {
|
|
72
117
|
std::vector<std::string> files;
|
|
@@ -87,7 +132,6 @@ std::vector<std::string> GitService::get_changed_files(const std::string &path,
|
|
|
87
132
|
char buffer[256];
|
|
88
133
|
while (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
|
|
89
134
|
std::string file(buffer);
|
|
90
|
-
// Remove newline
|
|
91
135
|
if (!file.empty() && file.back() == '\n') {
|
|
92
136
|
file.pop_back();
|
|
93
137
|
}
|
|
@@ -155,9 +155,13 @@ bool WebService::is_valid_url(const std::string &url) {
|
|
|
155
155
|
if (url.find("http://") == 0 || url.find("https://") == 0) {
|
|
156
156
|
// Must have at least a domain after protocol
|
|
157
157
|
size_t domain_start = url.find("://") + 3;
|
|
158
|
-
if (domain_start < url.length()
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
if (domain_start < url.length()) {
|
|
159
|
+
std::string domain = url.substr(domain_start);
|
|
160
|
+
// Allow localhost without dot
|
|
161
|
+
if (domain.starts_with("localhost") || domain.starts_with("127.") ||
|
|
162
|
+
url.find('.', domain_start) != std::string::npos) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
161
165
|
}
|
|
162
166
|
}
|
|
163
167
|
return false;
|
package/src/utils/ui.cpp
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
#include <iostream>
|
|
5
5
|
#include <string>
|
|
6
6
|
#include <thread>
|
|
7
|
+
#ifdef _WIN32
|
|
8
|
+
#include <windows.h>
|
|
9
|
+
#else
|
|
10
|
+
#include <sys/ioctl.h>
|
|
11
|
+
#include <unistd.h>
|
|
12
|
+
#endif
|
|
7
13
|
|
|
8
14
|
namespace Utils {
|
|
9
15
|
|
|
@@ -286,22 +292,102 @@ void UI::print_system_info(const std::string &mode, const std::string &model) {
|
|
|
286
292
|
// ===== Ready Interface =====
|
|
287
293
|
void UI::print_ready_interface(const std::string &mode,
|
|
288
294
|
const std::string &model) {
|
|
289
|
-
|
|
290
|
-
print_quick_help();
|
|
291
|
-
|
|
292
|
-
std::cout << "Ready - Type a command:\n";
|
|
293
|
-
|
|
294
|
-
std::cout << "Ready - Type a command or chat naturally:\n";
|
|
295
|
+
std::cout << Color::DIM << "[" << mode << " | " << model << "]" << Color::RESET << "\n";
|
|
295
296
|
}
|
|
296
297
|
|
|
297
298
|
// ===== Prompted Input =====
|
|
298
299
|
std::string UI::prompt_user(const std::string &prompt_text) {
|
|
299
|
-
// Only print the prompt once with consistent formatting
|
|
300
300
|
std::cout << Color::BOLD << "[You] " << prompt_text << Color::RESET << " > ";
|
|
301
|
-
|
|
302
301
|
std::string input;
|
|
303
302
|
std::getline(std::cin, input);
|
|
304
303
|
return input;
|
|
305
304
|
}
|
|
306
305
|
|
|
306
|
+
// ===== Terminal Management =====
|
|
307
|
+
int UI::get_terminal_width() {
|
|
308
|
+
#ifndef _WIN32
|
|
309
|
+
struct winsize w;
|
|
310
|
+
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == 0 && w.ws_col > 0)
|
|
311
|
+
return w.ws_col;
|
|
312
|
+
#endif
|
|
313
|
+
return 80;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
int UI::get_terminal_height() {
|
|
317
|
+
#ifndef _WIN32
|
|
318
|
+
struct winsize w;
|
|
319
|
+
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == 0 && w.ws_row > 0)
|
|
320
|
+
return w.ws_row;
|
|
321
|
+
#endif
|
|
322
|
+
return 24;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
void UI::clear_screen() {
|
|
326
|
+
std::cout << "\033[2J\033[H";
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
void UI::enter_chat_mode() {
|
|
330
|
+
clear_screen();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
void UI::exit_chat_mode() {
|
|
334
|
+
std::cout << "\033[?25h";
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
void UI::clear_line() {
|
|
338
|
+
std::cout << "\033[2K\r";
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
void UI::cursor_to(int line, int col) {
|
|
342
|
+
std::cout << "\033[" << line << ";" << col << "H";
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
void UI::draw_status_line(const std::string &mode,
|
|
346
|
+
const std::string &model,
|
|
347
|
+
const std::string &dir) {
|
|
348
|
+
cursor_to(1, 1);
|
|
349
|
+
clear_line();
|
|
350
|
+
std::cout << Color::DIM << "* " << Color::RESET
|
|
351
|
+
<< mode << " "
|
|
352
|
+
<< Color::BOLD << model << Color::RESET
|
|
353
|
+
<< " " << Color::DIM << dir << Color::RESET;
|
|
354
|
+
std::cout.flush();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
void UI::draw_context_line(const std::string &hints) {
|
|
358
|
+
int h = get_terminal_height();
|
|
359
|
+
cursor_to(h - 2, 1);
|
|
360
|
+
clear_line();
|
|
361
|
+
if (!hints.empty()) {
|
|
362
|
+
std::cout << Color::DIM << hints << Color::RESET;
|
|
363
|
+
}
|
|
364
|
+
std::cout.flush();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
void UI::draw_input_bar(const std::string &text, int cursor_pos) {
|
|
368
|
+
int h = get_terminal_height();
|
|
369
|
+
cursor_to(h - 1, 1);
|
|
370
|
+
clear_line();
|
|
371
|
+
std::cout << Color::CYAN << "> " << Color::RESET;
|
|
372
|
+
if (text.empty()) {
|
|
373
|
+
std::cout << Color::DIM << "Ask anything..." << Color::RESET;
|
|
374
|
+
cursor_to(h - 1, 3);
|
|
375
|
+
} else {
|
|
376
|
+
std::cout << text;
|
|
377
|
+
int pos = (cursor_pos < 0) ? (int)text.size() : cursor_pos;
|
|
378
|
+
cursor_to(h - 1, 3 + pos);
|
|
379
|
+
}
|
|
380
|
+
std::cout.flush();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
void UI::enable_mouse() {
|
|
384
|
+
std::cout << "\033[?1000h\033[?1006h";
|
|
385
|
+
std::cout.flush();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
void UI::disable_mouse() {
|
|
389
|
+
std::cout << "\033[?1006l\033[?1000l";
|
|
390
|
+
std::cout.flush();
|
|
391
|
+
}
|
|
392
|
+
|
|
307
393
|
} // namespace Utils
|
package/src/utils/version.cpp
CHANGED
|
@@ -22,13 +22,13 @@ size_t write_to_file(void *ptr, size_t size, size_t nmemb, void *userdata) {
|
|
|
22
22
|
return fwrite(ptr, size, nmemb, file);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
std::string
|
|
25
|
+
std::pair<std::string, std::string> get_release_asset_names(const std::string &version) {
|
|
26
26
|
#ifdef _WIN32
|
|
27
|
-
return "cursor-windows.exe";
|
|
27
|
+
return {"cursor_v" + version + "_windows_amd64.zip", "cursor-windows.exe"};
|
|
28
28
|
#elif defined(__APPLE__)
|
|
29
|
-
return "cursor-macos";
|
|
29
|
+
return {"cursor_v" + version + "_darwin_arm64.tar.gz", "cursor-macos"};
|
|
30
30
|
#else
|
|
31
|
-
return "cursor-linux";
|
|
31
|
+
return {"cursor_v" + version + "_linux_amd64.tar.gz", "cursor-linux"};
|
|
32
32
|
#endif
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -103,14 +103,15 @@ std::string check_update() {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
bool download_and_install(const std::string &version) {
|
|
106
|
-
|
|
106
|
+
auto [archive, binary_name] = get_release_asset_names(version);
|
|
107
107
|
std::string url =
|
|
108
|
-
"https://github.com/bniladridas/cursor/releases/download/" + version +
|
|
109
|
-
"/" +
|
|
108
|
+
"https://github.com/bniladridas/cursor/releases/download/v" + version +
|
|
109
|
+
"/" + archive;
|
|
110
110
|
|
|
111
111
|
std::string tmp = "/tmp/cursor-update-" + version;
|
|
112
|
+
std::string tmpDir = "/tmp/cursor-update-extract-" + version;
|
|
112
113
|
#ifdef _WIN32
|
|
113
|
-
tmp += ".
|
|
114
|
+
tmp += ".zip";
|
|
114
115
|
#endif
|
|
115
116
|
|
|
116
117
|
std::cout << "Downloading v" << version << "...\n";
|
|
@@ -139,12 +140,6 @@ bool download_and_install(const std::string &version) {
|
|
|
139
140
|
return false;
|
|
140
141
|
}
|
|
141
142
|
|
|
142
|
-
std::filesystem::permissions(tmp,
|
|
143
|
-
std::filesystem::perms::owner_exec |
|
|
144
|
-
std::filesystem::perms::owner_read |
|
|
145
|
-
std::filesystem::perms::owner_write,
|
|
146
|
-
std::filesystem::perm_options::add);
|
|
147
|
-
|
|
148
143
|
std::string exe = get_exe_path();
|
|
149
144
|
if (exe.empty()) {
|
|
150
145
|
std::cerr << "Downloaded to: " << tmp << "\nManually replace binary.\n";
|
|
@@ -160,14 +155,52 @@ bool download_and_install(const std::string &version) {
|
|
|
160
155
|
return false;
|
|
161
156
|
}
|
|
162
157
|
|
|
163
|
-
std::filesystem::
|
|
158
|
+
std::filesystem::create_directories(tmpDir, ec);
|
|
159
|
+
if (ec) {
|
|
160
|
+
std::cerr << "Failed to create extract dir: " << ec.message() << "\n";
|
|
161
|
+
std::filesystem::rename(backup, exe);
|
|
162
|
+
std::filesystem::remove(tmp);
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
{
|
|
167
|
+
#ifdef _WIN32
|
|
168
|
+
std::string cmd =
|
|
169
|
+
"powershell -NoProfile -Command \"Expand-Archive -LiteralPath '" +
|
|
170
|
+
tmp + "' -DestinationPath '" + tmpDir + "' -Force\"";
|
|
171
|
+
#else
|
|
172
|
+
std::string cmd = "tar -xzf \"" + tmp + "\" -C \"" + tmpDir + "\"";
|
|
173
|
+
#endif
|
|
174
|
+
int rc = std::system(cmd.c_str());
|
|
175
|
+
if (rc != 0) {
|
|
176
|
+
std::cerr << "Extract failed\n";
|
|
177
|
+
std::filesystem::rename(backup, exe);
|
|
178
|
+
std::filesystem::remove(tmp);
|
|
179
|
+
std::filesystem::remove_all(tmpDir);
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
std::string extracted = tmpDir + "/" + binary_name;
|
|
185
|
+
if (!std::filesystem::exists(extracted)) {
|
|
186
|
+
std::cerr << "Extracted binary not found: " << extracted << "\n";
|
|
187
|
+
std::filesystem::rename(backup, exe);
|
|
188
|
+
std::filesystem::remove(tmp);
|
|
189
|
+
std::filesystem::remove_all(tmpDir);
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
std::filesystem::rename(extracted, exe, ec);
|
|
164
194
|
if (ec) {
|
|
165
195
|
std::cerr << "Failed to install: " << ec.message() << "\n";
|
|
166
196
|
std::filesystem::rename(backup, exe);
|
|
167
197
|
std::filesystem::remove(tmp);
|
|
198
|
+
std::filesystem::remove_all(tmpDir);
|
|
168
199
|
return false;
|
|
169
200
|
}
|
|
170
201
|
|
|
202
|
+
std::filesystem::remove(tmp);
|
|
203
|
+
std::filesystem::remove_all(tmpDir);
|
|
171
204
|
std::cout << "Updated to v" << version << " successfully!\n";
|
|
172
205
|
return true;
|
|
173
206
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
repo_root="$(cd "$script_dir/../.." && pwd)"
|
|
6
|
+
|
|
7
|
+
binary_path="${BINARY_PATH:-${1:-}}"
|
|
8
|
+
if [[ -z "$binary_path" ]]; then
|
|
9
|
+
for candidate in \
|
|
10
|
+
"$repo_root/build/bin/cursor-agent" \
|
|
11
|
+
"$repo_root/build/bin/Release/cursor-agent.exe" \
|
|
12
|
+
"$repo_root/build/bin/cursor-agent.exe"; do
|
|
13
|
+
if [[ -x "$candidate" ]]; then
|
|
14
|
+
binary_path="$candidate"
|
|
15
|
+
break
|
|
16
|
+
fi
|
|
17
|
+
done
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
if [[ -z "$binary_path" ]]; then
|
|
21
|
+
echo "Usage: BINARY_PATH=/path/to/cursor-agent $0 [binary-path]" >&2
|
|
22
|
+
echo "Could not find a built cursor-agent under $repo_root/build/bin." >&2
|
|
23
|
+
exit 2
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
if [[ ! -x "$binary_path" ]]; then
|
|
27
|
+
echo "Binary is not executable: $binary_path" >&2
|
|
28
|
+
exit 2
|
|
29
|
+
fi
|
|
30
|
+
binary_path="$(cd "$(dirname "$binary_path")" && pwd)/$(basename "$binary_path")"
|
|
31
|
+
|
|
32
|
+
workdir="$(mktemp -d)"
|
|
33
|
+
mkdir -p "$workdir/home"
|
|
34
|
+
cleanup() {
|
|
35
|
+
rm -rf "$workdir"
|
|
36
|
+
}
|
|
37
|
+
trap cleanup EXIT
|
|
38
|
+
|
|
39
|
+
input_file="$workdir/input.txt"
|
|
40
|
+
output_file="$workdir/output.txt"
|
|
41
|
+
|
|
42
|
+
cat >"$input_file" <<'EOF'
|
|
43
|
+
help
|
|
44
|
+
version
|
|
45
|
+
write:e2e.txt alpha beta
|
|
46
|
+
read:e2e.txt
|
|
47
|
+
replace:e2e.txt:alpha:omega:1
|
|
48
|
+
read:e2e.txt
|
|
49
|
+
grep:omega:.:e2e.txt
|
|
50
|
+
remember:e2e-memory-fact
|
|
51
|
+
memory
|
|
52
|
+
clear
|
|
53
|
+
cmd:echo e2e-cmd-ok
|
|
54
|
+
!echo e2e-bang-ok
|
|
55
|
+
!
|
|
56
|
+
echo e2e-shell-mode-ok
|
|
57
|
+
!
|
|
58
|
+
/help
|
|
59
|
+
/stats
|
|
60
|
+
exit
|
|
61
|
+
EOF
|
|
62
|
+
|
|
63
|
+
set +e
|
|
64
|
+
bash -c 'cd "$1" && HOME="$1/home" CURSOR_SKIP_UPDATE_CHECK=1 "$2" <"$3" >"$4" 2>&1' bash "$workdir" "$binary_path" "$input_file" "$output_file" &
|
|
65
|
+
agent_pid=$!
|
|
66
|
+
deadline=$((SECONDS + 20))
|
|
67
|
+
while kill -0 "$agent_pid" 2>/dev/null; do
|
|
68
|
+
if ((SECONDS >= deadline)); then
|
|
69
|
+
kill "$agent_pid" 2>/dev/null || true
|
|
70
|
+
wait "$agent_pid" 2>/dev/null
|
|
71
|
+
run_status=124
|
|
72
|
+
break
|
|
73
|
+
fi
|
|
74
|
+
sleep 1
|
|
75
|
+
done
|
|
76
|
+
if [[ -z "${run_status:-}" ]]; then
|
|
77
|
+
wait "$agent_pid"
|
|
78
|
+
run_status=$?
|
|
79
|
+
fi
|
|
80
|
+
set -e
|
|
81
|
+
if [[ $run_status -ne 0 ]]; then
|
|
82
|
+
echo "Agent batch run failed with exit code $run_status" >&2
|
|
83
|
+
echo "=== Full output ===" >&2
|
|
84
|
+
cat "$output_file" >&2 || true
|
|
85
|
+
exit 1
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
assert_contains() {
|
|
89
|
+
local expected="$1"
|
|
90
|
+
if ! grep -Fq "$expected" "$output_file"; then
|
|
91
|
+
echo "Missing expected output: $expected" >&2
|
|
92
|
+
echo "=== Full output ===" >&2
|
|
93
|
+
cat "$output_file" >&2
|
|
94
|
+
exit 1
|
|
95
|
+
fi
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
assert_not_contains() {
|
|
99
|
+
local unexpected="$1"
|
|
100
|
+
if grep -Fq "$unexpected" "$output_file"; then
|
|
101
|
+
echo "Unexpected output found: $unexpected" >&2
|
|
102
|
+
echo "=== Full output ===" >&2
|
|
103
|
+
cat "$output_file" >&2
|
|
104
|
+
exit 1
|
|
105
|
+
fi
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
assert_contains "Welcome to Cursor"
|
|
109
|
+
assert_contains "Commands"
|
|
110
|
+
assert_contains "Cursor v"
|
|
111
|
+
assert_contains "File 'e2e.txt' written successfully"
|
|
112
|
+
assert_contains "alpha beta"
|
|
113
|
+
assert_contains "Successfully replaced 1 occurrence(s)"
|
|
114
|
+
assert_contains "omega beta"
|
|
115
|
+
assert_contains "Found 1 matches:"
|
|
116
|
+
assert_contains "Remembered: e2e-memory-fact"
|
|
117
|
+
assert_contains "e2e-memory-fact"
|
|
118
|
+
assert_contains "Session memory cleared"
|
|
119
|
+
assert_contains "e2e-cmd-ok"
|
|
120
|
+
assert_contains "e2e-bang-ok"
|
|
121
|
+
assert_contains "Entering shell mode"
|
|
122
|
+
assert_contains "e2e-shell-mode-ok"
|
|
123
|
+
assert_contains "Exiting shell mode"
|
|
124
|
+
assert_contains "Available meta commands:"
|
|
125
|
+
assert_contains "Session Statistics:"
|
|
126
|
+
assert_contains "Goodbye"
|
|
127
|
+
assert_contains "Agent run completed"
|
|
128
|
+
assert_not_contains "AI service returned status code"
|
|
129
|
+
assert_not_contains "AI service unavailable"
|
|
130
|
+
|
|
131
|
+
echo "Local batch E2E passed"
|
package/tests/main_test.cpp
CHANGED
|
@@ -1,15 +1,178 @@
|
|
|
1
|
+
#include "services/ai_service.h"
|
|
2
|
+
#include "memory_manager.h"
|
|
3
|
+
#include "agent.h"
|
|
1
4
|
#include "version.h"
|
|
2
5
|
#include <gtest/gtest.h>
|
|
6
|
+
#include <functional>
|
|
7
|
+
#include <iostream>
|
|
8
|
+
#include <sstream>
|
|
9
|
+
|
|
10
|
+
static std::string capture_stdout(const std::function<void()> &fn) {
|
|
11
|
+
std::ostringstream buffer;
|
|
12
|
+
std::streambuf *old_buf = std::cout.rdbuf(buffer.rdbuf());
|
|
13
|
+
fn();
|
|
14
|
+
std::cout.rdbuf(old_buf);
|
|
15
|
+
return buffer.str();
|
|
16
|
+
}
|
|
3
17
|
|
|
4
18
|
// Basic test to verify the testing framework works
|
|
5
19
|
TEST(BasicTest, SanityCheck) { EXPECT_EQ(1 + 1, 2); }
|
|
6
20
|
|
|
21
|
+
TEST(AgentTest, DirectCommandRouting) {
|
|
22
|
+
EXPECT_TRUE(Core::Agent::is_direct_command_input("cmd:ls"));
|
|
23
|
+
EXPECT_TRUE(Core::Agent::is_direct_command_input("build:make"));
|
|
24
|
+
EXPECT_TRUE(Core::Agent::is_direct_command_input("grep:main:src"));
|
|
25
|
+
EXPECT_FALSE(Core::Agent::is_direct_command_input("foo:bar"));
|
|
26
|
+
EXPECT_FALSE(Core::Agent::is_direct_command_input("abc:def:ghi"));
|
|
27
|
+
EXPECT_TRUE(Core::Agent::is_direct_command_input("memory"));
|
|
28
|
+
EXPECT_TRUE(Core::Agent::is_direct_command_input("clear"));
|
|
29
|
+
EXPECT_TRUE(Core::Agent::is_direct_command_input("forget"));
|
|
30
|
+
EXPECT_FALSE(Core::Agent::is_direct_command_input("memory show"));
|
|
31
|
+
EXPECT_FALSE(Core::Agent::is_direct_command_input("clear the build cache"));
|
|
32
|
+
EXPECT_FALSE(Core::Agent::is_direct_command_input("forget about that error"));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
TEST(AgentTest, MapNaturalLanguageToDirectCommands) {
|
|
36
|
+
EXPECT_TRUE(Core::Agent::map_nl_to_direct_command(
|
|
37
|
+
"can you check the files we changed").has_value());
|
|
38
|
+
EXPECT_EQ(*Core::Agent::map_nl_to_direct_command(
|
|
39
|
+
"can you check the files we changed"), "git:status");
|
|
40
|
+
EXPECT_EQ(*Core::Agent::map_nl_to_direct_command(
|
|
41
|
+
"show changed files in repo"), "git:status");
|
|
42
|
+
EXPECT_EQ(*Core::Agent::map_nl_to_direct_command(
|
|
43
|
+
"what is the commit history"), "git:log");
|
|
44
|
+
EXPECT_EQ(*Core::Agent::map_nl_to_direct_command(
|
|
45
|
+
"find TODO in code"), "todos:.");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
TEST(AgentTest, MapAllNaturalLanguageToDirectCommands) {
|
|
49
|
+
EXPECT_EQ(*Core::Agent::map_nl_to_direct_command("run make test"), "cmd:make test");
|
|
50
|
+
EXPECT_EQ(*Core::Agent::map_nl_to_direct_command("build the project"), "build:make");
|
|
51
|
+
EXPECT_EQ(*Core::Agent::map_nl_to_direct_command("read file src/main.cpp"), "read:src/main.cpp");
|
|
52
|
+
EXPECT_EQ(*Core::Agent::map_nl_to_direct_command("save file notes.txt as hello world"), "write:notes.txt hello world");
|
|
53
|
+
EXPECT_EQ(*Core::Agent::map_nl_to_direct_command("remember the api key is secret"), "remember:the api key is secret");
|
|
54
|
+
EXPECT_EQ(*Core::Agent::map_nl_to_direct_command("forget everything"), "forget");
|
|
55
|
+
EXPECT_EQ(*Core::Agent::map_nl_to_direct_command("show memory"), "memory");
|
|
56
|
+
EXPECT_EQ(*Core::Agent::map_nl_to_direct_command("clear memory"), "clear");
|
|
57
|
+
EXPECT_EQ(*Core::Agent::map_nl_to_direct_command("replace foo with bar in file README.md"), "replace:README.md:foo:bar");
|
|
58
|
+
EXPECT_EQ(*Core::Agent::map_nl_to_direct_command("open owner/repo on github"), "github:repo:owner/repo");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
TEST(AgentTest, AgenticWorkflowCommands) {
|
|
62
|
+
Core::Agent agent;
|
|
63
|
+
std::string output = capture_stdout([&] {
|
|
64
|
+
agent.process_user_input("/goal set Fix the issue");
|
|
65
|
+
agent.process_user_input("/task add Investigate logs");
|
|
66
|
+
agent.process_user_input("/task list");
|
|
67
|
+
});
|
|
68
|
+
EXPECT_NE(output.find("Goal set: Fix the issue"), std::string::npos);
|
|
69
|
+
EXPECT_NE(output.find("Task added: [1] Investigate logs"), std::string::npos);
|
|
70
|
+
EXPECT_NE(output.find("Active tasks:"), std::string::npos);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
TEST(AgentTest, DebugModeTogglesVerbosePipelineOutput) {
|
|
74
|
+
Core::Agent agent;
|
|
75
|
+
|
|
76
|
+
std::string output = capture_stdout([&] {
|
|
77
|
+
agent.process_user_input("git status");
|
|
78
|
+
});
|
|
79
|
+
EXPECT_EQ(output.find("INPUT RECEIVED"), std::string::npos);
|
|
80
|
+
EXPECT_EQ(output.find("Input classification"), std::string::npos);
|
|
81
|
+
|
|
82
|
+
output = capture_stdout([&] {
|
|
83
|
+
agent.process_user_input("/debug");
|
|
84
|
+
agent.process_user_input("git status");
|
|
85
|
+
});
|
|
86
|
+
EXPECT_NE(output.find("Verbose mode ON"), std::string::npos);
|
|
87
|
+
EXPECT_NE(output.find("INPUT RECEIVED"), std::string::npos);
|
|
88
|
+
EXPECT_NE(output.find("Input classification"), std::string::npos);
|
|
89
|
+
|
|
90
|
+
output = capture_stdout([&] {
|
|
91
|
+
agent.process_user_input("/debug");
|
|
92
|
+
agent.process_user_input("git status");
|
|
93
|
+
});
|
|
94
|
+
EXPECT_NE(output.find("Verbose mode OFF"), std::string::npos);
|
|
95
|
+
size_t off_pos = output.find("Verbose mode OFF");
|
|
96
|
+
EXPECT_EQ(output.find("INPUT RECEIVED", off_pos), std::string::npos);
|
|
97
|
+
EXPECT_EQ(output.find("Input classification", off_pos), std::string::npos);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
TEST(AgentTest, DetectGitStatusQuery) {
|
|
101
|
+
EXPECT_TRUE(Core::Agent::is_git_status_query(
|
|
102
|
+
"can you check the files we changed"));
|
|
103
|
+
EXPECT_TRUE(Core::Agent::is_git_status_query("what changed in the repo"));
|
|
104
|
+
EXPECT_TRUE(Core::Agent::is_git_status_query("show changed files"));
|
|
105
|
+
EXPECT_FALSE(Core::Agent::is_git_status_query("please help me with a diagram"));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
TEST(AgentTest, ShowAgentDocumentation) {
|
|
109
|
+
Core::Agent agent;
|
|
110
|
+
std::string output = capture_stdout([&] {
|
|
111
|
+
agent.show_agent_documentation();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
EXPECT_NE(output.find("Runtime help from AGENTS.md:"),
|
|
115
|
+
std::string::npos);
|
|
116
|
+
EXPECT_NE(output.find("Runtime Help Integration"), std::string::npos);
|
|
117
|
+
}
|
|
118
|
+
|
|
7
119
|
// Test for version functionality
|
|
8
120
|
TEST(VersionTest, VersionCommand) {
|
|
9
121
|
const char *version = Version::get_version();
|
|
10
122
|
EXPECT_STREQ(version, cursor_version_string);
|
|
11
123
|
}
|
|
12
124
|
|
|
125
|
+
TEST(AgentTest, FormattedGitStatusOutput) {
|
|
126
|
+
Core::Agent agent;
|
|
127
|
+
capture_stdout([&] { agent.process_user_input("/debug"); });
|
|
128
|
+
std::vector<std::string> empty_files;
|
|
129
|
+
std::vector<std::string> files = {"src/main.cpp", "include/agent.h", "CMakeLists.txt"};
|
|
130
|
+
|
|
131
|
+
// Test with empty files (should show clean status)
|
|
132
|
+
std::string output = capture_stdout([&] {
|
|
133
|
+
agent.show_git_status_results(empty_files);
|
|
134
|
+
});
|
|
135
|
+
EXPECT_NE(output.find("clean"), std::string::npos);
|
|
136
|
+
|
|
137
|
+
// Test with files (should show file list with numbers)
|
|
138
|
+
output = capture_stdout([&] {
|
|
139
|
+
agent.show_git_status_results(files);
|
|
140
|
+
});
|
|
141
|
+
EXPECT_NE(output.find("src/main.cpp"), std::string::npos);
|
|
142
|
+
EXPECT_NE(output.find("include/agent.h"), std::string::npos);
|
|
143
|
+
EXPECT_NE(output.find("CMakeLists.txt"), std::string::npos);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
TEST(AgentTest, FormattedFilePreview) {
|
|
147
|
+
Core::Agent agent;
|
|
148
|
+
capture_stdout([&] { agent.process_user_input("/debug"); });
|
|
149
|
+
std::string content = "int main() {\n std::cout << \"Hello\";\n return 0;\n}\n";
|
|
150
|
+
|
|
151
|
+
// Test file preview with syntax highlighting
|
|
152
|
+
std::string output = capture_stdout([&] {
|
|
153
|
+
agent.show_file_preview("test.cpp", content, 10);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Should show filename
|
|
157
|
+
EXPECT_NE(output.find("test.cpp"), std::string::npos);
|
|
158
|
+
// Should show content with line numbers
|
|
159
|
+
EXPECT_NE(output.find("main"), std::string::npos);
|
|
160
|
+
EXPECT_NE(output.find("Hello"), std::string::npos);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
TEST(AgentTest, FormattedFilePreviewHandlesErrors) {
|
|
164
|
+
Core::Agent agent;
|
|
165
|
+
capture_stdout([&] { agent.process_user_input("/debug"); });
|
|
166
|
+
std::string error_msg = "Error: File 'nonexistent.txt' does not exist";
|
|
167
|
+
|
|
168
|
+
// Test that error messages are displayed
|
|
169
|
+
std::string output = capture_stdout([&] {
|
|
170
|
+
agent.show_file_preview("nonexistent.txt", error_msg, 10);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
EXPECT_NE(output.find("Error:"), std::string::npos);
|
|
174
|
+
}
|
|
175
|
+
|
|
13
176
|
int main(int argc, char **argv) {
|
|
14
177
|
::testing::InitGoogleTest(&argc, argv);
|
|
15
178
|
return RUN_ALL_TESTS();
|
|
Binary file
|
|
Binary file
|
|
Binary file
|