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.
@@ -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.4",
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": {
@@ -1,4 +1,5 @@
1
1
  allowBuilds:
2
+ electron-winstaller: set this to true or false
2
3
  sharp: true
3
4
  unrs-resolver: true
4
5
  minimumReleaseAgeExclude:
@@ -52,7 +52,7 @@ import {
52
52
  generateAdminCode, listAuthorizedUsers,
53
53
  } from "./telegram-bot.mjs"
54
54
 
55
- const VERSION = "5.3.4"
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
- import { JSDOM } from "jsdom"
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
 
@@ -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");