stella-coder 5.3.3 → 5.3.5
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/stella-cli/index.mjs +49 -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.5",
|
|
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/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.5"
|
|
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")
|
|
@@ -299,12 +299,21 @@ async function runTurn(userText) {
|
|
|
299
299
|
console.log(red("\n ✗ API ключ не задан. Введи /login для настройки.\n"))
|
|
300
300
|
return
|
|
301
301
|
}
|
|
302
|
+
console.log(dim(` → модель: ${state.model}, ключ: ${apiKey ? "есть" : "нет"}`))
|
|
302
303
|
state.messages.push({ role: "user", content: userText })
|
|
303
304
|
state.turns++
|
|
304
305
|
state.interrupted = false
|
|
305
306
|
|
|
306
307
|
const isOllama = state.model.startsWith("ollama:")
|
|
307
308
|
const controller = new AbortController()
|
|
309
|
+
|
|
310
|
+
// 30 second timeout
|
|
311
|
+
const timeout = setTimeout(() => {
|
|
312
|
+
controller.abort()
|
|
313
|
+
stopSpinner()
|
|
314
|
+
console.log("\n" + red("✗ Таймаут: API не отвечает 30 секунд. Проверь модель: /model"))
|
|
315
|
+
}, 30000)
|
|
316
|
+
|
|
308
317
|
const onSigint = () => {
|
|
309
318
|
state.interrupted = true
|
|
310
319
|
controller.abort()
|
|
@@ -360,6 +369,41 @@ async function runTurn(userText) {
|
|
|
360
369
|
}
|
|
361
370
|
console.log("\n")
|
|
362
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
|
+
|
|
363
407
|
result = streamText({
|
|
364
408
|
model: getModel(state.model),
|
|
365
409
|
system: systemPrompt(),
|
|
@@ -382,6 +426,8 @@ async function runTurn(userText) {
|
|
|
382
426
|
switch (part.type) {
|
|
383
427
|
case "text-delta": {
|
|
384
428
|
stopSpinner()
|
|
429
|
+
gotText = true
|
|
430
|
+
clearTimeout(fallbackTimer)
|
|
385
431
|
if (firstText) {
|
|
386
432
|
process.stdout.write("\n" + violet("⏺ ") )
|
|
387
433
|
firstText = false
|
|
@@ -421,6 +467,7 @@ async function runTurn(userText) {
|
|
|
421
467
|
}
|
|
422
468
|
case "finish": {
|
|
423
469
|
stopSpinner()
|
|
470
|
+
clearTimeout(fallbackTimer)
|
|
424
471
|
renderer.flush()
|
|
425
472
|
break
|
|
426
473
|
}
|
|
@@ -455,6 +502,7 @@ async function runTurn(userText) {
|
|
|
455
502
|
}
|
|
456
503
|
}
|
|
457
504
|
} finally {
|
|
505
|
+
clearTimeout(timeout)
|
|
458
506
|
process.removeListener("SIGINT", onSigint)
|
|
459
507
|
}
|
|
460
508
|
console.log()
|
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");
|