@oblivihon/steer 0.1.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/dist/agent.d.ts +32 -0
- package/dist/agent.js +243 -0
- package/dist/auth.d.ts +4 -0
- package/dist/auth.js +34 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +48 -0
- package/dist/cursorAuth.d.ts +10 -0
- package/dist/cursorAuth.js +95 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +138 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +113 -0
- package/package.json +42 -0
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type AgentStreamEvent = {
|
|
2
|
+
kind: "session";
|
|
3
|
+
sessionId: string;
|
|
4
|
+
} | {
|
|
5
|
+
kind: "assistant_delta";
|
|
6
|
+
text: string;
|
|
7
|
+
} | {
|
|
8
|
+
kind: "assistant_final";
|
|
9
|
+
text: string;
|
|
10
|
+
} | {
|
|
11
|
+
kind: "tool_start";
|
|
12
|
+
callId: string;
|
|
13
|
+
name: string;
|
|
14
|
+
detail: string;
|
|
15
|
+
path?: string;
|
|
16
|
+
} | {
|
|
17
|
+
kind: "tool_done";
|
|
18
|
+
callId: string;
|
|
19
|
+
name: string;
|
|
20
|
+
detail: string;
|
|
21
|
+
path?: string;
|
|
22
|
+
} | {
|
|
23
|
+
kind: "error";
|
|
24
|
+
message: string;
|
|
25
|
+
} | {
|
|
26
|
+
kind: "done";
|
|
27
|
+
result: string;
|
|
28
|
+
};
|
|
29
|
+
export declare function isEditTool(name: string): boolean;
|
|
30
|
+
export declare function abortAgent(): void;
|
|
31
|
+
export declare function runAgent(repoPath: string, prompt: string, modelId: string, onEvent: (event: AgentStreamEvent) => void): Promise<void>;
|
|
32
|
+
export declare function getFileDiff(repoPath: string, filePath: string): Promise<string>;
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { createInterface } from "readline";
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import { resolve, relative, isAbsolute } from "path";
|
|
5
|
+
import { execFile } from "child_process";
|
|
6
|
+
import { promisify } from "util";
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
// ── Helpers (from server/agentRunner.ts) ─────────────────────────────
|
|
9
|
+
function extractText(message) {
|
|
10
|
+
if (!message || typeof message !== "object")
|
|
11
|
+
return "";
|
|
12
|
+
const content = message.content;
|
|
13
|
+
if (typeof content === "string")
|
|
14
|
+
return content;
|
|
15
|
+
if (!Array.isArray(content))
|
|
16
|
+
return "";
|
|
17
|
+
return content
|
|
18
|
+
.map((part) => {
|
|
19
|
+
if (part && typeof part === "object" && "text" in part) {
|
|
20
|
+
return String(part.text ?? "");
|
|
21
|
+
}
|
|
22
|
+
return "";
|
|
23
|
+
})
|
|
24
|
+
.join("");
|
|
25
|
+
}
|
|
26
|
+
function toolInfo(toolCall) {
|
|
27
|
+
if (!toolCall)
|
|
28
|
+
return { name: "tool", detail: "" };
|
|
29
|
+
const key = Object.keys(toolCall).find((k) => k.endsWith("ToolCall") ||
|
|
30
|
+
(!["hookAdditionalContexts", "toolCallId", "startedAtMs", "completedAtMs"].includes(k) &&
|
|
31
|
+
typeof toolCall[k] === "object"));
|
|
32
|
+
const name = key ? key.replace(/ToolCall$/, "") : "tool";
|
|
33
|
+
const body = key ? toolCall[key] : undefined;
|
|
34
|
+
const args = body?.args && typeof body.args === "object"
|
|
35
|
+
? body.args
|
|
36
|
+
: undefined;
|
|
37
|
+
let detail = "";
|
|
38
|
+
let path;
|
|
39
|
+
if (args) {
|
|
40
|
+
for (const k of ["path", "filePath", "file_path", "target_file", "targetFile"]) {
|
|
41
|
+
if (typeof args[k] === "string" && String(args[k]).trim()) {
|
|
42
|
+
path = String(args[k]).trim();
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
detail =
|
|
47
|
+
String(args.command ?? args.globPattern ?? path ?? args.targetDirectory ?? "") ||
|
|
48
|
+
JSON.stringify(args).slice(0, 120);
|
|
49
|
+
}
|
|
50
|
+
return { name, detail, path };
|
|
51
|
+
}
|
|
52
|
+
// ── Edit-tool detection (from server/gitDiff.ts) ─────────────────────
|
|
53
|
+
const EDIT_TOOL_RE = /^(write|edit|strreplace|searchreplace|delete|applypatch|editnotebook|create|updatefile|deletefile|writefile)/i;
|
|
54
|
+
export function isEditTool(name) {
|
|
55
|
+
return EDIT_TOOL_RE.test(name.replace(/ToolCall$/i, ""));
|
|
56
|
+
}
|
|
57
|
+
// ── Agent runner ─────────────────────────────────────────────────────
|
|
58
|
+
let activeChild = null;
|
|
59
|
+
export function abortAgent() {
|
|
60
|
+
activeChild?.kill("SIGTERM");
|
|
61
|
+
activeChild = null;
|
|
62
|
+
}
|
|
63
|
+
export function runAgent(repoPath, prompt, modelId, onEvent) {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const args = [
|
|
66
|
+
"agent",
|
|
67
|
+
"--print",
|
|
68
|
+
"--output-format",
|
|
69
|
+
"stream-json",
|
|
70
|
+
"--stream-partial-output",
|
|
71
|
+
"--force",
|
|
72
|
+
"--trust",
|
|
73
|
+
];
|
|
74
|
+
if (modelId && modelId !== "auto") {
|
|
75
|
+
args.push("--model", modelId);
|
|
76
|
+
}
|
|
77
|
+
args.push(prompt);
|
|
78
|
+
const child = spawn("cursor", args, {
|
|
79
|
+
cwd: repoPath,
|
|
80
|
+
env: process.env,
|
|
81
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
82
|
+
});
|
|
83
|
+
activeChild = child;
|
|
84
|
+
let stderr = "";
|
|
85
|
+
let settled = false;
|
|
86
|
+
let assistantBuf = "";
|
|
87
|
+
let gotTerminalEvent = false;
|
|
88
|
+
const finish = (err) => {
|
|
89
|
+
if (settled)
|
|
90
|
+
return;
|
|
91
|
+
settled = true;
|
|
92
|
+
activeChild = null;
|
|
93
|
+
if (err)
|
|
94
|
+
reject(err);
|
|
95
|
+
else
|
|
96
|
+
resolve();
|
|
97
|
+
};
|
|
98
|
+
child.stderr.on("data", (chunk) => {
|
|
99
|
+
stderr += chunk.toString();
|
|
100
|
+
});
|
|
101
|
+
const rl = createInterface({ input: child.stdout });
|
|
102
|
+
rl.on("line", (line) => {
|
|
103
|
+
const trimmed = line.trim();
|
|
104
|
+
if (!trimmed)
|
|
105
|
+
return;
|
|
106
|
+
let ev;
|
|
107
|
+
try {
|
|
108
|
+
ev = JSON.parse(trimmed);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const type = ev.type;
|
|
114
|
+
if (type === "system" && ev.subtype === "init" && ev.session_id) {
|
|
115
|
+
onEvent({ kind: "session", sessionId: String(ev.session_id) });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (type === "assistant") {
|
|
119
|
+
const text = extractText(ev.message);
|
|
120
|
+
if (!text)
|
|
121
|
+
return;
|
|
122
|
+
if ("timestamp_ms" in ev) {
|
|
123
|
+
assistantBuf += text;
|
|
124
|
+
onEvent({ kind: "assistant_delta", text: assistantBuf });
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
assistantBuf = text;
|
|
128
|
+
onEvent({ kind: "assistant_final", text });
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (type === "tool_call") {
|
|
133
|
+
const callId = String(ev.call_id ?? "");
|
|
134
|
+
const info = toolInfo(ev.tool_call);
|
|
135
|
+
if (ev.subtype === "started") {
|
|
136
|
+
onEvent({
|
|
137
|
+
kind: "tool_start",
|
|
138
|
+
callId,
|
|
139
|
+
name: info.name,
|
|
140
|
+
detail: info.detail,
|
|
141
|
+
path: info.path,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
else if (ev.subtype === "completed") {
|
|
145
|
+
onEvent({
|
|
146
|
+
kind: "tool_done",
|
|
147
|
+
callId,
|
|
148
|
+
name: info.name,
|
|
149
|
+
detail: info.detail,
|
|
150
|
+
path: info.path,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (type === "result") {
|
|
156
|
+
gotTerminalEvent = true;
|
|
157
|
+
if (ev.session_id) {
|
|
158
|
+
onEvent({ kind: "session", sessionId: String(ev.session_id) });
|
|
159
|
+
}
|
|
160
|
+
if (ev.is_error) {
|
|
161
|
+
const msg = String(ev.result != null && ev.result !== "" ? ev.result : stderr || "Agent error");
|
|
162
|
+
onEvent({ kind: "error", message: msg });
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
const result = String(ev.result ?? assistantBuf);
|
|
166
|
+
onEvent({ kind: "done", result });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
child.on("error", (err) => {
|
|
171
|
+
if (!gotTerminalEvent) {
|
|
172
|
+
onEvent({ kind: "error", message: err.message });
|
|
173
|
+
}
|
|
174
|
+
finish(err);
|
|
175
|
+
});
|
|
176
|
+
child.on("close", (code) => {
|
|
177
|
+
if (settled)
|
|
178
|
+
return;
|
|
179
|
+
if (code === 0) {
|
|
180
|
+
if (!gotTerminalEvent && assistantBuf) {
|
|
181
|
+
onEvent({ kind: "done", result: assistantBuf });
|
|
182
|
+
}
|
|
183
|
+
finish();
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
const msg = stderr.trim() || `Agent exited with code ${code ?? "unknown"}`;
|
|
187
|
+
if (!gotTerminalEvent) {
|
|
188
|
+
onEvent({ kind: "error", message: msg });
|
|
189
|
+
}
|
|
190
|
+
finish(new Error(msg));
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
// ── File diff (from server/gitDiff.ts) ───────────────────────────────
|
|
196
|
+
function resolveInRepo(repoPath, filePath) {
|
|
197
|
+
const abs = isAbsolute(filePath) ? filePath : resolve(repoPath, filePath);
|
|
198
|
+
const rel = relative(repoPath, abs);
|
|
199
|
+
if (rel.startsWith("..") || rel === "")
|
|
200
|
+
return null;
|
|
201
|
+
return rel.replace(/\\/g, "/");
|
|
202
|
+
}
|
|
203
|
+
async function runGit(repoPath, args) {
|
|
204
|
+
try {
|
|
205
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
206
|
+
cwd: repoPath,
|
|
207
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
208
|
+
encoding: "utf8",
|
|
209
|
+
});
|
|
210
|
+
return stdout;
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
const e = err;
|
|
214
|
+
return e.stdout || "";
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async function isTracked(repoPath, rel) {
|
|
218
|
+
try {
|
|
219
|
+
await execFileAsync("git", ["ls-files", "--error-unmatch", "--", rel], {
|
|
220
|
+
cwd: repoPath,
|
|
221
|
+
encoding: "utf8",
|
|
222
|
+
});
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
export async function getFileDiff(repoPath, filePath) {
|
|
230
|
+
const rel = resolveInRepo(repoPath, filePath);
|
|
231
|
+
if (!rel)
|
|
232
|
+
return "";
|
|
233
|
+
const abs = resolve(repoPath, rel);
|
|
234
|
+
let patch = await runGit(repoPath, ["diff", "HEAD", "--", rel]);
|
|
235
|
+
if (!patch)
|
|
236
|
+
patch = await runGit(repoPath, ["diff", "--", rel]);
|
|
237
|
+
if (!patch)
|
|
238
|
+
patch = await runGit(repoPath, ["diff", "--cached", "--", rel]);
|
|
239
|
+
if (!patch && existsSync(abs) && !(await isTracked(repoPath, rel))) {
|
|
240
|
+
patch = await runGit(repoPath, ["diff", "--no-index", "--", "/dev/null", rel]);
|
|
241
|
+
}
|
|
242
|
+
return patch.trim();
|
|
243
|
+
}
|
package/dist/auth.d.ts
ADDED
package/dist/auth.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { loadConfig, saveConfig, clearConfig } from "./config.js";
|
|
2
|
+
export async function login(serverUrl, email, password) {
|
|
3
|
+
const url = `${serverUrl.replace(/\/+$/, "")}/api/auth/login`;
|
|
4
|
+
const res = await fetch(url, {
|
|
5
|
+
method: "POST",
|
|
6
|
+
headers: { "Content-Type": "application/json" },
|
|
7
|
+
body: JSON.stringify({ email, password }),
|
|
8
|
+
});
|
|
9
|
+
if (!res.ok) {
|
|
10
|
+
const body = await res.text();
|
|
11
|
+
throw new Error(`Login failed (${res.status}): ${body}`);
|
|
12
|
+
}
|
|
13
|
+
const data = (await res.json());
|
|
14
|
+
if (!data.token) {
|
|
15
|
+
throw new Error("Server did not return a token");
|
|
16
|
+
}
|
|
17
|
+
const config = {
|
|
18
|
+
serverUrl: serverUrl.replace(/\/+$/, ""),
|
|
19
|
+
token: data.token,
|
|
20
|
+
email,
|
|
21
|
+
};
|
|
22
|
+
saveConfig(config);
|
|
23
|
+
return config;
|
|
24
|
+
}
|
|
25
|
+
export function logout() {
|
|
26
|
+
clearConfig();
|
|
27
|
+
}
|
|
28
|
+
export function getAuthHeaders() {
|
|
29
|
+
const config = loadConfig();
|
|
30
|
+
if (!config) {
|
|
31
|
+
throw new Error("Not logged in. Run `steer login` first.");
|
|
32
|
+
}
|
|
33
|
+
return { Authorization: `Bearer ${config.token}` };
|
|
34
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface Config {
|
|
2
|
+
serverUrl: string;
|
|
3
|
+
token: string;
|
|
4
|
+
email: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function getConfigDir(): string;
|
|
7
|
+
export declare function loadConfig(): Config | null;
|
|
8
|
+
export declare function saveConfig(config: Config): void;
|
|
9
|
+
export declare function clearConfig(): void;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync, rmSync, existsSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
const CONFIG_DIR_NAME = "steer";
|
|
5
|
+
const LEGACY_CONFIG_DIR_NAME = "shared-agent";
|
|
6
|
+
const CONFIG_FILE = "config.json";
|
|
7
|
+
export function getConfigDir() {
|
|
8
|
+
const dir = join(homedir(), ".config", CONFIG_DIR_NAME);
|
|
9
|
+
if (!existsSync(dir)) {
|
|
10
|
+
mkdirSync(dir, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
return dir;
|
|
13
|
+
}
|
|
14
|
+
export function loadConfig() {
|
|
15
|
+
const filePath = join(getConfigDir(), CONFIG_FILE);
|
|
16
|
+
const legacyPath = join(homedir(), ".config", LEGACY_CONFIG_DIR_NAME, CONFIG_FILE);
|
|
17
|
+
const path = existsSync(filePath)
|
|
18
|
+
? filePath
|
|
19
|
+
: existsSync(legacyPath)
|
|
20
|
+
? legacyPath
|
|
21
|
+
: null;
|
|
22
|
+
if (!path)
|
|
23
|
+
return null;
|
|
24
|
+
try {
|
|
25
|
+
const raw = readFileSync(path, "utf-8");
|
|
26
|
+
const parsed = JSON.parse(raw);
|
|
27
|
+
if (!parsed.serverUrl || !parsed.token || !parsed.email)
|
|
28
|
+
return null;
|
|
29
|
+
// Migrate legacy config into ~/.config/steer/
|
|
30
|
+
if (path === legacyPath) {
|
|
31
|
+
saveConfig(parsed);
|
|
32
|
+
}
|
|
33
|
+
return parsed;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function saveConfig(config) {
|
|
40
|
+
const filePath = join(getConfigDir(), CONFIG_FILE);
|
|
41
|
+
writeFileSync(filePath, JSON.stringify(config, null, 2), "utf-8");
|
|
42
|
+
}
|
|
43
|
+
export function clearConfig() {
|
|
44
|
+
const filePath = join(getConfigDir(), CONFIG_FILE);
|
|
45
|
+
if (existsSync(filePath)) {
|
|
46
|
+
rmSync(filePath);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function getCursorAuth(): Promise<{
|
|
2
|
+
authenticated: boolean;
|
|
3
|
+
email?: string;
|
|
4
|
+
error?: string;
|
|
5
|
+
}>;
|
|
6
|
+
/**
|
|
7
|
+
* Ensure the Cursor CLI is logged in before starting the worker.
|
|
8
|
+
* Prompts interactive `cursor agent login` if not authenticated.
|
|
9
|
+
*/
|
|
10
|
+
export declare function ensureCursorAuth(): Promise<void>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { spawn, execFile } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
export async function getCursorAuth() {
|
|
7
|
+
try {
|
|
8
|
+
const { stdout } = await execFileAsync("cursor", ["agent", "whoami", "--format", "json"], {
|
|
9
|
+
timeout: 30_000,
|
|
10
|
+
encoding: "utf8",
|
|
11
|
+
maxBuffer: 1024 * 1024,
|
|
12
|
+
env: { ...process.env, NO_COLOR: "1" },
|
|
13
|
+
});
|
|
14
|
+
const data = JSON.parse(stdout.trim());
|
|
15
|
+
if (data.isAuthenticated || data.status === "authenticated") {
|
|
16
|
+
return {
|
|
17
|
+
authenticated: true,
|
|
18
|
+
email: data.userInfo?.email,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
return { authenticated: false };
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
const e = err;
|
|
25
|
+
// cursor sometimes prints JSON on stdout even with non-zero exit
|
|
26
|
+
if (e.stdout) {
|
|
27
|
+
try {
|
|
28
|
+
const data = JSON.parse(String(e.stdout).trim());
|
|
29
|
+
if (data.isAuthenticated || data.status === "authenticated") {
|
|
30
|
+
return {
|
|
31
|
+
authenticated: true,
|
|
32
|
+
email: data.userInfo?.email,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return { authenticated: false };
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// fall through
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (e.code === "ENOENT") {
|
|
42
|
+
return {
|
|
43
|
+
authenticated: false,
|
|
44
|
+
error: "`cursor` CLI not found. Install Cursor CLI first: https://cursor.com",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
authenticated: false,
|
|
49
|
+
error: e.stderr?.trim() || e.message || "Failed to check Cursor auth",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function runCursorLogin() {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
const child = spawn("cursor", ["agent", "login"], {
|
|
56
|
+
stdio: "inherit",
|
|
57
|
+
env: process.env,
|
|
58
|
+
});
|
|
59
|
+
child.on("error", (err) => {
|
|
60
|
+
console.error(chalk.red(`Failed to start cursor login: ${err.message}`));
|
|
61
|
+
resolve(1);
|
|
62
|
+
});
|
|
63
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Ensure the Cursor CLI is logged in before starting the worker.
|
|
68
|
+
* Prompts interactive `cursor agent login` if not authenticated.
|
|
69
|
+
*/
|
|
70
|
+
export async function ensureCursorAuth() {
|
|
71
|
+
const spinner = ora("Checking Cursor CLI login…").start();
|
|
72
|
+
let auth = await getCursorAuth();
|
|
73
|
+
if (auth.error && auth.error.includes("not found")) {
|
|
74
|
+
spinner.fail(auth.error);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
if (auth.authenticated) {
|
|
78
|
+
spinner.succeed(`Cursor CLI logged in as ${chalk.cyan(auth.email || "unknown")}`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
spinner.warn("Cursor CLI is not logged in");
|
|
82
|
+
console.log(chalk.yellow("\n Local agents need Cursor CLI auth. Opening login…\n"));
|
|
83
|
+
const code = await runCursorLogin();
|
|
84
|
+
if (code !== 0) {
|
|
85
|
+
console.error(chalk.red("\nCursor login failed or was cancelled. Run `cursor agent login` and try again."));
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
const verify = ora("Verifying Cursor login…").start();
|
|
89
|
+
auth = await getCursorAuth();
|
|
90
|
+
if (!auth.authenticated) {
|
|
91
|
+
verify.fail("Still not logged into Cursor. Run `cursor agent login` and try again.");
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
verify.succeed(`Cursor CLI logged in as ${chalk.cyan(auth.email || "unknown")}`);
|
|
95
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
import { createInterface } from "readline";
|
|
6
|
+
import { login, logout } from "./auth.js";
|
|
7
|
+
import { loadConfig } from "./config.js";
|
|
8
|
+
import { ensureCursorAuth } from "./cursorAuth.js";
|
|
9
|
+
import { startWorker } from "./worker.js";
|
|
10
|
+
const program = new Command();
|
|
11
|
+
function prompt(question) {
|
|
12
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
rl.question(question, (answer) => {
|
|
15
|
+
rl.close();
|
|
16
|
+
resolve(answer.trim());
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function promptSecret(question) {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
process.stdout.write(question);
|
|
23
|
+
const stdin = process.stdin;
|
|
24
|
+
const wasRaw = stdin.isRaw;
|
|
25
|
+
if (stdin.isTTY)
|
|
26
|
+
stdin.setRawMode(true);
|
|
27
|
+
let input = "";
|
|
28
|
+
const onData = (ch) => {
|
|
29
|
+
const c = ch.toString();
|
|
30
|
+
if (c === "\n" || c === "\r") {
|
|
31
|
+
stdin.removeListener("data", onData);
|
|
32
|
+
if (stdin.isTTY)
|
|
33
|
+
stdin.setRawMode(wasRaw ?? false);
|
|
34
|
+
process.stdout.write("\n");
|
|
35
|
+
resolve(input);
|
|
36
|
+
}
|
|
37
|
+
else if (c === "\u0003") {
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
else if (c === "\u007F" || c === "\b") {
|
|
41
|
+
input = input.slice(0, -1);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
input += c;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
stdin.resume();
|
|
48
|
+
stdin.on("data", onData);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
program
|
|
52
|
+
.name("steer")
|
|
53
|
+
.description("CLI worker for Steer — run local Cursor agents")
|
|
54
|
+
.version("0.1.0");
|
|
55
|
+
program
|
|
56
|
+
.command("login")
|
|
57
|
+
.description("Log in to the Steer server")
|
|
58
|
+
.action(async () => {
|
|
59
|
+
try {
|
|
60
|
+
const serverUrl = await prompt("Server URL: ");
|
|
61
|
+
const email = await prompt("Email: ");
|
|
62
|
+
const password = await promptSecret("Password: ");
|
|
63
|
+
if (!serverUrl || !email || !password) {
|
|
64
|
+
console.error(chalk.red("All fields are required."));
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
const spinner = ora("Logging in…").start();
|
|
68
|
+
try {
|
|
69
|
+
const config = await login(serverUrl, email, password);
|
|
70
|
+
spinner.succeed(`Logged in as ${chalk.cyan(config.email)}`);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
spinner.fail(err.message);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
program
|
|
82
|
+
.command("logout")
|
|
83
|
+
.description("Clear stored credentials")
|
|
84
|
+
.action(() => {
|
|
85
|
+
logout();
|
|
86
|
+
console.log(chalk.green("Logged out."));
|
|
87
|
+
});
|
|
88
|
+
program
|
|
89
|
+
.command("status")
|
|
90
|
+
.description("Show connection status and logged-in user")
|
|
91
|
+
.action(async () => {
|
|
92
|
+
const config = loadConfig();
|
|
93
|
+
if (!config) {
|
|
94
|
+
console.log(chalk.yellow("Not logged in."));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
console.log(chalk.blue("Logged in as:"), chalk.cyan(config.email));
|
|
98
|
+
console.log(chalk.blue("Server:"), config.serverUrl);
|
|
99
|
+
const spinner = ora("Checking server…").start();
|
|
100
|
+
try {
|
|
101
|
+
const res = await fetch(`${config.serverUrl}/api/rooms`, {
|
|
102
|
+
headers: { Authorization: `Bearer ${config.token}` },
|
|
103
|
+
});
|
|
104
|
+
if (!res.ok) {
|
|
105
|
+
spinner.fail(`Server returned ${res.status}`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const rooms = (await res.json());
|
|
109
|
+
spinner.succeed(`Server online — ${rooms.length} room(s)`);
|
|
110
|
+
for (const room of rooms) {
|
|
111
|
+
console.log(chalk.gray(` • ${room.name || room.id}`));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
spinner.fail(`Cannot reach server: ${err.message}`);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
program
|
|
119
|
+
.command("start")
|
|
120
|
+
.description("Start the worker daemon")
|
|
121
|
+
.option("--repo <path>", "Override repository path for all prompts")
|
|
122
|
+
.action(async (opts) => {
|
|
123
|
+
const config = loadConfig();
|
|
124
|
+
if (!config) {
|
|
125
|
+
console.error(chalk.red("Not logged in. Run `steer login` first."));
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
// Require Cursor CLI login before accepting Local prompts
|
|
129
|
+
await ensureCursorAuth();
|
|
130
|
+
console.log(chalk.blue.bold("\n Steer Worker\n"));
|
|
131
|
+
console.log(chalk.gray(` Server: ${config.serverUrl}`));
|
|
132
|
+
console.log(chalk.gray(` User: ${config.email}`));
|
|
133
|
+
if (opts.repo)
|
|
134
|
+
console.log(chalk.gray(` Repo: ${opts.repo}`));
|
|
135
|
+
console.log();
|
|
136
|
+
startWorker(opts.repo);
|
|
137
|
+
});
|
|
138
|
+
program.parse();
|
package/dist/worker.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function startWorker(repoPathOverride?: string): void;
|
package/dist/worker.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { io } from "socket.io-client";
|
|
2
|
+
import { hostname } from "os";
|
|
3
|
+
import { createHash } from "crypto";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { loadConfig } from "./config.js";
|
|
6
|
+
import { runAgent, abortAgent, isEditTool, getFileDiff, } from "./agent.js";
|
|
7
|
+
function generateWorkerId(email) {
|
|
8
|
+
const raw = `${hostname()}-${email}`;
|
|
9
|
+
return createHash("sha256").update(raw).digest("hex").slice(0, 16);
|
|
10
|
+
}
|
|
11
|
+
export function startWorker(repoPathOverride) {
|
|
12
|
+
const config = loadConfig();
|
|
13
|
+
if (!config) {
|
|
14
|
+
console.error(chalk.red("Not logged in. Run `steer login` first."));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
const workerId = generateWorkerId(config.email);
|
|
18
|
+
const serverUrl = config.serverUrl;
|
|
19
|
+
console.log(chalk.blue("Connecting to"), serverUrl);
|
|
20
|
+
console.log(chalk.gray(`Worker ID: ${workerId}`));
|
|
21
|
+
const socket = io(`${serverUrl}/worker`, {
|
|
22
|
+
auth: { token: config.token },
|
|
23
|
+
reconnection: true,
|
|
24
|
+
reconnectionAttempts: Infinity,
|
|
25
|
+
reconnectionDelay: 2000,
|
|
26
|
+
reconnectionDelayMax: 30000,
|
|
27
|
+
transports: ["websocket", "polling"],
|
|
28
|
+
});
|
|
29
|
+
socket.on("connect", () => {
|
|
30
|
+
console.log(chalk.green("✓ Connected to server"));
|
|
31
|
+
socket.emit("worker:ready", { workerId });
|
|
32
|
+
});
|
|
33
|
+
socket.on("disconnect", (reason) => {
|
|
34
|
+
console.log(chalk.yellow(`Disconnected: ${reason}`));
|
|
35
|
+
});
|
|
36
|
+
socket.on("connect_error", (err) => {
|
|
37
|
+
console.error(chalk.red(`Connection error: ${err.message}`));
|
|
38
|
+
});
|
|
39
|
+
socket.on("worker:run-prompt", (payload) => {
|
|
40
|
+
const { roomId, prompt, repoPath: payloadRepoPath, modelId } = payload;
|
|
41
|
+
const repoPath = repoPathOverride || payloadRepoPath;
|
|
42
|
+
console.log(chalk.cyan(`\n━━━ Running prompt in room ${roomId} ━━━`));
|
|
43
|
+
console.log(chalk.gray(`Repo: ${repoPath}`));
|
|
44
|
+
console.log(chalk.gray(`Model: ${modelId}`));
|
|
45
|
+
console.log(chalk.gray(`Prompt: ${prompt.slice(0, 100)}${prompt.length > 100 ? "…" : ""}`));
|
|
46
|
+
const editedPaths = new Set();
|
|
47
|
+
let lastMsgId = "";
|
|
48
|
+
const onEvent = (event) => {
|
|
49
|
+
socket.emit("worker:agent-event", { roomId, event });
|
|
50
|
+
if (event.kind === "tool_start") {
|
|
51
|
+
console.log(chalk.yellow(` ▸ ${event.name} ${event.detail}`));
|
|
52
|
+
}
|
|
53
|
+
else if (event.kind === "tool_done") {
|
|
54
|
+
console.log(chalk.green(` ✓ ${event.name} ${event.detail}`));
|
|
55
|
+
if (isEditTool(event.name) && event.path) {
|
|
56
|
+
editedPaths.add(event.path);
|
|
57
|
+
lastMsgId = event.callId;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else if (event.kind === "assistant_final") {
|
|
61
|
+
console.log(chalk.white(` Assistant: ${event.text.slice(0, 120)}…`));
|
|
62
|
+
}
|
|
63
|
+
else if (event.kind === "error") {
|
|
64
|
+
console.error(chalk.red(` ✗ Error: ${event.message}`));
|
|
65
|
+
}
|
|
66
|
+
else if (event.kind === "done") {
|
|
67
|
+
console.log(chalk.green(" ✓ Agent finished"));
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
runAgent(repoPath, prompt, modelId, onEvent)
|
|
71
|
+
.then(async () => {
|
|
72
|
+
for (const filePath of editedPaths) {
|
|
73
|
+
try {
|
|
74
|
+
const patch = await getFileDiff(repoPath, filePath);
|
|
75
|
+
if (patch) {
|
|
76
|
+
socket.emit("worker:file-diff", {
|
|
77
|
+
roomId,
|
|
78
|
+
msgId: lastMsgId,
|
|
79
|
+
toolName: "edit",
|
|
80
|
+
path: filePath,
|
|
81
|
+
patch,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// diff failures are non-fatal
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
.catch((err) => {
|
|
91
|
+
console.error(chalk.red(`Agent error: ${err.message}`));
|
|
92
|
+
socket.emit("worker:agent-event", {
|
|
93
|
+
roomId,
|
|
94
|
+
event: { kind: "error", message: err.message },
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
socket.on("worker:abort", (_payload) => {
|
|
99
|
+
console.log(chalk.yellow(" ⚠ Abort requested"));
|
|
100
|
+
abortAgent();
|
|
101
|
+
});
|
|
102
|
+
process.on("SIGINT", () => {
|
|
103
|
+
console.log(chalk.gray("\nShutting down worker…"));
|
|
104
|
+
abortAgent();
|
|
105
|
+
socket.disconnect();
|
|
106
|
+
process.exit(0);
|
|
107
|
+
});
|
|
108
|
+
process.on("SIGTERM", () => {
|
|
109
|
+
abortAgent();
|
|
110
|
+
socket.disconnect();
|
|
111
|
+
process.exit(0);
|
|
112
|
+
});
|
|
113
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@oblivihon/steer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI worker for Steer — run local Cursor agents for multiplayer sessions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"steer": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"dev": "tsx src/index.ts",
|
|
15
|
+
"prepublishOnly": "pnpm build"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"cursor",
|
|
19
|
+
"agent",
|
|
20
|
+
"cli",
|
|
21
|
+
"steer",
|
|
22
|
+
"multiplayer"
|
|
23
|
+
],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"commander": "^13.0.0",
|
|
33
|
+
"socket.io-client": "^4.8.0",
|
|
34
|
+
"chalk": "^5.4.0",
|
|
35
|
+
"ora": "^8.0.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^22.0.0",
|
|
39
|
+
"tsx": "^4.19.0",
|
|
40
|
+
"typescript": "^5.7.0"
|
|
41
|
+
}
|
|
42
|
+
}
|