@opencoreai/opencore 0.2.2
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/LICENSE +62 -0
- package/README.md +205 -0
- package/bin/opencore.mjs +343 -0
- package/opencore dashboard/app.js +923 -0
- package/opencore dashboard/index.html +15 -0
- package/opencore dashboard/styles.css +965 -0
- package/package.json +46 -0
- package/scripts/postinstall.mjs +448 -0
- package/src/credential-store.mjs +209 -0
- package/src/dashboard-server.ts +403 -0
- package/src/index.ts +2523 -0
- package/src/mac-controller.mjs +614 -0
- package/src/opencore-indicator.js +140 -0
- package/src/skill-catalog.mjs +305 -0
- package/templates/default-guidelines.md +142 -0
- package/templates/default-heartbeat.md +20 -0
- package/templates/default-instructions.md +72 -0
- package/templates/default-memory.md +7 -0
- package/templates/default-soul.md +130 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import http from "node:http";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import type net from "node:net";
|
|
6
|
+
import { WebSocketServer } from "ws";
|
|
7
|
+
import { SKILL_CATALOG } from "./skill-catalog.mjs";
|
|
8
|
+
import {
|
|
9
|
+
deleteCredentialEntry,
|
|
10
|
+
readCredentialStore,
|
|
11
|
+
updateCredentialDefaults,
|
|
12
|
+
upsertCredentialEntry,
|
|
13
|
+
} from "./credential-store.mjs";
|
|
14
|
+
|
|
15
|
+
export type ChatRole = "user" | "assistant" | "system" | "error";
|
|
16
|
+
|
|
17
|
+
export type ChatEntry = {
|
|
18
|
+
id: string;
|
|
19
|
+
role: ChatRole;
|
|
20
|
+
source: "terminal" | "dashboard" | "manager" | "telegram" | "system";
|
|
21
|
+
text: string;
|
|
22
|
+
ts: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type DashboardServerOptions = {
|
|
26
|
+
rootDir: string;
|
|
27
|
+
openCoreHome: string;
|
|
28
|
+
screenshotDir: string;
|
|
29
|
+
onDashboardPrompt: (text: string) => void;
|
|
30
|
+
getTelegramConfig: () => Promise<Record<string, any>>;
|
|
31
|
+
setTelegramConfig: (next: Record<string, any>) => Promise<Record<string, any>>;
|
|
32
|
+
disconnectTelegram: () => Promise<Record<string, any>>;
|
|
33
|
+
onCredentialsUpdated?: (payload: Record<string, any>) => Promise<void> | void;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function nowIso() {
|
|
37
|
+
return new Date().toISOString();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function id() {
|
|
41
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class DashboardServer {
|
|
45
|
+
private readonly options: DashboardServerOptions;
|
|
46
|
+
private readonly app = express();
|
|
47
|
+
private readonly server: http.Server;
|
|
48
|
+
private readonly wss: WebSocketServer;
|
|
49
|
+
private readonly dashboardDir: string;
|
|
50
|
+
private readonly skillsDir: string;
|
|
51
|
+
private readonly chatLogPath: string;
|
|
52
|
+
private readonly vendorMap: Record<string, string>;
|
|
53
|
+
private readonly filesMap: Record<string, string>;
|
|
54
|
+
private readonly sockets = new Set<net.Socket>();
|
|
55
|
+
private chatHistory: ChatEntry[] = [];
|
|
56
|
+
private started = false;
|
|
57
|
+
|
|
58
|
+
constructor(options: DashboardServerOptions) {
|
|
59
|
+
this.options = options;
|
|
60
|
+
this.dashboardDir = path.join(options.rootDir, "opencore dashboard");
|
|
61
|
+
this.skillsDir = path.join(options.openCoreHome, "skills");
|
|
62
|
+
this.chatLogPath = path.join(options.openCoreHome, "chat-history.json");
|
|
63
|
+
this.vendorMap = {
|
|
64
|
+
"react.production.min.js": path.join(options.rootDir, "node_modules", "react", "umd", "react.production.min.js"),
|
|
65
|
+
"react-dom.production.min.js": path.join(
|
|
66
|
+
options.rootDir,
|
|
67
|
+
"node_modules",
|
|
68
|
+
"react-dom",
|
|
69
|
+
"umd",
|
|
70
|
+
"react-dom.production.min.js",
|
|
71
|
+
),
|
|
72
|
+
};
|
|
73
|
+
this.filesMap = {
|
|
74
|
+
soul: path.join(options.openCoreHome, "soul.md"),
|
|
75
|
+
memory: path.join(options.openCoreHome, "memory.md"),
|
|
76
|
+
heartbeat: path.join(options.openCoreHome, "heartbeat.md"),
|
|
77
|
+
guidelines: path.join(options.openCoreHome, "guidelines.md"),
|
|
78
|
+
instructions: path.join(options.openCoreHome, "instructions.md"),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
this.app.use(express.json({ limit: "5mb" }));
|
|
82
|
+
this.configureRoutes();
|
|
83
|
+
this.server = http.createServer(this.app);
|
|
84
|
+
this.server.on("connection", (socket) => {
|
|
85
|
+
this.sockets.add(socket);
|
|
86
|
+
socket.on("close", () => this.sockets.delete(socket));
|
|
87
|
+
});
|
|
88
|
+
this.wss = new WebSocketServer({ server: this.server, path: "/ws" });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private async loadChatHistory() {
|
|
92
|
+
try {
|
|
93
|
+
const raw = await fs.readFile(this.chatLogPath, "utf8");
|
|
94
|
+
const parsed = JSON.parse(raw || "[]");
|
|
95
|
+
this.chatHistory = Array.isArray(parsed) ? parsed.slice(-2000) : [];
|
|
96
|
+
} catch {
|
|
97
|
+
this.chatHistory = [];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private async persistChatHistory() {
|
|
102
|
+
await fs.writeFile(this.chatLogPath, `${JSON.stringify(this.chatHistory, null, 2)}\n`, "utf8");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private async listScreenshots() {
|
|
106
|
+
try {
|
|
107
|
+
const files = await fs.readdir(this.options.screenshotDir);
|
|
108
|
+
const items = await Promise.all(
|
|
109
|
+
files
|
|
110
|
+
.filter((name) => name.toLowerCase().endsWith(".png"))
|
|
111
|
+
.map(async (name) => {
|
|
112
|
+
const abs = path.join(this.options.screenshotDir, name);
|
|
113
|
+
const stat = await fs.stat(abs);
|
|
114
|
+
return { name, path: abs, ts: stat.mtime.toISOString(), size: stat.size };
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
return items.sort((a, b) => (a.ts < b.ts ? 1 : -1));
|
|
118
|
+
} catch {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private wsBroadcast(payload: unknown) {
|
|
124
|
+
const data = JSON.stringify(payload);
|
|
125
|
+
for (const client of this.wss.clients) {
|
|
126
|
+
if (client.readyState === 1) {
|
|
127
|
+
client.send(data);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private async listInstalledSkillIds() {
|
|
133
|
+
const installed = new Set<string>();
|
|
134
|
+
try {
|
|
135
|
+
const entries = await fs.readdir(this.skillsDir, { withFileTypes: true });
|
|
136
|
+
for (const entry of entries) {
|
|
137
|
+
if (!entry.isDirectory()) continue;
|
|
138
|
+
const id = entry.name;
|
|
139
|
+
const skillFile = path.join(this.skillsDir, id, "SKILL.md");
|
|
140
|
+
try {
|
|
141
|
+
await fs.access(skillFile);
|
|
142
|
+
installed.add(id);
|
|
143
|
+
} catch {}
|
|
144
|
+
}
|
|
145
|
+
} catch {}
|
|
146
|
+
return installed;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private async installSkill(id: string) {
|
|
150
|
+
const skill = SKILL_CATALOG.find((item) => item.id === id);
|
|
151
|
+
if (!skill) {
|
|
152
|
+
return { ok: false, error: "Unknown skill." };
|
|
153
|
+
}
|
|
154
|
+
const skillDir = path.join(this.skillsDir, skill.id);
|
|
155
|
+
const skillFile = path.join(skillDir, "SKILL.md");
|
|
156
|
+
const configFile = path.join(skillDir, "config.json");
|
|
157
|
+
await fs.mkdir(skillDir, { recursive: true });
|
|
158
|
+
await fs.writeFile(skillFile, `${skill.markdown.trim()}\n`, "utf8");
|
|
159
|
+
await fs.writeFile(
|
|
160
|
+
configFile,
|
|
161
|
+
`${JSON.stringify(
|
|
162
|
+
{
|
|
163
|
+
id: skill.id,
|
|
164
|
+
name: skill.name,
|
|
165
|
+
description: skill.description,
|
|
166
|
+
...skill.config,
|
|
167
|
+
},
|
|
168
|
+
null,
|
|
169
|
+
2,
|
|
170
|
+
)}\n`,
|
|
171
|
+
"utf8",
|
|
172
|
+
);
|
|
173
|
+
return { ok: true };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private configureRoutes() {
|
|
177
|
+
this.app.get("/api/health", (_req, res) => {
|
|
178
|
+
res.json({ ok: true, service: "opencore-dashboard" });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
this.app.get("/api/chat", async (_req, res) => {
|
|
182
|
+
res.json({ messages: this.chatHistory.slice(-500) });
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
this.app.post("/api/chat", async (req, res) => {
|
|
186
|
+
const text = String(req.body?.text || "").trim();
|
|
187
|
+
if (!text) return res.status(400).json({ error: "Message text is required." });
|
|
188
|
+
this.options.onDashboardPrompt(text);
|
|
189
|
+
res.json({ ok: true });
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
this.app.get("/api/files/:name", async (req, res) => {
|
|
193
|
+
const key = String(req.params.name || "");
|
|
194
|
+
const filePath = this.filesMap[key];
|
|
195
|
+
if (!filePath) return res.status(404).json({ error: "Unknown file." });
|
|
196
|
+
try {
|
|
197
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
198
|
+
res.json({ name: key, path: filePath, content });
|
|
199
|
+
} catch (error) {
|
|
200
|
+
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
this.app.put("/api/files/:name", async (req, res) => {
|
|
205
|
+
const key = String(req.params.name || "");
|
|
206
|
+
const filePath = this.filesMap[key];
|
|
207
|
+
if (!filePath) return res.status(404).json({ error: "Unknown file." });
|
|
208
|
+
const content = String(req.body?.content ?? "");
|
|
209
|
+
try {
|
|
210
|
+
await fs.writeFile(filePath, content, "utf8");
|
|
211
|
+
this.wsBroadcast({ type: "file_saved", name: key, ts: nowIso() });
|
|
212
|
+
res.json({ ok: true });
|
|
213
|
+
} catch (error) {
|
|
214
|
+
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
this.app.get("/api/screenshots", async (_req, res) => {
|
|
219
|
+
res.json({ items: await this.listScreenshots() });
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
this.app.get("/api/screenshots/file", async (req, res) => {
|
|
223
|
+
const filePath = String(req.query.path || "");
|
|
224
|
+
if (!filePath.startsWith(this.options.screenshotDir)) {
|
|
225
|
+
return res.status(403).json({ error: "Forbidden screenshot path." });
|
|
226
|
+
}
|
|
227
|
+
res.sendFile(filePath, (err) => {
|
|
228
|
+
if (err) res.status(404).end();
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
this.app.get("/api/skills", async (_req, res) => {
|
|
233
|
+
const installed = await this.listInstalledSkillIds();
|
|
234
|
+
res.json({
|
|
235
|
+
items: SKILL_CATALOG.map((skill) => ({
|
|
236
|
+
id: skill.id,
|
|
237
|
+
name: skill.name,
|
|
238
|
+
description: skill.description,
|
|
239
|
+
installed: installed.has(skill.id),
|
|
240
|
+
})),
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
this.app.post("/api/skills/install", async (req, res) => {
|
|
245
|
+
const id = String(req.body?.id || "").trim();
|
|
246
|
+
if (!id) return res.status(400).json({ error: "Skill id is required." });
|
|
247
|
+
try {
|
|
248
|
+
const result = await this.installSkill(id);
|
|
249
|
+
if (!result.ok) return res.status(404).json({ error: result.error || "Unknown skill." });
|
|
250
|
+
this.wsBroadcast({ type: "skill_installed", id, ts: nowIso() });
|
|
251
|
+
res.json({ ok: true, id });
|
|
252
|
+
} catch (error) {
|
|
253
|
+
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
this.app.get("/api/telegram", async (_req, res) => {
|
|
258
|
+
try {
|
|
259
|
+
const config = await this.options.getTelegramConfig();
|
|
260
|
+
res.json(config);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
this.app.put("/api/telegram", async (req, res) => {
|
|
267
|
+
try {
|
|
268
|
+
const config = await this.options.setTelegramConfig(req.body || {});
|
|
269
|
+
this.wsBroadcast({ type: "telegram_config_updated", payload: config, ts: nowIso() });
|
|
270
|
+
res.json(config);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
this.app.post("/api/telegram/disconnect", async (_req, res) => {
|
|
277
|
+
try {
|
|
278
|
+
const config = await this.options.disconnectTelegram();
|
|
279
|
+
this.wsBroadcast({ type: "telegram_config_updated", payload: config, ts: nowIso() });
|
|
280
|
+
res.json(config);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
this.app.get("/api/credentials", async (_req, res) => {
|
|
287
|
+
try {
|
|
288
|
+
const store = await readCredentialStore();
|
|
289
|
+
res.json(store);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
this.app.put("/api/credentials/defaults", async (req, res) => {
|
|
296
|
+
try {
|
|
297
|
+
const store = await updateCredentialDefaults(req.body || {});
|
|
298
|
+
await this.options.onCredentialsUpdated?.(store);
|
|
299
|
+
this.wsBroadcast({ type: "credentials_updated", payload: store, ts: nowIso() });
|
|
300
|
+
res.json(store);
|
|
301
|
+
} catch (error) {
|
|
302
|
+
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
this.app.post("/api/credentials/upsert", async (req, res) => {
|
|
307
|
+
try {
|
|
308
|
+
const store = await upsertCredentialEntry(req.body || {});
|
|
309
|
+
await this.options.onCredentialsUpdated?.(store);
|
|
310
|
+
this.wsBroadcast({ type: "credentials_updated", payload: store, ts: nowIso() });
|
|
311
|
+
res.json(store);
|
|
312
|
+
} catch (error) {
|
|
313
|
+
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
this.app.post("/api/credentials/delete", async (req, res) => {
|
|
318
|
+
try {
|
|
319
|
+
const id = String(req.body?.id || req.body?.website || "").trim();
|
|
320
|
+
if (!id) return res.status(400).json({ error: "Credential id or website is required." });
|
|
321
|
+
const store = await deleteCredentialEntry(id);
|
|
322
|
+
await this.options.onCredentialsUpdated?.(store);
|
|
323
|
+
this.wsBroadcast({ type: "credentials_updated", payload: store, ts: nowIso() });
|
|
324
|
+
res.json(store);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
this.app.use("/dashboard", express.static(this.dashboardDir));
|
|
331
|
+
|
|
332
|
+
this.app.get("/vendor/:file", (req, res) => {
|
|
333
|
+
const file = String(req.params.file || "");
|
|
334
|
+
const resolved = this.vendorMap[file];
|
|
335
|
+
if (!resolved) return res.status(404).end();
|
|
336
|
+
res.sendFile(resolved, (err) => {
|
|
337
|
+
if (err) res.status(404).end();
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
this.app.get("/", (_req, res) => {
|
|
342
|
+
res.redirect("/dashboard/");
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async start(port = 4111) {
|
|
347
|
+
if (this.started) return;
|
|
348
|
+
await fs.mkdir(this.options.screenshotDir, { recursive: true });
|
|
349
|
+
await fs.mkdir(path.dirname(this.chatLogPath), { recursive: true });
|
|
350
|
+
await this.loadChatHistory();
|
|
351
|
+
await new Promise<void>((resolve, reject) => {
|
|
352
|
+
this.server.once("error", reject);
|
|
353
|
+
this.server.listen(port, "127.0.0.1", () => resolve());
|
|
354
|
+
});
|
|
355
|
+
this.started = true;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async stop(timeoutMs = 1200) {
|
|
359
|
+
if (!this.started) return;
|
|
360
|
+
for (const client of this.wss.clients) {
|
|
361
|
+
try {
|
|
362
|
+
client.terminate();
|
|
363
|
+
} catch {}
|
|
364
|
+
}
|
|
365
|
+
await new Promise<void>((resolve) => {
|
|
366
|
+
let done = false;
|
|
367
|
+
const finish = () => {
|
|
368
|
+
if (done) return;
|
|
369
|
+
done = true;
|
|
370
|
+
resolve();
|
|
371
|
+
};
|
|
372
|
+
const timer = setTimeout(() => {
|
|
373
|
+
for (const socket of this.sockets) {
|
|
374
|
+
try {
|
|
375
|
+
socket.destroy();
|
|
376
|
+
} catch {}
|
|
377
|
+
}
|
|
378
|
+
finish();
|
|
379
|
+
}, Math.max(200, Number(timeoutMs) || 1200));
|
|
380
|
+
|
|
381
|
+
this.wss.close(() => {});
|
|
382
|
+
this.server.close(() => {
|
|
383
|
+
clearTimeout(timer);
|
|
384
|
+
finish();
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
this.started = false;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async publishChat(entry: Omit<ChatEntry, "id" | "ts">) {
|
|
391
|
+
const full: ChatEntry = { ...entry, id: id(), ts: nowIso() };
|
|
392
|
+
this.chatHistory.push(full);
|
|
393
|
+
if (this.chatHistory.length > 2000) {
|
|
394
|
+
this.chatHistory = this.chatHistory.slice(this.chatHistory.length - 2000);
|
|
395
|
+
}
|
|
396
|
+
await this.persistChatHistory();
|
|
397
|
+
this.wsBroadcast({ type: "chat_message", payload: full });
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
publishEvent(payload: unknown) {
|
|
401
|
+
this.wsBroadcast({ type: "event", payload, ts: nowIso() });
|
|
402
|
+
}
|
|
403
|
+
}
|