@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.
@@ -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: `layout ${drawerOpen ? "" : "drawer-closed"}`.trim(), key: "layout" }, [
861
- e("aside", { className: `drawer ${drawerOpen ? "open" : "closed"}`, key: "drawer" }, [
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
- !drawerOpen &&
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.2.2",
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.js",
17
+ "src/opencore-indicator.m",
18
18
  "src/skill-catalog.mjs",
19
19
  "scripts",
20
20
  "templates",
@@ -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"]);
@@ -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
 
@@ -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) => name.toLowerCase().endsWith(".png"))
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 filePath = String(req.query.path || "");
224
- if (!filePath.startsWith(this.options.screenshotDir)) {
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." });