stella-coder 5.3.4 → 5.3.6
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/Desktop/main.ts +254 -0
- package/Desktop/preload.ts +36 -0
- package/Desktop/renderer/index.html +12 -0
- package/Desktop/shared/contracts.ts +88 -0
- package/Desktop/tsconfig.main.json +14 -0
- package/Desktop/vite.config.ts +17 -0
- package/install.ps1 +39 -0
- package/package.json +43 -2
- package/pnpm-workspace.yaml +1 -0
- package/stella-cli/index.mjs +39 -1
- package/stella-cli/web-parser.mjs +11 -1
- package/test-stream.mjs +23 -0
package/Desktop/main.ts
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { app, BrowserWindow, dialog, ipcMain } from "electron";
|
|
2
|
+
import { execFile, spawn, type ChildProcess } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import crypto from "node:crypto";
|
|
8
|
+
import { pathToFileURL } from "node:url";
|
|
9
|
+
import type {
|
|
10
|
+
AgentOutputEvent,
|
|
11
|
+
AgentStatusEvent,
|
|
12
|
+
DesktopSettings,
|
|
13
|
+
ProjectInfo,
|
|
14
|
+
SystemMetrics,
|
|
15
|
+
TaskRecord,
|
|
16
|
+
} from "./shared/contracts";
|
|
17
|
+
|
|
18
|
+
const execFileAsync = promisify(execFile);
|
|
19
|
+
const DEFAULT_SETTINGS: DesktopSettings = { model: "mimo-v2.5-free", recentWorkspaces: [] };
|
|
20
|
+
const MAX_HISTORY = 20;
|
|
21
|
+
const MAX_OUTPUT_LENGTH = 12000;
|
|
22
|
+
let mainWindow: BrowserWindow | null = null;
|
|
23
|
+
let activeProcess: ChildProcess | null = null;
|
|
24
|
+
let activeTask: TaskRecord | null = null;
|
|
25
|
+
let cpuSample: Array<{ idle: number; total: number }> | null = null;
|
|
26
|
+
|
|
27
|
+
function desktopDataPath(file: string) {
|
|
28
|
+
return path.join(app.getPath("userData"), file);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sourceRoot() {
|
|
32
|
+
return app.isPackaged ? path.join(process.resourcesPath, "app.asar.unpacked") : path.resolve(__dirname, "../../..");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function cliPath() {
|
|
36
|
+
return path.join(sourceRoot(), "stella-cli", "index.mjs");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function loadJson<T>(file: string, fallback: T): Promise<T> {
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(await fs.readFile(file, "utf8")) as T;
|
|
42
|
+
} catch {
|
|
43
|
+
return fallback;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function saveJson(file: string, value: unknown) {
|
|
48
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
49
|
+
await fs.writeFile(file, JSON.stringify(value, null, 2), "utf8");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function getSettings(): Promise<DesktopSettings> {
|
|
53
|
+
const saved = await loadJson<Partial<DesktopSettings>>(desktopDataPath("settings.json"), {});
|
|
54
|
+
return { ...DEFAULT_SETTINGS, ...saved, recentWorkspaces: saved.recentWorkspaces ?? [] };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function saveSettings(next: Partial<DesktopSettings>) {
|
|
58
|
+
const settings = { ...(await getSettings()), ...next };
|
|
59
|
+
await saveJson(desktopDataPath("settings.json"), settings);
|
|
60
|
+
return settings;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function getTasks() {
|
|
64
|
+
return loadJson<TaskRecord[]>(desktopDataPath("tasks.json"), []);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function storeTask(task: TaskRecord) {
|
|
68
|
+
const tasks = await getTasks();
|
|
69
|
+
const withoutCurrent = tasks.filter((entry) => entry.id !== task.id);
|
|
70
|
+
await saveJson(desktopDataPath("tasks.json"), [task, ...withoutCurrent].slice(0, MAX_HISTORY));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function redactSecrets(value: string) {
|
|
74
|
+
return value
|
|
75
|
+
.replace(/Bearer\s+[^\s]+/gi, "Bearer [скрыто]")
|
|
76
|
+
.replace(/\b(?:sk-|zen-|opk_)[A-Za-z0-9_-]{8,}\b/g, "[скрыто]");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function emit(channel: "agent:output" | "agent:status" | "metrics:update", payload: AgentOutputEvent | AgentStatusEvent | SystemMetrics) {
|
|
80
|
+
mainWindow?.webContents.send(channel, payload);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function appendOutput(channel: "stdout" | "stderr", raw: string) {
|
|
84
|
+
if (!activeTask) return;
|
|
85
|
+
const text = redactSecrets(raw);
|
|
86
|
+
activeTask.output = (activeTask.output + text).slice(-MAX_OUTPUT_LENGTH);
|
|
87
|
+
emit("agent:output", { taskId: activeTask.id, channel, text });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function getProjectInfo(workspace: string): Promise<ProjectInfo> {
|
|
91
|
+
const resolved = path.resolve(workspace);
|
|
92
|
+
const name = path.basename(resolved) || resolved;
|
|
93
|
+
try {
|
|
94
|
+
await fs.access(resolved);
|
|
95
|
+
} catch {
|
|
96
|
+
throw new Error("Папка проекта недоступна");
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const { stdout } = await execFileAsync("git", ["-C", resolved, "status", "--short", "--branch"], { windowsHide: true });
|
|
100
|
+
const lines = stdout.split(/\r?\n/).filter(Boolean);
|
|
101
|
+
const branchLine = lines.shift() ?? "";
|
|
102
|
+
const branch = branchLine.startsWith("## ") ? branchLine.slice(3).split("...")[0] : null;
|
|
103
|
+
const changedFiles = lines.map((line) => line.slice(3).trim()).filter(Boolean).slice(0, 100);
|
|
104
|
+
return { path: resolved, name, branch, changedFiles, isGitRepository: true };
|
|
105
|
+
} catch {
|
|
106
|
+
return { path: resolved, name, branch: null, changedFiles: [], isGitRepository: false };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function cpuTotals() {
|
|
111
|
+
return os.cpus().map((cpu) => ({
|
|
112
|
+
idle: cpu.times.idle,
|
|
113
|
+
total: Object.values(cpu.times).reduce((sum, value) => sum + value, 0),
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function powerShellJson(command: string): Promise<unknown> {
|
|
118
|
+
const { stdout } = await execFileAsync("powershell.exe", ["-NoProfile", "-Command", command], { windowsHide: true, timeout: 5000 });
|
|
119
|
+
return JSON.parse(stdout.trim() || "null");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function getMetrics(): Promise<SystemMetrics> {
|
|
123
|
+
const now = cpuTotals();
|
|
124
|
+
let cpuPercent: number | null = null;
|
|
125
|
+
if (cpuSample?.length === now.length) {
|
|
126
|
+
const idle = now.reduce((sum, entry, index) => sum + entry.idle - (cpuSample?.[index]?.idle ?? 0), 0);
|
|
127
|
+
const total = now.reduce((sum, entry, index) => sum + entry.total - (cpuSample?.[index]?.total ?? 0), 0);
|
|
128
|
+
cpuPercent = total > 0 ? Math.round((1 - idle / total) * 100) : null;
|
|
129
|
+
}
|
|
130
|
+
cpuSample = now;
|
|
131
|
+
const totalMemory = os.totalmem();
|
|
132
|
+
const memoryUsedGb = Number(((totalMemory - os.freemem()) / 1024 ** 3).toFixed(1));
|
|
133
|
+
const memoryTotalGb = Number((totalMemory / 1024 ** 3).toFixed(1));
|
|
134
|
+
let diskUsedGb: number | null = null;
|
|
135
|
+
let diskTotalGb: number | null = null;
|
|
136
|
+
let gpuName: string | null = null;
|
|
137
|
+
let gpuPercent: number | null = null;
|
|
138
|
+
let gpuTemperature: number | null = null;
|
|
139
|
+
try {
|
|
140
|
+
const disk = await powerShellJson("Get-CimInstance Win32_LogicalDisk -Filter \"DeviceID='C:'\" | Select-Object Size,FreeSpace | ConvertTo-Json -Compress") as { Size?: number; FreeSpace?: number };
|
|
141
|
+
if (disk?.Size) {
|
|
142
|
+
diskTotalGb = Number((disk.Size / 1024 ** 3).toFixed(1));
|
|
143
|
+
diskUsedGb = Number(((disk.Size - (disk.FreeSpace ?? 0)) / 1024 ** 3).toFixed(1));
|
|
144
|
+
}
|
|
145
|
+
} catch {}
|
|
146
|
+
try {
|
|
147
|
+
const { stdout } = await execFileAsync("nvidia-smi", ["--query-gpu=name,utilization.gpu,temperature.gpu", "--format=csv,noheader,nounits"], { windowsHide: true, timeout: 5000 });
|
|
148
|
+
const [name, usage, temperature] = stdout.trim().split(",").map((entry) => entry.trim());
|
|
149
|
+
gpuName = name || null;
|
|
150
|
+
gpuPercent = Number.isFinite(Number(usage)) ? Number(usage) : null;
|
|
151
|
+
gpuTemperature = Number.isFinite(Number(temperature)) ? Number(temperature) : null;
|
|
152
|
+
} catch {}
|
|
153
|
+
return { cpuPercent, memoryUsedGb, memoryTotalGb, diskUsedGb, diskTotalGb, gpuName, gpuPercent, gpuTemperature };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function securityModule() {
|
|
157
|
+
const modulePath = path.join(sourceRoot(), "stella-cli", "security.mjs");
|
|
158
|
+
return import(pathToFileURL(modulePath).href) as Promise<{
|
|
159
|
+
getApiKey: () => { apiKey?: string } | null;
|
|
160
|
+
saveApiKey: (key: string) => { ok: boolean; error?: string };
|
|
161
|
+
deleteApiKey: () => { ok: boolean; error?: string };
|
|
162
|
+
}>;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function finishActiveTask(status: TaskRecord["status"]) {
|
|
166
|
+
if (!activeTask) return;
|
|
167
|
+
activeTask.status = status;
|
|
168
|
+
activeTask.finishedAt = new Date().toISOString();
|
|
169
|
+
activeTask.durationMs = Date.now() - Date.parse(activeTask.startedAt);
|
|
170
|
+
const project = await getProjectInfo(activeTask.workspace);
|
|
171
|
+
activeTask.changedFiles = project.changedFiles;
|
|
172
|
+
await storeTask(activeTask);
|
|
173
|
+
emit("agent:status", { task: activeTask });
|
|
174
|
+
activeTask = null;
|
|
175
|
+
activeProcess = null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function startTask(input: { workspace: string; prompt: string; model: string }) {
|
|
179
|
+
if (activeProcess) throw new Error("Сейчас уже выполняется задача");
|
|
180
|
+
const prompt = input.prompt.trim();
|
|
181
|
+
if (!prompt) throw new Error("Введите задачу для Stella");
|
|
182
|
+
const project = await getProjectInfo(input.workspace);
|
|
183
|
+
const task: TaskRecord = {
|
|
184
|
+
id: crypto.randomUUID(), prompt, workspace: project.path, model: input.model,
|
|
185
|
+
status: "running", startedAt: new Date().toISOString(), output: "", changedFiles: [],
|
|
186
|
+
};
|
|
187
|
+
activeTask = task;
|
|
188
|
+
const child = spawn(process.execPath, [cliPath(), "-p", prompt, "--model", input.model], {
|
|
189
|
+
cwd: project.path,
|
|
190
|
+
windowsHide: true,
|
|
191
|
+
env: { ...process.env, ELECTRON_RUN_AS_NODE: "1", FORCE_COLOR: "0" },
|
|
192
|
+
});
|
|
193
|
+
activeProcess = child;
|
|
194
|
+
emit("agent:status", { task });
|
|
195
|
+
child.stdout?.on("data", (chunk: Buffer) => appendOutput("stdout", chunk.toString()));
|
|
196
|
+
child.stderr?.on("data", (chunk: Buffer) => appendOutput("stderr", chunk.toString()));
|
|
197
|
+
child.on("error", (error) => appendOutput("stderr", `\n${error.message}\n`));
|
|
198
|
+
child.on("close", async (code) => {
|
|
199
|
+
const status = code === 0 ? "completed" : "failed";
|
|
200
|
+
await finishActiveTask(status);
|
|
201
|
+
});
|
|
202
|
+
return task;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function stopTask(taskId: string) {
|
|
206
|
+
if (!activeProcess || !activeTask || activeTask.id !== taskId) return;
|
|
207
|
+
const pid = activeProcess.pid;
|
|
208
|
+
if (pid && process.platform === "win32") {
|
|
209
|
+
try { await execFileAsync("taskkill", ["/pid", String(pid), "/t", "/f"], { windowsHide: true }); } catch {}
|
|
210
|
+
} else {
|
|
211
|
+
activeProcess.kill();
|
|
212
|
+
}
|
|
213
|
+
await finishActiveTask("cancelled");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function createWindow() {
|
|
217
|
+
mainWindow = new BrowserWindow({
|
|
218
|
+
width: 1200, height: 760, minWidth: 980, minHeight: 660,
|
|
219
|
+
frame: false, backgroundColor: "#f8f8fc", show: false,
|
|
220
|
+
webPreferences: { preload: path.join(__dirname, "preload.js"), contextIsolation: true, nodeIntegration: false, sandbox: true },
|
|
221
|
+
});
|
|
222
|
+
const devUrl = process.env.ELECTRON_RENDERER_URL;
|
|
223
|
+
if (devUrl) void mainWindow.loadURL(devUrl); else void mainWindow.loadFile(path.join(__dirname, "../renderer/index.html"));
|
|
224
|
+
mainWindow.once("ready-to-show", () => mainWindow?.show());
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
app.whenReady().then(() => {
|
|
228
|
+
ipcMain.handle("workspace:select", async () => {
|
|
229
|
+
const result = await dialog.showOpenDialog(mainWindow!, { properties: ["openDirectory", "createDirectory"] });
|
|
230
|
+
if (result.canceled || !result.filePaths[0]) return null;
|
|
231
|
+
const project = await getProjectInfo(result.filePaths[0]);
|
|
232
|
+
const settings = await getSettings();
|
|
233
|
+
await saveSettings({ recentWorkspaces: [project.path, ...settings.recentWorkspaces.filter((entry) => entry !== project.path)].slice(0, 8) });
|
|
234
|
+
return project;
|
|
235
|
+
});
|
|
236
|
+
ipcMain.handle("project:get", (_event, workspace: string) => getProjectInfo(workspace));
|
|
237
|
+
ipcMain.handle("settings:get", () => getSettings());
|
|
238
|
+
ipcMain.handle("settings:model", (_event, model: string) => saveSettings({ model }));
|
|
239
|
+
ipcMain.handle("key:has", async () => Boolean((await securityModule()).getApiKey()?.apiKey));
|
|
240
|
+
ipcMain.handle("key:save", async (_event, key: string) => (await securityModule()).saveApiKey(key));
|
|
241
|
+
ipcMain.handle("key:delete", async () => (await securityModule()).deleteApiKey());
|
|
242
|
+
ipcMain.handle("metrics:get", () => getMetrics());
|
|
243
|
+
ipcMain.handle("tasks:list", () => getTasks());
|
|
244
|
+
ipcMain.handle("task:start", (_event, input) => startTask(input));
|
|
245
|
+
ipcMain.handle("task:stop", (_event, id: string) => stopTask(id));
|
|
246
|
+
ipcMain.handle("window:minimize", () => mainWindow?.minimize());
|
|
247
|
+
ipcMain.handle("window:maximize", () => mainWindow?.isMaximized() ? mainWindow.unmaximize() : mainWindow?.maximize());
|
|
248
|
+
ipcMain.handle("window:close", () => mainWindow?.close());
|
|
249
|
+
setInterval(() => void getMetrics().then((metrics) => emit("metrics:update", metrics)), 2000);
|
|
250
|
+
createWindow();
|
|
251
|
+
app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); });
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
app.on("window-all-closed", () => { if (process.platform !== "darwin") app.quit(); });
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { contextBridge, ipcRenderer } from "electron";
|
|
2
|
+
import type { StellaDesktopApi } from "./shared/contracts";
|
|
3
|
+
|
|
4
|
+
const api: StellaDesktopApi = {
|
|
5
|
+
selectWorkspace: () => ipcRenderer.invoke("workspace:select"),
|
|
6
|
+
getProjectInfo: (workspace) => ipcRenderer.invoke("project:get", workspace),
|
|
7
|
+
getSettings: () => ipcRenderer.invoke("settings:get"),
|
|
8
|
+
setModel: (model) => ipcRenderer.invoke("settings:model", model),
|
|
9
|
+
hasApiKey: () => ipcRenderer.invoke("key:has"),
|
|
10
|
+
saveApiKey: (key) => ipcRenderer.invoke("key:save", key),
|
|
11
|
+
deleteApiKey: () => ipcRenderer.invoke("key:delete"),
|
|
12
|
+
getMetrics: () => ipcRenderer.invoke("metrics:get"),
|
|
13
|
+
getTasks: () => ipcRenderer.invoke("tasks:list"),
|
|
14
|
+
startTask: (input) => ipcRenderer.invoke("task:start", input),
|
|
15
|
+
stopTask: (taskId) => ipcRenderer.invoke("task:stop", taskId),
|
|
16
|
+
onMetrics: (listener) => {
|
|
17
|
+
const wrapped = (_event: Electron.IpcRendererEvent, payload: Parameters<typeof listener>[0]) => listener(payload);
|
|
18
|
+
ipcRenderer.on("metrics:update", wrapped);
|
|
19
|
+
return () => ipcRenderer.removeListener("metrics:update", wrapped);
|
|
20
|
+
},
|
|
21
|
+
onAgentOutput: (listener) => {
|
|
22
|
+
const wrapped = (_event: Electron.IpcRendererEvent, payload: Parameters<typeof listener>[0]) => listener(payload);
|
|
23
|
+
ipcRenderer.on("agent:output", wrapped);
|
|
24
|
+
return () => ipcRenderer.removeListener("agent:output", wrapped);
|
|
25
|
+
},
|
|
26
|
+
onAgentStatus: (listener) => {
|
|
27
|
+
const wrapped = (_event: Electron.IpcRendererEvent, payload: Parameters<typeof listener>[0]) => listener(payload);
|
|
28
|
+
ipcRenderer.on("agent:status", wrapped);
|
|
29
|
+
return () => ipcRenderer.removeListener("agent:status", wrapped);
|
|
30
|
+
},
|
|
31
|
+
minimizeWindow: () => ipcRenderer.invoke("window:minimize"),
|
|
32
|
+
toggleMaximizeWindow: () => ipcRenderer.invoke("window:maximize"),
|
|
33
|
+
closeWindow: () => ipcRenderer.invoke("window:close"),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
contextBridge.exposeInMainWorld("stella", api);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="ru">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Stella Coder</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export type TaskStatus = "running" | "completed" | "failed" | "cancelled";
|
|
2
|
+
|
|
3
|
+
export interface ModelOption {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const MODELS: ModelOption[] = [
|
|
9
|
+
{ id: "mimo-v2.5-free", label: "MiMo V2.5 — бесплатно" },
|
|
10
|
+
{ id: "deepseek-v4-flash-free", label: "DeepSeek V4 Flash — бесплатно" },
|
|
11
|
+
{ id: "gpt-5.4", label: "GPT-5.4" },
|
|
12
|
+
{ id: "gpt-5.4-mini", label: "GPT-5.4 Mini" },
|
|
13
|
+
{ id: "gpt-5.2-codex", label: "GPT-5.2 Codex" },
|
|
14
|
+
{ id: "gpt-5-codex", label: "GPT-5 Codex" },
|
|
15
|
+
{ id: "claude-sonnet-4.5", label: "Claude Sonnet 4.5" },
|
|
16
|
+
{ id: "claude-opus-4.5", label: "Claude Opus 4.5" },
|
|
17
|
+
{ id: "gemini-3-flash", label: "Gemini 3 Flash" },
|
|
18
|
+
{ id: "deepseek-v4-pro", label: "DeepSeek V4 Pro" },
|
|
19
|
+
{ id: "glm-5.2", label: "GLM 5.2" },
|
|
20
|
+
{ id: "kimi-k2.6", label: "Kimi K2.6" },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export interface DesktopSettings {
|
|
24
|
+
model: string;
|
|
25
|
+
recentWorkspaces: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ProjectInfo {
|
|
29
|
+
path: string;
|
|
30
|
+
name: string;
|
|
31
|
+
branch: string | null;
|
|
32
|
+
changedFiles: string[];
|
|
33
|
+
isGitRepository: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SystemMetrics {
|
|
37
|
+
cpuPercent: number | null;
|
|
38
|
+
memoryUsedGb: number;
|
|
39
|
+
memoryTotalGb: number;
|
|
40
|
+
diskUsedGb: number | null;
|
|
41
|
+
diskTotalGb: number | null;
|
|
42
|
+
gpuName: string | null;
|
|
43
|
+
gpuPercent: number | null;
|
|
44
|
+
gpuTemperature: number | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface TaskRecord {
|
|
48
|
+
id: string;
|
|
49
|
+
prompt: string;
|
|
50
|
+
workspace: string;
|
|
51
|
+
model: string;
|
|
52
|
+
status: TaskStatus;
|
|
53
|
+
startedAt: string;
|
|
54
|
+
finishedAt?: string;
|
|
55
|
+
durationMs?: number;
|
|
56
|
+
output: string;
|
|
57
|
+
changedFiles: string[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface AgentOutputEvent {
|
|
61
|
+
taskId: string;
|
|
62
|
+
channel: "stdout" | "stderr";
|
|
63
|
+
text: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface AgentStatusEvent {
|
|
67
|
+
task: TaskRecord;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface StellaDesktopApi {
|
|
71
|
+
selectWorkspace(): Promise<ProjectInfo | null>;
|
|
72
|
+
getProjectInfo(workspace: string): Promise<ProjectInfo>;
|
|
73
|
+
getSettings(): Promise<DesktopSettings>;
|
|
74
|
+
setModel(model: string): Promise<DesktopSettings>;
|
|
75
|
+
hasApiKey(): Promise<boolean>;
|
|
76
|
+
saveApiKey(key: string): Promise<{ ok: boolean; error?: string }>;
|
|
77
|
+
deleteApiKey(): Promise<{ ok: boolean; error?: string }>;
|
|
78
|
+
getMetrics(): Promise<SystemMetrics>;
|
|
79
|
+
getTasks(): Promise<TaskRecord[]>;
|
|
80
|
+
startTask(input: { workspace: string; prompt: string; model: string }): Promise<TaskRecord>;
|
|
81
|
+
stopTask(taskId: string): Promise<void>;
|
|
82
|
+
onMetrics(listener: (metrics: SystemMetrics) => void): () => void;
|
|
83
|
+
onAgentOutput(listener: (event: AgentOutputEvent) => void): () => void;
|
|
84
|
+
onAgentStatus(listener: (event: AgentStatusEvent) => void): () => void;
|
|
85
|
+
minimizeWindow(): Promise<void>;
|
|
86
|
+
toggleMaximizeWindow(): Promise<void>;
|
|
87
|
+
closeWindow(): Promise<void>;
|
|
88
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"moduleResolution": "Node",
|
|
6
|
+
"rootDir": ".",
|
|
7
|
+
"outDir": "dist/main",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"types": ["node", "electron"]
|
|
12
|
+
},
|
|
13
|
+
"include": ["main.ts", "preload.ts", "shared/**/*.ts"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import react from "@vitejs/plugin-react";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [react()],
|
|
7
|
+
root: path.resolve(__dirname, "renderer"),
|
|
8
|
+
base: "./",
|
|
9
|
+
build: {
|
|
10
|
+
outDir: path.resolve(__dirname, "dist/renderer"),
|
|
11
|
+
emptyOutDir: false,
|
|
12
|
+
},
|
|
13
|
+
server: {
|
|
14
|
+
port: 5173,
|
|
15
|
+
strictPort: true,
|
|
16
|
+
},
|
|
17
|
+
});
|
package/install.ps1
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
$nd = "$env:USERPROFILE\StellaNode"
|
|
2
|
+
$np = "$nd\node-v22.14.0-win-x64"
|
|
3
|
+
|
|
4
|
+
Write-Host " [1/3] Downloading Node.js..."
|
|
5
|
+
if (Test-Path "$np\node.exe") {
|
|
6
|
+
Write-Host " [OK] Already have Node.js"
|
|
7
|
+
} else {
|
|
8
|
+
Invoke-WebRequest -Uri "https://nodejs.org/dist/v22.14.0/node-v22.14.0-win-x64.zip" -OutFile "$env:TEMP\node.zip" -UseBasicParsing
|
|
9
|
+
if (-not (Test-Path "$env:TEMP\node.zip")) {
|
|
10
|
+
Write-Host " [FAIL] Download failed"
|
|
11
|
+
exit 1
|
|
12
|
+
}
|
|
13
|
+
if (-not (Test-Path $nd)) {
|
|
14
|
+
New-Item -ItemType Directory -Path $nd -Force | Out-Null
|
|
15
|
+
}
|
|
16
|
+
Expand-Archive -Path "$env:TEMP\node.zip" -DestinationPath $nd -Force
|
|
17
|
+
Remove-Item "$env:TEMP\node.zip" -Force
|
|
18
|
+
if (-not (Test-Path "$np\node.exe")) {
|
|
19
|
+
Write-Host " [FAIL] Extract failed"
|
|
20
|
+
exit 1
|
|
21
|
+
}
|
|
22
|
+
Write-Host " [OK] Node.js installed"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
Write-Host " [2/3] Installing stella-coder..."
|
|
26
|
+
$npm = "$np\node_modules\npm\bin\npm-cli.js"
|
|
27
|
+
& "$np\node.exe" "$npm" install -g stella-coder@latest
|
|
28
|
+
if ($LASTEXITCODE -ne 0) {
|
|
29
|
+
Write-Host " [FAIL] npm install failed"
|
|
30
|
+
exit 1
|
|
31
|
+
}
|
|
32
|
+
Write-Host " [OK] stella-coder installed"
|
|
33
|
+
|
|
34
|
+
Write-Host " [3/3] Fixing stella wrapper..."
|
|
35
|
+
Remove-Item "$np\stella.ps1" -Force -ErrorAction SilentlyContinue
|
|
36
|
+
|
|
37
|
+
Write-Host ""
|
|
38
|
+
Write-Host " DONE!"
|
|
39
|
+
Write-Host " Run: & `"$np\stella.cmd`""
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stella-coder",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Stella Coder 5.0 — AI coding agent with Telegram bot, huge context, TDD, Git ecosystem, presentations, computer control, smart home, Office automation, and antivirus",
|
|
6
6
|
"main": "stella-cli/index.mjs",
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
"stellar-av": "node antimalware/index.mjs",
|
|
13
13
|
"dev": "next dev",
|
|
14
14
|
"build": "next build",
|
|
15
|
+
"desktop:dev": "concurrently -k -n renderer,main,electron \"vite --config desktop/vite.config.ts\" \"tsc --watch -p desktop/tsconfig.main.json\" \"wait-on tcp:5173 file:desktop/dist/main/main.js && cross-env ELECTRON_RENDERER_URL=http://localhost:5173 electron desktop/dist/main/main.js\"",
|
|
16
|
+
"desktop:build": "tsc -p desktop/tsconfig.main.json && vite build --config desktop/vite.config.ts",
|
|
17
|
+
"desktop:dist": "pnpm desktop:build && electron-builder --win nsis",
|
|
15
18
|
"build:cli": "node stella-cli/build.mjs",
|
|
16
19
|
"start": "next start",
|
|
17
20
|
"lint": "eslint ."
|
|
@@ -48,16 +51,54 @@
|
|
|
48
51
|
"zod": "^4.4.3"
|
|
49
52
|
},
|
|
50
53
|
"devDependencies": {
|
|
54
|
+
"@vitejs/plugin-react": "^6.0.3",
|
|
51
55
|
"@eslint/eslintrc": "^3.3.0",
|
|
52
56
|
"@tailwindcss/postcss": "^4.2.0",
|
|
53
57
|
"@types/node": "^24",
|
|
54
58
|
"@types/react": "^19",
|
|
55
59
|
"@types/react-dom": "^19",
|
|
60
|
+
"concurrently": "^10.0.3",
|
|
61
|
+
"cross-env": "^10.1.0",
|
|
62
|
+
"electron": "^43.1.0",
|
|
63
|
+
"electron-builder": "^26.15.3",
|
|
56
64
|
"eslint": "^9.28.0",
|
|
57
65
|
"eslint-config-next": "^16.2.0",
|
|
58
66
|
"postcss": "^8.5",
|
|
59
67
|
"tailwindcss": "^4.2.0",
|
|
60
|
-
"typescript": "5.7.3"
|
|
68
|
+
"typescript": "5.7.3",
|
|
69
|
+
"vite": "^8.1.4",
|
|
70
|
+
"wait-on": "^9.0.10"
|
|
71
|
+
},
|
|
72
|
+
"build": {
|
|
73
|
+
"appId": "com.stella.coder",
|
|
74
|
+
"productName": "Stella Coder",
|
|
75
|
+
"directories": {
|
|
76
|
+
"output": "release-desktop"
|
|
77
|
+
},
|
|
78
|
+
"extraMetadata": {
|
|
79
|
+
"main": "desktop/dist/main/main.js"
|
|
80
|
+
},
|
|
81
|
+
"files": [
|
|
82
|
+
"desktop/dist/**/*",
|
|
83
|
+
"stella-cli/**/*",
|
|
84
|
+
"antimalware/**/*",
|
|
85
|
+
"package.json"
|
|
86
|
+
],
|
|
87
|
+
"asar": true,
|
|
88
|
+
"asarUnpack": [
|
|
89
|
+
"stella-cli/**/*",
|
|
90
|
+
"antimalware/**/*"
|
|
91
|
+
],
|
|
92
|
+
"win": {
|
|
93
|
+
"target": "nsis",
|
|
94
|
+
"artifactName": "Stella-Coder-Setup-${version}.${ext}"
|
|
95
|
+
},
|
|
96
|
+
"nsis": {
|
|
97
|
+
"oneClick": false,
|
|
98
|
+
"allowToChangeInstallationDirectory": true,
|
|
99
|
+
"createDesktopShortcut": true,
|
|
100
|
+
"createStartMenuShortcut": true
|
|
101
|
+
}
|
|
61
102
|
},
|
|
62
103
|
"pnpm": {
|
|
63
104
|
"overrides": {
|
package/pnpm-workspace.yaml
CHANGED
package/stella-cli/index.mjs
CHANGED
|
@@ -52,7 +52,7 @@ import {
|
|
|
52
52
|
generateAdminCode, listAuthorizedUsers,
|
|
53
53
|
} from "./telegram-bot.mjs"
|
|
54
54
|
|
|
55
|
-
const VERSION = "5.3.
|
|
55
|
+
const VERSION = "5.3.6"
|
|
56
56
|
const CONFIG_DIR = path.join(os.homedir(), ".stella")
|
|
57
57
|
const CONFIG_PATH = path.join(CONFIG_DIR, "config.json")
|
|
58
58
|
const HISTORY_PATH = path.join(CONFIG_DIR, "history.json")
|
|
@@ -369,6 +369,41 @@ async function runTurn(userText) {
|
|
|
369
369
|
}
|
|
370
370
|
console.log("\n")
|
|
371
371
|
} else {
|
|
372
|
+
// Try streaming first, fallback to generateText if no text in 10s
|
|
373
|
+
let gotText = false
|
|
374
|
+
const fallbackTimer = setTimeout(async () => {
|
|
375
|
+
if (!gotText) {
|
|
376
|
+
stopSpinner()
|
|
377
|
+
console.log(dim(" ⏳ Streaming stuck, using generateText fallback..."))
|
|
378
|
+
try {
|
|
379
|
+
const res = await generateText({
|
|
380
|
+
model: getModel(state.model),
|
|
381
|
+
system: systemPrompt(),
|
|
382
|
+
messages: state.messages,
|
|
383
|
+
tools,
|
|
384
|
+
maxSteps: 30,
|
|
385
|
+
abortSignal: controller.signal,
|
|
386
|
+
})
|
|
387
|
+
if (res.text) {
|
|
388
|
+
process.stdout.write("\n" + violet("⏺ ") + res.text + "\n")
|
|
389
|
+
state.messages.push({ role: "assistant", content: res.text })
|
|
390
|
+
for (const toolMsg of res.response.messages) {
|
|
391
|
+
if (toolMsg.role === "assistant") state.messages.push(toolMsg)
|
|
392
|
+
}
|
|
393
|
+
const usage = res.usage
|
|
394
|
+
const inTok = usage.promptTokens ?? 0
|
|
395
|
+
const outTok = usage.completionTokens ?? 0
|
|
396
|
+
state.totalTokens.input += inTok
|
|
397
|
+
state.totalTokens.output += outTok
|
|
398
|
+
const dur = ((Date.now() - t0) / 1000).toFixed(1)
|
|
399
|
+
console.log(dim(" ") + darkGray(`⏱ ${dur}s · ↑${inTok} ↓${outTok} ток · ${blue(state.model)}`))
|
|
400
|
+
}
|
|
401
|
+
} catch (e2) {
|
|
402
|
+
console.log("\n" + red("✗ Ошибка fallback: ") + String(e2?.message || e2).slice(0, 300))
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}, 10000)
|
|
406
|
+
|
|
372
407
|
result = streamText({
|
|
373
408
|
model: getModel(state.model),
|
|
374
409
|
system: systemPrompt(),
|
|
@@ -391,6 +426,8 @@ async function runTurn(userText) {
|
|
|
391
426
|
switch (part.type) {
|
|
392
427
|
case "text-delta": {
|
|
393
428
|
stopSpinner()
|
|
429
|
+
gotText = true
|
|
430
|
+
clearTimeout(fallbackTimer)
|
|
394
431
|
if (firstText) {
|
|
395
432
|
process.stdout.write("\n" + violet("⏺ ") )
|
|
396
433
|
firstText = false
|
|
@@ -430,6 +467,7 @@ async function runTurn(userText) {
|
|
|
430
467
|
}
|
|
431
468
|
case "finish": {
|
|
432
469
|
stopSpinner()
|
|
470
|
+
clearTimeout(fallbackTimer)
|
|
433
471
|
renderer.flush()
|
|
434
472
|
break
|
|
435
473
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import http from "http"
|
|
2
2
|
import https from "https"
|
|
3
|
-
|
|
3
|
+
let JSDOM
|
|
4
|
+
try { const m = await import("jsdom"); JSDOM = m.JSDOM } catch { JSDOM = null }
|
|
5
|
+
|
|
6
|
+
function requireJsdom() {
|
|
7
|
+
if (!JSDOM) throw new Error("jsdom не установлен. Установи: npm install jsdom")
|
|
8
|
+
}
|
|
4
9
|
|
|
5
10
|
function fetchUrl(url, options = {}) {
|
|
6
11
|
return new Promise((resolve, reject) => {
|
|
@@ -44,6 +49,7 @@ export class WebParser {
|
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
const result = await fetchUrl(url)
|
|
52
|
+
requireJsdom()
|
|
47
53
|
const dom = new JSDOM(result.html)
|
|
48
54
|
const doc = dom.window.document
|
|
49
55
|
|
|
@@ -100,6 +106,7 @@ export class WebParser {
|
|
|
100
106
|
|
|
101
107
|
async extractForms(url) {
|
|
102
108
|
const page = await this.fetchPage(url)
|
|
109
|
+
requireJsdom()
|
|
103
110
|
const dom = new JSDOM(page.html)
|
|
104
111
|
const doc = dom.window.document
|
|
105
112
|
|
|
@@ -126,6 +133,7 @@ export class WebParser {
|
|
|
126
133
|
if (engine === "duckduckgo") {
|
|
127
134
|
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`
|
|
128
135
|
const page = await this.fetchPage(url)
|
|
136
|
+
requireJsdom()
|
|
129
137
|
const dom = new JSDOM(page.html)
|
|
130
138
|
const doc = dom.window.document
|
|
131
139
|
|
|
@@ -141,6 +149,7 @@ export class WebParser {
|
|
|
141
149
|
if (engine === "google") {
|
|
142
150
|
const url = `https://www.google.com/search?q=${encodeURIComponent(query)}`
|
|
143
151
|
const page = await this.fetchPage(url)
|
|
152
|
+
requireJsdom()
|
|
144
153
|
const dom = new JSDOM(page.html)
|
|
145
154
|
const doc = dom.window.document
|
|
146
155
|
|
|
@@ -158,6 +167,7 @@ export class WebParser {
|
|
|
158
167
|
|
|
159
168
|
async getSEO(url) {
|
|
160
169
|
const page = await this.fetchPage(url)
|
|
170
|
+
requireJsdom()
|
|
161
171
|
const dom = new JSDOM(page.html)
|
|
162
172
|
const doc = dom.window.document
|
|
163
173
|
|
package/test-stream.mjs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { streamText } from "ai";
|
|
2
|
+
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
3
|
+
|
|
4
|
+
const z = createOpenAICompatible({
|
|
5
|
+
name: "z",
|
|
6
|
+
baseURL: "https://opencode.ai/zen/v1",
|
|
7
|
+
apiKey: "sk-U6RlsOyn7qmvEsYEQ0c4syqtCpff0GQXd2z8Wr9m5Px2tpxrtU7GiUvTRM4arfw8",
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const m = z.chatModel("mimo-v2.5-free");
|
|
11
|
+
console.log("Starting...");
|
|
12
|
+
|
|
13
|
+
const r = streamText({
|
|
14
|
+
model: m,
|
|
15
|
+
messages: [{ role: "user", content: "say hi" }],
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
console.log("Waiting for stream...");
|
|
19
|
+
|
|
20
|
+
for await (const p of r.fullStream) {
|
|
21
|
+
if (p.type === "text-delta") process.stdout.write(p.text);
|
|
22
|
+
}
|
|
23
|
+
console.log("\nDONE");
|