@opencoreai/opencore 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/opencore dashboard/app.js +74 -31
- package/opencore dashboard/styles.css +71 -32
- package/package.json +2 -2
- package/scripts/postinstall.mjs +7 -0
- package/src/credential-store.mjs +11 -0
- package/src/dashboard-server.ts +48 -3
- package/src/index.ts +382 -28
- package/src/opencore-indicator.m +493 -0
- package/templates/default-computer-profile.md +7 -0
- package/templates/default-guidelines.md +4 -0
- package/templates/default-instructions.md +2 -0
- package/src/opencore-indicator.js +0 -140
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
const PAGES = [
|
|
6
6
|
{ id: "chat", label: "Mission Chat", eyebrow: "Control", blurb: "Live conversation with OpenCore and real-time task flow." },
|
|
7
|
+
{ id: "schedules", label: "Scheduled Tasks", eyebrow: "Automation", blurb: "Cron-backed tasks that are still pending, active, missed, or need attention." },
|
|
7
8
|
{ id: "credentials", label: "Credentials", eyebrow: "Identity Access", blurb: "Store website logins, default email settings, and email-activation policy locally." },
|
|
8
9
|
{ id: "telegram", label: "Telegram", eyebrow: "Remote Control", blurb: "Connect Telegram, update bot settings, and monitor link status." },
|
|
9
10
|
{ id: "skills", label: "Skills", eyebrow: "Capability", blurb: "Installed and available OpenCore skills." },
|
|
10
11
|
{ id: "soul", label: "soul.md", eyebrow: "Identity", blurb: "Core identity, posture, and persona memory." },
|
|
11
12
|
{ id: "memory", label: "memory.md", eyebrow: "Recall", blurb: "Durable machine facts, lessons, and task history." },
|
|
13
|
+
{ id: "computer-profile", label: "computer-profile.md", eyebrow: "Machine State", blurb: "Hardware, browser, workspace, app inventory, and other durable computer facts." },
|
|
12
14
|
{ id: "heartbeat", label: "heartbeat.md", eyebrow: "Execution", blurb: "Current plan plus scheduled-task tracking." },
|
|
13
15
|
{ id: "guidelines", label: "guidelines.md", eyebrow: "Guardrails", blurb: "Persistent safety and permission boundaries." },
|
|
14
16
|
{ id: "instructions", label: "instructions.md", eyebrow: "Operating Mode", blurb: "Workflow preferences and execution style." },
|
|
@@ -47,10 +49,10 @@
|
|
|
47
49
|
const [fileContent, setFileContent] = useState("");
|
|
48
50
|
const [fileBusy, setFileBusy] = useState(false);
|
|
49
51
|
const [shots, setShots] = useState([]);
|
|
52
|
+
const [schedules, setSchedules] = useState([]);
|
|
50
53
|
const [skills, setSkills] = useState([]);
|
|
51
54
|
const [skillBusy, setSkillBusy] = useState({});
|
|
52
55
|
const [expandedSkills, setExpandedSkills] = useState({});
|
|
53
|
-
const [drawerOpen, setDrawerOpen] = useState(true);
|
|
54
56
|
const [liveEvent, setLiveEvent] = useState("Waiting for activity");
|
|
55
57
|
const [theme, setTheme] = useState(initialTheme === "dark" ? "dark" : "light");
|
|
56
58
|
const [telegramConfig, setTelegramConfig] = useState({
|
|
@@ -99,6 +101,12 @@
|
|
|
99
101
|
setShots(json.items || []);
|
|
100
102
|
}
|
|
101
103
|
|
|
104
|
+
async function loadSchedules() {
|
|
105
|
+
const res = await fetch("/api/schedules");
|
|
106
|
+
const json = await res.json();
|
|
107
|
+
setSchedules(Array.isArray(json.items) ? json.items : []);
|
|
108
|
+
}
|
|
109
|
+
|
|
102
110
|
async function loadSkills() {
|
|
103
111
|
const res = await fetch("/api/skills");
|
|
104
112
|
const json = await res.json();
|
|
@@ -140,7 +148,7 @@
|
|
|
140
148
|
}, [theme]);
|
|
141
149
|
|
|
142
150
|
useEffect(() => {
|
|
143
|
-
Promise.all([loadChat(), loadShots(), loadSkills(), loadTelegram(), loadCredentials()])
|
|
151
|
+
Promise.all([loadChat(), loadShots(), loadSchedules(), loadSkills(), loadTelegram(), loadCredentials()])
|
|
144
152
|
.then(() => setStatus("Online"))
|
|
145
153
|
.catch(() => setStatus("Connection issue"));
|
|
146
154
|
|
|
@@ -159,12 +167,18 @@
|
|
|
159
167
|
loadShots();
|
|
160
168
|
} else if (evt.type === "task_started") {
|
|
161
169
|
setLiveEvent("Task running");
|
|
170
|
+
loadSchedules();
|
|
162
171
|
} else if (evt.type === "task_completed") {
|
|
163
172
|
setLiveEvent("Task completed");
|
|
173
|
+
loadSchedules();
|
|
164
174
|
} else if (evt.type === "task_failed") {
|
|
165
175
|
setLiveEvent("Task failed");
|
|
176
|
+
loadSchedules();
|
|
166
177
|
} else if (evt.type === "file_updated") {
|
|
167
178
|
setLiveEvent(`Updated ${String(evt.target || "").split("/").pop() || "file"}`);
|
|
179
|
+
if (String(evt.target || "").includes("schedules.json") || String(evt.target || "").includes("heartbeat.md")) {
|
|
180
|
+
loadSchedules();
|
|
181
|
+
}
|
|
168
182
|
} else if (evt.type === "action") {
|
|
169
183
|
setLiveEvent(`Action · ${evt.action || "working"}`);
|
|
170
184
|
}
|
|
@@ -204,12 +218,15 @@
|
|
|
204
218
|
}, []);
|
|
205
219
|
|
|
206
220
|
useEffect(() => {
|
|
207
|
-
if (["soul", "memory", "guidelines", "instructions", "heartbeat"].includes(page)) {
|
|
221
|
+
if (["soul", "memory", "computer-profile", "guidelines", "instructions", "heartbeat"].includes(page)) {
|
|
208
222
|
loadFile(page);
|
|
209
223
|
}
|
|
210
224
|
if (page === "screenshots") {
|
|
211
225
|
loadShots();
|
|
212
226
|
}
|
|
227
|
+
if (page === "schedules") {
|
|
228
|
+
loadSchedules();
|
|
229
|
+
}
|
|
213
230
|
if (page === "skills") {
|
|
214
231
|
loadSkills();
|
|
215
232
|
}
|
|
@@ -429,19 +446,21 @@
|
|
|
429
446
|
const currentPage = PAGES.find((p) => p.id === page) || PAGES[0];
|
|
430
447
|
const installedCount = skills.filter((s) => s.installed).length;
|
|
431
448
|
const latestChat = chat.length ? chat[chat.length - 1] : null;
|
|
449
|
+
const pendingScheduleCount = schedules.length;
|
|
432
450
|
|
|
433
451
|
const stats = useMemo(
|
|
434
452
|
() => [
|
|
435
453
|
{ label: "Connection", value: status, tone: status === "Online" ? "good" : "warn" },
|
|
454
|
+
{ label: "Pending Schedules", value: String(pendingScheduleCount), tone: pendingScheduleCount ? "warn" : "good" },
|
|
436
455
|
{ label: "Installed Skills", value: String(installedCount), tone: "accent" },
|
|
437
456
|
{ label: "Screenshots", value: String(shots.length), tone: "neutral" },
|
|
438
457
|
{ label: "Live State", value: liveEvent, tone: "neutral wide" },
|
|
439
458
|
],
|
|
440
|
-
[installedCount, liveEvent, shots.length, status],
|
|
459
|
+
[installedCount, liveEvent, pendingScheduleCount, shots.length, status],
|
|
441
460
|
);
|
|
442
461
|
|
|
443
462
|
function renderChatPage() {
|
|
444
|
-
return e("div", { className: "content-body stacked" }, [
|
|
463
|
+
return e("div", { className: "content-body stacked scroll-pane credentials-page" }, [
|
|
445
464
|
e("div", { className: "section-head", key: "head" }, [
|
|
446
465
|
e("div", { className: "section-title", key: "title" }, "Conversation Stream"),
|
|
447
466
|
e("div", { className: "section-meta", key: "meta" }, latestChat ? `Last message · ${fmtTs(latestChat.ts)}` : "No messages yet"),
|
|
@@ -458,6 +477,13 @@
|
|
|
458
477
|
e("span", { key: "t" }, fmtTs(m.ts)),
|
|
459
478
|
]),
|
|
460
479
|
e("div", { className: "msg-text", key: "t" }, m.text),
|
|
480
|
+
m.screenshot_path
|
|
481
|
+
? e("img", {
|
|
482
|
+
className: "chat-shot",
|
|
483
|
+
key: "shot",
|
|
484
|
+
src: `/api/screenshots/file?path=${encodeURIComponent(m.screenshot_path)}`,
|
|
485
|
+
})
|
|
486
|
+
: null,
|
|
461
487
|
]),
|
|
462
488
|
]),
|
|
463
489
|
),
|
|
@@ -503,10 +529,47 @@
|
|
|
503
529
|
);
|
|
504
530
|
}
|
|
505
531
|
|
|
532
|
+
function renderSchedulesPage() {
|
|
533
|
+
return e(
|
|
534
|
+
"div",
|
|
535
|
+
{ className: "schedules-grid content-body scroll-pane" },
|
|
536
|
+
schedules.length
|
|
537
|
+
? schedules.map((item) =>
|
|
538
|
+
e("article", { className: "schedule-card", key: item.id }, [
|
|
539
|
+
e("div", { className: "skill-top", key: "top" }, [
|
|
540
|
+
e("div", { key: "title" }, [
|
|
541
|
+
e("div", { className: "skill-kicker", key: "k" }, item.schedule_kind || "scheduled"),
|
|
542
|
+
e("h3", { key: "h3" }, item.summary || item.id),
|
|
543
|
+
]),
|
|
544
|
+
e(
|
|
545
|
+
"span",
|
|
546
|
+
{ className: `skill-status ${["error", "missed"].includes(String(item.last_status || "").toLowerCase()) ? "not-installed" : "installed"}`, key: "status" },
|
|
547
|
+
item.last_status || "scheduled",
|
|
548
|
+
),
|
|
549
|
+
]),
|
|
550
|
+
e("p", { key: "task" }, item.task || "No task description"),
|
|
551
|
+
e("div", { className: "schedule-meta", key: "meta" }, [
|
|
552
|
+
e("div", { key: "id" }, `ID: ${item.id}`),
|
|
553
|
+
e("div", { key: "cron" }, `Cron: ${item.cron_expression || "n/a"}`),
|
|
554
|
+
e("div", { key: "expected" }, `Expected: ${item.expected_time_iso ? fmtTs(item.expected_time_iso) : "n/a"}`),
|
|
555
|
+
e("div", { key: "last" }, `Last run: ${item.last_run_at ? fmtTs(item.last_run_at) : "n/a"}`),
|
|
556
|
+
]),
|
|
557
|
+
item.last_error ? e("div", { className: "skill-note", key: "error" }, `Last error: ${item.last_error}`) : null,
|
|
558
|
+
]),
|
|
559
|
+
)
|
|
560
|
+
: [
|
|
561
|
+
e("div", { className: "empty-state", key: "empty" }, [
|
|
562
|
+
e("div", { className: "section-title", key: "title" }, "No pending scheduled tasks"),
|
|
563
|
+
e("div", { className: "section-meta", key: "meta" }, "Anything active, missed, running, or errored will appear here."),
|
|
564
|
+
]),
|
|
565
|
+
],
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
|
|
506
569
|
function renderSkillsPage() {
|
|
507
570
|
return e(
|
|
508
571
|
"div",
|
|
509
|
-
{ className: "skills-grid content-body" },
|
|
572
|
+
{ className: "skills-grid content-body scroll-pane" },
|
|
510
573
|
skills.map((s) => {
|
|
511
574
|
const expanded = Boolean(expandedSkills[s.id]);
|
|
512
575
|
const titleLong = String(s.name || "").length > 26;
|
|
@@ -554,7 +617,7 @@
|
|
|
554
617
|
}
|
|
555
618
|
|
|
556
619
|
function renderTelegramPage() {
|
|
557
|
-
return e("div", { className: "content-body stacked" }, [
|
|
620
|
+
return e("div", { className: "content-body stacked scroll-pane" }, [
|
|
558
621
|
e("div", { className: "section-head", key: "head" }, [
|
|
559
622
|
e("div", { className: "section-title", key: "title" }, "Telegram Settings"),
|
|
560
623
|
e(
|
|
@@ -822,6 +885,7 @@
|
|
|
822
885
|
|
|
823
886
|
function renderPage() {
|
|
824
887
|
if (page === "chat") return renderChatPage();
|
|
888
|
+
if (page === "schedules") return renderSchedulesPage();
|
|
825
889
|
if (page === "credentials") return renderCredentialsPage();
|
|
826
890
|
if (page === "telegram") return renderTelegramPage();
|
|
827
891
|
if (page === "screenshots") return renderScreenshotsPage();
|
|
@@ -857,23 +921,13 @@
|
|
|
857
921
|
{ className: "stats-row", key: "stats" },
|
|
858
922
|
stats.map((item, idx) => e(StatCard, { key: `${item.label}-${idx}`, label: item.label, value: item.value, tone: item.tone })),
|
|
859
923
|
),
|
|
860
|
-
e("div", { className:
|
|
861
|
-
e("aside", { className:
|
|
924
|
+
e("div", { className: "layout", key: "layout" }, [
|
|
925
|
+
e("aside", { className: "drawer open", key: "drawer" }, [
|
|
862
926
|
e("div", { className: "drawer-head", key: "dh" }, [
|
|
863
927
|
e("div", { className: "drawer-head-main", key: "main" }, [
|
|
864
928
|
e("div", { className: "drawer-kicker", key: "dk" }, "Workspace"),
|
|
865
929
|
e("div", { className: "drawer-title", key: "dt" }, "Navigation"),
|
|
866
930
|
]),
|
|
867
|
-
e(
|
|
868
|
-
"button",
|
|
869
|
-
{
|
|
870
|
-
className: "drawer-inline-toggle",
|
|
871
|
-
key: "tg",
|
|
872
|
-
onClick: () => setDrawerOpen((v) => !v),
|
|
873
|
-
"aria-label": drawerOpen ? "Close sidebar" : "Open sidebar",
|
|
874
|
-
},
|
|
875
|
-
"◂",
|
|
876
|
-
),
|
|
877
931
|
]),
|
|
878
932
|
e(
|
|
879
933
|
"div",
|
|
@@ -894,18 +948,7 @@
|
|
|
894
948
|
),
|
|
895
949
|
),
|
|
896
950
|
]),
|
|
897
|
-
|
|
898
|
-
e(
|
|
899
|
-
"button",
|
|
900
|
-
{
|
|
901
|
-
className: "drawer-reopen",
|
|
902
|
-
key: "reopen",
|
|
903
|
-
onClick: () => setDrawerOpen(true),
|
|
904
|
-
"aria-label": "Open sidebar",
|
|
905
|
-
},
|
|
906
|
-
"▸",
|
|
907
|
-
),
|
|
908
|
-
e("main", { className: "page", key: "page" }, [
|
|
951
|
+
e("main", { className: `page ${page === "credentials" ? "page-scrollable" : ""}`.trim(), key: "page" }, [
|
|
909
952
|
e("div", { className: "page-head", key: "ph" }, [
|
|
910
953
|
e("div", { key: "copy" }, [
|
|
911
954
|
e("div", { className: "page-kicker", key: "pk" }, currentPage.eyebrow),
|
|
@@ -244,10 +244,6 @@ body {
|
|
|
244
244
|
min-height: 0;
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
-
.layout.drawer-closed {
|
|
248
|
-
grid-template-columns: minmax(0, 1fr);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
247
|
.drawer {
|
|
252
248
|
background: transparent;
|
|
253
249
|
border: 0;
|
|
@@ -263,10 +259,6 @@ body {
|
|
|
263
259
|
flex-direction: column;
|
|
264
260
|
}
|
|
265
261
|
|
|
266
|
-
.drawer.closed {
|
|
267
|
-
display: none;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
262
|
.drawer-head {
|
|
271
263
|
padding: 4px 10px 12px 12px;
|
|
272
264
|
display: flex;
|
|
@@ -279,30 +271,6 @@ body {
|
|
|
279
271
|
min-width: 0;
|
|
280
272
|
}
|
|
281
273
|
|
|
282
|
-
.drawer-inline-toggle,
|
|
283
|
-
.drawer-reopen {
|
|
284
|
-
width: 34px;
|
|
285
|
-
height: 34px;
|
|
286
|
-
border: 1px solid rgba(129, 73, 33, 0.14);
|
|
287
|
-
border-radius: 999px;
|
|
288
|
-
background: rgba(255, 247, 237, 0.7);
|
|
289
|
-
color: var(--accent);
|
|
290
|
-
font-size: 16px;
|
|
291
|
-
font-weight: 800;
|
|
292
|
-
cursor: pointer;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
:root[data-theme="dark"] .drawer-inline-toggle,
|
|
296
|
-
:root[data-theme="dark"] .drawer-reopen {
|
|
297
|
-
background: rgba(36, 26, 21, 0.86);
|
|
298
|
-
border-color: rgba(255, 176, 118, 0.16);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
.drawer-reopen {
|
|
302
|
-
align-self: flex-start;
|
|
303
|
-
margin: 12px 8px 0 0;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
274
|
.drawer-title {
|
|
307
275
|
font-size: 22px;
|
|
308
276
|
font-weight: 800;
|
|
@@ -384,6 +352,11 @@ body {
|
|
|
384
352
|
backdrop-filter: none;
|
|
385
353
|
}
|
|
386
354
|
|
|
355
|
+
.page.page-scrollable {
|
|
356
|
+
overflow-y: auto;
|
|
357
|
+
overscroll-behavior: contain;
|
|
358
|
+
}
|
|
359
|
+
|
|
387
360
|
.page-head {
|
|
388
361
|
padding: 14px 20px 12px;
|
|
389
362
|
border-bottom: 1px solid rgba(101, 67, 37, 0.1);
|
|
@@ -409,6 +382,27 @@ body {
|
|
|
409
382
|
overflow: hidden;
|
|
410
383
|
}
|
|
411
384
|
|
|
385
|
+
.scroll-pane {
|
|
386
|
+
overflow-y: auto;
|
|
387
|
+
min-height: 0;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.credentials-page .credentials-grid {
|
|
391
|
+
overflow: visible !important;
|
|
392
|
+
overflow-y: visible !important;
|
|
393
|
+
max-height: none;
|
|
394
|
+
grid-auto-rows: auto;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.page.page-scrollable .content-body.credentials-page {
|
|
398
|
+
overflow: visible;
|
|
399
|
+
flex: 0 0 auto;
|
|
400
|
+
min-height: auto;
|
|
401
|
+
height: auto;
|
|
402
|
+
max-height: none;
|
|
403
|
+
padding-bottom: 18px;
|
|
404
|
+
}
|
|
405
|
+
|
|
412
406
|
.stacked {
|
|
413
407
|
display: flex;
|
|
414
408
|
flex-direction: column;
|
|
@@ -526,6 +520,22 @@ body {
|
|
|
526
520
|
line-height: 1.5;
|
|
527
521
|
}
|
|
528
522
|
|
|
523
|
+
.chat-shot {
|
|
524
|
+
display: block;
|
|
525
|
+
width: 100%;
|
|
526
|
+
max-width: 560px;
|
|
527
|
+
margin-top: 12px;
|
|
528
|
+
border-radius: 16px;
|
|
529
|
+
border: 1px solid rgba(112, 69, 35, 0.14);
|
|
530
|
+
background: rgba(255, 255, 255, 0.4);
|
|
531
|
+
object-fit: contain;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
:root[data-theme="dark"] .chat-shot {
|
|
535
|
+
border-color: rgba(255, 189, 140, 0.18);
|
|
536
|
+
background: rgba(18, 12, 10, 0.65);
|
|
537
|
+
}
|
|
538
|
+
|
|
529
539
|
.composer-shell {
|
|
530
540
|
padding: 12px 20px 18px;
|
|
531
541
|
border-top: 1px solid rgba(101, 67, 37, 0.1);
|
|
@@ -639,6 +649,7 @@ button {
|
|
|
639
649
|
|
|
640
650
|
.shots-grid,
|
|
641
651
|
.skills-grid,
|
|
652
|
+
.schedules-grid,
|
|
642
653
|
.credentials-grid {
|
|
643
654
|
padding: 14px 20px 18px;
|
|
644
655
|
overflow-y: auto;
|
|
@@ -651,6 +662,7 @@ button {
|
|
|
651
662
|
|
|
652
663
|
.shot-card,
|
|
653
664
|
.skill-card,
|
|
665
|
+
.schedule-card,
|
|
654
666
|
.credential-card,
|
|
655
667
|
.settings-card {
|
|
656
668
|
border-radius: 0;
|
|
@@ -674,12 +686,27 @@ button {
|
|
|
674
686
|
overflow: hidden;
|
|
675
687
|
}
|
|
676
688
|
|
|
689
|
+
.schedule-card {
|
|
690
|
+
border: 1px solid rgba(101, 68, 38, 0.12);
|
|
691
|
+
border-radius: 18px;
|
|
692
|
+
background: rgba(255, 250, 244, 0.58);
|
|
693
|
+
box-shadow: 0 10px 24px rgba(95, 58, 31, 0.06);
|
|
694
|
+
padding: 12px 14px;
|
|
695
|
+
align-self: start;
|
|
696
|
+
}
|
|
697
|
+
|
|
677
698
|
:root[data-theme="dark"] .skill-card {
|
|
678
699
|
border-color: rgba(255, 188, 140, 0.12);
|
|
679
700
|
background: rgba(34, 25, 20, 0.78);
|
|
680
701
|
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.22);
|
|
681
702
|
}
|
|
682
703
|
|
|
704
|
+
:root[data-theme="dark"] .schedule-card {
|
|
705
|
+
border-color: rgba(255, 188, 140, 0.12);
|
|
706
|
+
background: rgba(34, 25, 20, 0.78);
|
|
707
|
+
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.22);
|
|
708
|
+
}
|
|
709
|
+
|
|
683
710
|
.credential-card {
|
|
684
711
|
border: 1px solid rgba(101, 68, 38, 0.12);
|
|
685
712
|
border-radius: 18px;
|
|
@@ -793,6 +820,18 @@ button {
|
|
|
793
820
|
align-items: flex-start;
|
|
794
821
|
}
|
|
795
822
|
|
|
823
|
+
.schedule-meta {
|
|
824
|
+
display: grid;
|
|
825
|
+
gap: 6px;
|
|
826
|
+
margin-top: 8px;
|
|
827
|
+
color: var(--ink-soft);
|
|
828
|
+
font-size: 13px;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
.empty-state {
|
|
832
|
+
padding: 16px 0;
|
|
833
|
+
}
|
|
834
|
+
|
|
796
835
|
.shot-name,
|
|
797
836
|
.skill-card h3 {
|
|
798
837
|
margin: 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opencoreai/opencore",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "LicenseRef-OpenCore-Personal-Use-1.0",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"src/dashboard-server.ts",
|
|
15
15
|
"src/index.ts",
|
|
16
16
|
"src/mac-controller.mjs",
|
|
17
|
-
"src/opencore-indicator.
|
|
17
|
+
"src/opencore-indicator.m",
|
|
18
18
|
"src/skill-catalog.mjs",
|
|
19
19
|
"scripts",
|
|
20
20
|
"templates",
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { stdin as input, stdout as output } from "node:process";
|
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { SKILL_CATALOG } from "../src/skill-catalog.mjs";
|
|
11
11
|
import { ensureCredentialStore } from "../src/credential-store.mjs";
|
|
12
|
+
process.umask(0o077);
|
|
12
13
|
|
|
13
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
15
|
const __dirname = path.dirname(__filename);
|
|
@@ -20,6 +21,7 @@ const GUIDELINES_PATH = path.join(OPENCORE_HOME, "guidelines.md");
|
|
|
20
21
|
const INSTRUCTIONS_PATH = path.join(OPENCORE_HOME, "instructions.md");
|
|
21
22
|
const HEARTBEAT_PATH = path.join(OPENCORE_HOME, "heartbeat.md");
|
|
22
23
|
const SOUL_PATH = path.join(OPENCORE_HOME, "soul.md");
|
|
24
|
+
const COMPUTER_PROFILE_PATH = path.join(OPENCORE_HOME, "computer-profile.md");
|
|
23
25
|
const PROFILE_SECTION_START = "<!-- OPENCORE_INSTALL_PROFILE_START -->";
|
|
24
26
|
const PROFILE_SECTION_END = "<!-- OPENCORE_INSTALL_PROFILE_END -->";
|
|
25
27
|
const DIRECTORIES = [
|
|
@@ -386,6 +388,10 @@ async function ensureTemplateApplied(filePath, template, legacyDefaults = []) {
|
|
|
386
388
|
async function run() {
|
|
387
389
|
const defaultSoul = await readTemplate("default-soul.md", "# OpenCore Soul\nName: OpenCore\n");
|
|
388
390
|
const defaultMemory = await readTemplate("default-memory.md", LEGACY_DEFAULT_MEMORY);
|
|
391
|
+
const defaultComputerProfile = await readTemplate(
|
|
392
|
+
"default-computer-profile.md",
|
|
393
|
+
"# OpenCore Computer Profile\n\n## Machine Snapshot\n- Pending first machine scan.\n\n## Learned Facts\n- None yet.\n",
|
|
394
|
+
);
|
|
389
395
|
const defaultGuidelines = await readTemplate("default-guidelines.md", "# OpenCore Guidelines\n");
|
|
390
396
|
const defaultInstructions = await readTemplate("default-instructions.md", "# OpenCore Instructions\n");
|
|
391
397
|
const defaultHeartbeat = await readTemplate("default-heartbeat.md", "# OpenCore Heartbeat\n");
|
|
@@ -396,6 +402,7 @@ async function run() {
|
|
|
396
402
|
|
|
397
403
|
await ensureTemplateApplied(path.join(OPENCORE_HOME, "soul.md"), defaultSoul, ["# OpenCore Soul\nName: OpenCore\n"]);
|
|
398
404
|
await ensureTemplateApplied(path.join(OPENCORE_HOME, "memory.md"), defaultMemory, [LEGACY_DEFAULT_MEMORY]);
|
|
405
|
+
await ensureTemplateApplied(COMPUTER_PROFILE_PATH, defaultComputerProfile, [defaultComputerProfile]);
|
|
399
406
|
await ensureTemplateApplied(HEARTBEAT_PATH, defaultHeartbeat, ["# OpenCore Heartbeat\n"]);
|
|
400
407
|
await ensureTemplateApplied(GUIDELINES_PATH, defaultGuidelines, ["# OpenCore Guidelines\n"]);
|
|
401
408
|
await ensureTemplateApplied(INSTRUCTIONS_PATH, defaultInstructions, ["# OpenCore Instructions\n"]);
|
package/src/credential-store.mjs
CHANGED
|
@@ -5,6 +5,15 @@ import { promises as fs } from "node:fs";
|
|
|
5
5
|
const OPENCORE_HOME = path.join(os.homedir(), ".opencore");
|
|
6
6
|
const CREDENTIALS_PATH = path.join(OPENCORE_HOME, "configs", "credentials.json");
|
|
7
7
|
|
|
8
|
+
async function tightenCredentialStorePermissions() {
|
|
9
|
+
try {
|
|
10
|
+
await fs.chmod(path.dirname(CREDENTIALS_PATH), 0o700);
|
|
11
|
+
} catch {}
|
|
12
|
+
try {
|
|
13
|
+
await fs.chmod(CREDENTIALS_PATH, 0o600);
|
|
14
|
+
} catch {}
|
|
15
|
+
}
|
|
16
|
+
|
|
8
17
|
export function defaultCredentialsStore() {
|
|
9
18
|
return {
|
|
10
19
|
default_email: "",
|
|
@@ -26,6 +35,7 @@ export async function ensureCredentialStore() {
|
|
|
26
35
|
} catch {
|
|
27
36
|
await fs.writeFile(CREDENTIALS_PATH, `${JSON.stringify(defaultCredentialsStore(), null, 2)}\n`, "utf8");
|
|
28
37
|
}
|
|
38
|
+
await tightenCredentialStorePermissions();
|
|
29
39
|
}
|
|
30
40
|
|
|
31
41
|
export async function readCredentialStore() {
|
|
@@ -42,6 +52,7 @@ export async function readCredentialStore() {
|
|
|
42
52
|
export async function writeCredentialStore(next) {
|
|
43
53
|
const normalized = normalizeCredentialStore(next);
|
|
44
54
|
await fs.writeFile(CREDENTIALS_PATH, `${JSON.stringify(normalized, null, 2)}\n`, "utf8");
|
|
55
|
+
await tightenCredentialStorePermissions();
|
|
45
56
|
return normalized;
|
|
46
57
|
}
|
|
47
58
|
|
package/src/dashboard-server.ts
CHANGED
|
@@ -20,6 +20,7 @@ export type ChatEntry = {
|
|
|
20
20
|
source: "terminal" | "dashboard" | "manager" | "telegram" | "system";
|
|
21
21
|
text: string;
|
|
22
22
|
ts: string;
|
|
23
|
+
screenshot_path?: string;
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
type DashboardServerOptions = {
|
|
@@ -48,6 +49,7 @@ export class DashboardServer {
|
|
|
48
49
|
private readonly wss: WebSocketServer;
|
|
49
50
|
private readonly dashboardDir: string;
|
|
50
51
|
private readonly skillsDir: string;
|
|
52
|
+
private readonly schedulesPath: string;
|
|
51
53
|
private readonly chatLogPath: string;
|
|
52
54
|
private readonly vendorMap: Record<string, string>;
|
|
53
55
|
private readonly filesMap: Record<string, string>;
|
|
@@ -57,8 +59,10 @@ export class DashboardServer {
|
|
|
57
59
|
|
|
58
60
|
constructor(options: DashboardServerOptions) {
|
|
59
61
|
this.options = options;
|
|
62
|
+
this.app.disable("x-powered-by");
|
|
60
63
|
this.dashboardDir = path.join(options.rootDir, "opencore dashboard");
|
|
61
64
|
this.skillsDir = path.join(options.openCoreHome, "skills");
|
|
65
|
+
this.schedulesPath = path.join(options.openCoreHome, "configs", "schedules.json");
|
|
62
66
|
this.chatLogPath = path.join(options.openCoreHome, "chat-history.json");
|
|
63
67
|
this.vendorMap = {
|
|
64
68
|
"react.production.min.js": path.join(options.rootDir, "node_modules", "react", "umd", "react.production.min.js"),
|
|
@@ -73,11 +77,25 @@ export class DashboardServer {
|
|
|
73
77
|
this.filesMap = {
|
|
74
78
|
soul: path.join(options.openCoreHome, "soul.md"),
|
|
75
79
|
memory: path.join(options.openCoreHome, "memory.md"),
|
|
80
|
+
"computer-profile": path.join(options.openCoreHome, "computer-profile.md"),
|
|
76
81
|
heartbeat: path.join(options.openCoreHome, "heartbeat.md"),
|
|
77
82
|
guidelines: path.join(options.openCoreHome, "guidelines.md"),
|
|
78
83
|
instructions: path.join(options.openCoreHome, "instructions.md"),
|
|
79
84
|
};
|
|
80
85
|
|
|
86
|
+
this.app.use((req, res, next) => {
|
|
87
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
88
|
+
res.setHeader("X-Frame-Options", "DENY");
|
|
89
|
+
res.setHeader("Referrer-Policy", "no-referrer");
|
|
90
|
+
res.setHeader(
|
|
91
|
+
"Content-Security-Policy",
|
|
92
|
+
"default-src 'self'; connect-src 'self' ws: wss:; img-src 'self' data: blob:; style-src 'self'; script-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'",
|
|
93
|
+
);
|
|
94
|
+
if (req.path.startsWith("/api/")) {
|
|
95
|
+
res.setHeader("Cache-Control", "no-store");
|
|
96
|
+
}
|
|
97
|
+
next();
|
|
98
|
+
});
|
|
81
99
|
this.app.use(express.json({ limit: "5mb" }));
|
|
82
100
|
this.configureRoutes();
|
|
83
101
|
this.server = http.createServer(this.app);
|
|
@@ -107,7 +125,7 @@ export class DashboardServer {
|
|
|
107
125
|
const files = await fs.readdir(this.options.screenshotDir);
|
|
108
126
|
const items = await Promise.all(
|
|
109
127
|
files
|
|
110
|
-
.filter((name) =>
|
|
128
|
+
.filter((name) => /\.(png|jpg|jpeg)$/i.test(name))
|
|
111
129
|
.map(async (name) => {
|
|
112
130
|
const abs = path.join(this.options.screenshotDir, name);
|
|
113
131
|
const stat = await fs.stat(abs);
|
|
@@ -173,6 +191,24 @@ export class DashboardServer {
|
|
|
173
191
|
return { ok: true };
|
|
174
192
|
}
|
|
175
193
|
|
|
194
|
+
private async listPendingSchedules() {
|
|
195
|
+
try {
|
|
196
|
+
const raw = await fs.readFile(this.schedulesPath, "utf8");
|
|
197
|
+
const parsed = JSON.parse(raw || "[]");
|
|
198
|
+
const items = Array.isArray(parsed) ? parsed : [];
|
|
199
|
+
return items
|
|
200
|
+
.filter((item: any) => {
|
|
201
|
+
const status = String(item?.last_status || "scheduled").trim().toLowerCase();
|
|
202
|
+
const active = Boolean(item?.active);
|
|
203
|
+
if (active) return true;
|
|
204
|
+
return ["scheduled", "running", "error", "missed"].includes(status);
|
|
205
|
+
})
|
|
206
|
+
.sort((a: any, b: any) => String(b?.updated_at || "").localeCompare(String(a?.updated_at || "")));
|
|
207
|
+
} catch {
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
176
212
|
private configureRoutes() {
|
|
177
213
|
this.app.get("/api/health", (_req, res) => {
|
|
178
214
|
res.json({ ok: true, service: "opencore-dashboard" });
|
|
@@ -220,8 +256,9 @@ export class DashboardServer {
|
|
|
220
256
|
});
|
|
221
257
|
|
|
222
258
|
this.app.get("/api/screenshots/file", async (req, res) => {
|
|
223
|
-
const
|
|
224
|
-
|
|
259
|
+
const screenshotRoot = path.resolve(this.options.screenshotDir);
|
|
260
|
+
const filePath = path.resolve(String(req.query.path || ""));
|
|
261
|
+
if (!(filePath === screenshotRoot || filePath.startsWith(`${screenshotRoot}${path.sep}`))) {
|
|
225
262
|
return res.status(403).json({ error: "Forbidden screenshot path." });
|
|
226
263
|
}
|
|
227
264
|
res.sendFile(filePath, (err) => {
|
|
@@ -241,6 +278,14 @@ export class DashboardServer {
|
|
|
241
278
|
});
|
|
242
279
|
});
|
|
243
280
|
|
|
281
|
+
this.app.get("/api/schedules", async (_req, res) => {
|
|
282
|
+
try {
|
|
283
|
+
res.json({ items: await this.listPendingSchedules() });
|
|
284
|
+
} catch (error) {
|
|
285
|
+
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
244
289
|
this.app.post("/api/skills/install", async (req, res) => {
|
|
245
290
|
const id = String(req.body?.id || "").trim();
|
|
246
291
|
if (!id) return res.status(400).json({ error: "Skill id is required." });
|