@opencoreai/opencore 0.2.2 → 0.4.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/src/index.ts CHANGED
@@ -14,6 +14,7 @@ import { MacController } from "./mac-controller.mjs";
14
14
  import { SKILL_CATALOG } from "./skill-catalog.mjs";
15
15
  import {
16
16
  buildCredentialExecutionContext,
17
+ credentialStorePath,
17
18
  ensureCredentialStore,
18
19
  extractCredentialSaveCandidates,
19
20
  readCredentialStore,
@@ -35,6 +36,7 @@ const MANAGER_HEARTBEAT_INTERVAL_MS = Math.max(
35
36
  const OPENCORE_HOME = path.join(os.homedir(), ".opencore");
36
37
  const SOUL_PATH = path.join(OPENCORE_HOME, "soul.md");
37
38
  const MEMORY_PATH = path.join(OPENCORE_HOME, "memory.md");
39
+ const COMPUTER_PROFILE_PATH = path.join(OPENCORE_HOME, "computer-profile.md");
38
40
  const HEARTBEAT_PATH = path.join(OPENCORE_HOME, "heartbeat.md");
39
41
  const GUIDELINES_PATH = path.join(OPENCORE_HOME, "guidelines.md");
40
42
  const INSTRUCTIONS_PATH = path.join(OPENCORE_HOME, "instructions.md");
@@ -49,9 +51,18 @@ const __filename = fileURLToPath(import.meta.url);
49
51
  const __dirname = path.dirname(__filename);
50
52
  const ROOT_DIR = path.resolve(__dirname, "..");
51
53
  const TEMPLATE_DIR = path.join(ROOT_DIR, "templates");
52
- const INDICATOR_SCRIPT_PATH = path.join(ROOT_DIR, "src", "opencore-indicator.js");
54
+ const INDICATOR_SOURCE_PATH = path.join(ROOT_DIR, "src", "opencore-indicator.m");
55
+ const INDICATOR_BINARY_PATH = path.join(OPENCORE_HOME, "cache", "opencore-indicator");
53
56
  const DEFAULT_SOUL = "# OpenCore Soul\nName: OpenCore\n";
54
57
  const DEFAULT_MEMORY = "# OpenCore Memory\n";
58
+ const DEFAULT_COMPUTER_PROFILE = `# OpenCore Computer Profile
59
+
60
+ ## Machine Snapshot
61
+ - Pending first machine scan.
62
+
63
+ ## Learned Facts
64
+ - None yet.
65
+ `;
55
66
  const DEFAULT_HEARTBEAT = `# OpenCore Heartbeat
56
67
 
57
68
  ## Current Task
@@ -74,6 +85,7 @@ This file stores durable notes and action history for both the Manager Agent and
74
85
  const EDITABLE_FILES: Record<string, string> = {
75
86
  soul: SOUL_PATH,
76
87
  memory: MEMORY_PATH,
88
+ "computer-profile": COMPUTER_PROFILE_PATH,
77
89
  heartbeat: HEARTBEAT_PATH,
78
90
  guidelines: GUIDELINES_PATH,
79
91
  instructions: INSTRUCTIONS_PATH,
@@ -147,6 +159,7 @@ type TelegramConfig = {
147
159
  const require = createRequire(import.meta.url);
148
160
  const { version: APP_VERSION } = require("../package.json");
149
161
  const execFileAsync = promisify(execFile);
162
+ process.umask(0o077);
150
163
 
151
164
  async function readSettings() {
152
165
  try {
@@ -178,6 +191,155 @@ async function ensureTemplateApplied(filePath: string, template: string, legacyD
178
191
  }
179
192
  }
180
193
 
194
+ async function chmodIfPossible(targetPath: string, mode: number) {
195
+ try {
196
+ await fs.chmod(targetPath, mode);
197
+ } catch {}
198
+ }
199
+
200
+ async function enforceOpenCorePermissions() {
201
+ const dirs = [
202
+ OPENCORE_HOME,
203
+ path.join(OPENCORE_HOME, "configs"),
204
+ path.join(OPENCORE_HOME, "logs"),
205
+ path.join(OPENCORE_HOME, "cache"),
206
+ SCREENSHOT_DIR,
207
+ SKILLS_DIR,
208
+ ];
209
+ const files = [
210
+ SOUL_PATH,
211
+ MEMORY_PATH,
212
+ COMPUTER_PROFILE_PATH,
213
+ HEARTBEAT_PATH,
214
+ GUIDELINES_PATH,
215
+ INSTRUCTIONS_PATH,
216
+ SETTINGS_PATH,
217
+ SCHEDULES_PATH,
218
+ INDICATOR_STATE_PATH,
219
+ SCHEDULER_LOG_PATH,
220
+ credentialStorePath(),
221
+ ];
222
+ for (const dir of dirs) await chmodIfPossible(dir, 0o700);
223
+ for (const file of files) await chmodIfPossible(file, 0o600);
224
+ }
225
+
226
+ function upsertMarkedSection(text: string, startMarker: string, endMarker: string, body: string) {
227
+ const nextSection = `${startMarker}\n${body.trim()}\n${endMarker}`;
228
+ const pattern = new RegExp(`${startMarker}[\\s\\S]*?${endMarker}`, "m");
229
+ if (pattern.test(text)) {
230
+ return text.replace(pattern, nextSection);
231
+ }
232
+ const base = String(text || "").trimEnd();
233
+ return `${base}\n\n${nextSection}\n`;
234
+ }
235
+
236
+ async function safeExecOutput(file: string, args: string[] = []) {
237
+ try {
238
+ const { stdout } = await execFileAsync(file, args, { maxBuffer: 1024 * 1024 * 2 } as any);
239
+ return String(stdout || "").trim();
240
+ } catch {
241
+ return "";
242
+ }
243
+ }
244
+
245
+ async function detectDefaultBrowser() {
246
+ const bundleId = await safeExecOutput("osascript", ["-e", 'id of application (path to default web browser)']);
247
+ return bundleId || "unknown";
248
+ }
249
+
250
+ async function collectComputerProfileSnapshot() {
251
+ const [productName, productVersion, buildVersion, arch, model, browserBundleId] = await Promise.all([
252
+ safeExecOutput("sw_vers", ["-productName"]),
253
+ safeExecOutput("sw_vers", ["-productVersion"]),
254
+ safeExecOutput("sw_vers", ["-buildVersion"]),
255
+ safeExecOutput("uname", ["-m"]),
256
+ safeExecOutput("sysctl", ["-n", "hw.model"]),
257
+ detectDefaultBrowser(),
258
+ ]);
259
+
260
+ const appNames = [];
261
+ for (const baseDir of ["/Applications", path.join(os.homedir(), "Applications")]) {
262
+ try {
263
+ const entries = await fs.readdir(baseDir, { withFileTypes: true });
264
+ for (const entry of entries) {
265
+ if (!entry.isDirectory()) continue;
266
+ if (!entry.name.toLowerCase().endsWith(".app")) continue;
267
+ appNames.push(entry.name.replace(/\.app$/i, ""));
268
+ }
269
+ } catch {}
270
+ }
271
+ const apps = [...new Set(appNames)].sort((a, b) => a.localeCompare(b)).slice(0, 30);
272
+
273
+ return [
274
+ "## Machine Snapshot",
275
+ `- Computer model: ${model || "unknown"}`,
276
+ `- Architecture: ${arch || "unknown"}`,
277
+ `- macOS: ${[productName, productVersion].filter(Boolean).join(" ") || "unknown"}${buildVersion ? ` (${buildVersion})` : ""}`,
278
+ `- Default browser bundle ID: ${browserBundleId}`,
279
+ `- User home: ${os.homedir()}`,
280
+ `- Current workspace root: ${process.cwd()}`,
281
+ `- OpenCore project root: ${ROOT_DIR}`,
282
+ `- Sample installed apps: ${apps.length ? apps.join(", ") : "none detected"}`,
283
+ ].join("\n");
284
+ }
285
+
286
+ async function refreshComputerProfileSnapshot() {
287
+ const existing = await readTextFile(COMPUTER_PROFILE_PATH, DEFAULT_COMPUTER_PROFILE);
288
+ const snapshot = await collectComputerProfileSnapshot();
289
+ const next = upsertMarkedSection(
290
+ existing,
291
+ "<!-- OPENCORE_COMPUTER_SNAPSHOT_START -->",
292
+ "<!-- OPENCORE_COMPUTER_SNAPSHOT_END -->",
293
+ snapshot,
294
+ );
295
+ await fs.writeFile(COMPUTER_PROFILE_PATH, `${next.trimEnd()}\n`, "utf8");
296
+ await enforceOpenCorePermissions();
297
+ }
298
+
299
+ function shouldStoreAsComputerFact(note: string) {
300
+ const text = String(note || "").toLowerCase();
301
+ return [
302
+ "browser",
303
+ "safari",
304
+ "chrome",
305
+ "arc",
306
+ "finder",
307
+ "application",
308
+ "app ",
309
+ "installed",
310
+ "mac",
311
+ "computer",
312
+ "workspace",
313
+ "screen recording",
314
+ "accessibility",
315
+ "permission",
316
+ "display",
317
+ "desktop",
318
+ "folder",
319
+ "directory",
320
+ ].some((token) => text.includes(token));
321
+ }
322
+
323
+ async function appendComputerProfileFacts(notes: string[]) {
324
+ const lines = Array.isArray(notes)
325
+ ? notes.map((note) => String(note || "").trim()).filter((note) => note && shouldStoreAsComputerFact(note))
326
+ : [];
327
+ if (!lines.length) return;
328
+ const existing = await readTextFile(COMPUTER_PROFILE_PATH, DEFAULT_COMPUTER_PROFILE);
329
+ const existingLines = new Set(
330
+ existing
331
+ .split("\n")
332
+ .map((line) => line.trim().replace(/^- /, ""))
333
+ .filter(Boolean),
334
+ );
335
+ const additions = lines.filter((line) => !existingLines.has(line));
336
+ if (!additions.length) return;
337
+ const updated = existing.trimEnd().replace(/\n*$/, "");
338
+ const body = `${updated}\n${additions.map((line) => `- ${line}`).join("\n")}\n`;
339
+ await fs.writeFile(COMPUTER_PROFILE_PATH, body, "utf8");
340
+ await enforceOpenCorePermissions();
341
+ }
342
+
181
343
  async function ensureOpenCoreHome() {
182
344
  await fs.mkdir(path.join(OPENCORE_HOME, "configs"), { recursive: true });
183
345
  await fs.mkdir(path.join(OPENCORE_HOME, "logs"), { recursive: true });
@@ -187,12 +349,14 @@ async function ensureOpenCoreHome() {
187
349
 
188
350
  const soulTemplate = await readTemplate("default-soul.md", DEFAULT_SOUL);
189
351
  const memoryTemplate = await readTemplate("default-memory.md", LEGACY_DEFAULT_MEMORY);
352
+ const computerProfileTemplate = await readTemplate("default-computer-profile.md", DEFAULT_COMPUTER_PROFILE);
190
353
  const guidelinesTemplate = await readTemplate("default-guidelines.md", DEFAULT_GUIDELINES);
191
354
  const instructionsTemplate = await readTemplate("default-instructions.md", DEFAULT_INSTRUCTIONS);
192
355
  const heartbeatTemplate = await readTemplate("default-heartbeat.md", DEFAULT_HEARTBEAT);
193
356
 
194
357
  await ensureTemplateApplied(SOUL_PATH, soulTemplate, [DEFAULT_SOUL]);
195
358
  await ensureTemplateApplied(MEMORY_PATH, memoryTemplate, [LEGACY_DEFAULT_MEMORY, DEFAULT_MEMORY]);
359
+ await ensureTemplateApplied(COMPUTER_PROFILE_PATH, computerProfileTemplate, [DEFAULT_COMPUTER_PROFILE]);
196
360
  await ensureTemplateApplied(HEARTBEAT_PATH, heartbeatTemplate, [DEFAULT_HEARTBEAT]);
197
361
  await ensureTemplateApplied(GUIDELINES_PATH, guidelinesTemplate, [DEFAULT_GUIDELINES]);
198
362
  await ensureTemplateApplied(INSTRUCTIONS_PATH, instructionsTemplate, [DEFAULT_INSTRUCTIONS]);
@@ -231,6 +395,7 @@ async function ensureOpenCoreHome() {
231
395
  }
232
396
  await ensureDefaultOpenCoreSkills();
233
397
  await ensureCredentialStore();
398
+ await enforceOpenCorePermissions();
234
399
  }
235
400
 
236
401
  async function ensureDefaultOpenCoreSkills() {
@@ -277,6 +442,7 @@ async function readSchedules(): Promise<ScheduledTaskRecord[]> {
277
442
 
278
443
  async function writeSchedules(items: ScheduledTaskRecord[]) {
279
444
  await fs.writeFile(SCHEDULES_PATH, `${JSON.stringify(items, null, 2)}\n`, "utf8");
445
+ await enforceOpenCorePermissions();
280
446
  }
281
447
 
282
448
  async function updateScheduleRecord(id: string, apply: (item: ScheduledTaskRecord) => ScheduledTaskRecord | null) {
@@ -431,23 +597,82 @@ async function showMacNotification(title: string, message: string) {
431
597
  }
432
598
  }
433
599
 
600
+ const indicatorStateCache: {
601
+ running: boolean;
602
+ active: boolean;
603
+ message: string;
604
+ events: string[];
605
+ updated_at?: string;
606
+ } = {
607
+ running: false,
608
+ active: false,
609
+ message: "OpenCore is idle",
610
+ events: [],
611
+ };
612
+
434
613
  async function writeIndicatorState(state: {
435
614
  running: boolean;
436
615
  active: boolean;
437
616
  message?: string;
617
+ events?: string[];
438
618
  }) {
619
+ indicatorStateCache.running = Boolean(state.running);
620
+ indicatorStateCache.active = Boolean(state.active);
621
+ indicatorStateCache.message = String(state.message || indicatorStateCache.message || "").trim();
622
+ if (Array.isArray(state.events)) {
623
+ indicatorStateCache.events = state.events.slice(-14);
624
+ }
625
+ indicatorStateCache.updated_at = new Date().toISOString();
439
626
  const payload = {
440
- running: Boolean(state.running),
441
- active: Boolean(state.active),
442
- message: String(state.message || "").trim(),
627
+ running: indicatorStateCache.running,
628
+ active: indicatorStateCache.active,
629
+ message: indicatorStateCache.message,
630
+ events: indicatorStateCache.events,
443
631
  updated_at: new Date().toISOString(),
444
632
  };
445
633
  await fs.writeFile(INDICATOR_STATE_PATH, `${JSON.stringify(payload, null, 2)}\n`, "utf8").catch(() => {});
634
+ await enforceOpenCorePermissions();
635
+ }
636
+
637
+ async function appendIndicatorEvent(text: string, options: { active?: boolean; message?: string } = {}) {
638
+ const line = String(text || "").trim();
639
+ if (!line) return;
640
+ const stamp = new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
641
+ const events = [...indicatorStateCache.events, `${stamp} ${line}`].slice(-14);
642
+ await writeIndicatorState({
643
+ running: indicatorStateCache.running || true,
644
+ active: options.active ?? indicatorStateCache.active,
645
+ message: options.message ?? indicatorStateCache.message,
646
+ events,
647
+ });
446
648
  }
447
649
 
448
- function startIndicatorProcess() {
650
+ async function ensureIndicatorBinary() {
449
651
  try {
450
- const child = spawn("osascript", ["-l", "JavaScript", INDICATOR_SCRIPT_PATH], {
652
+ const [sourceStat, binaryStat] = await Promise.all([
653
+ fs.stat(INDICATOR_SOURCE_PATH),
654
+ fs.stat(INDICATOR_BINARY_PATH).catch(() => null),
655
+ ]);
656
+ if (binaryStat && binaryStat.mtimeMs >= sourceStat.mtimeMs) {
657
+ return INDICATOR_BINARY_PATH;
658
+ }
659
+ await execFileAsync("clang", ["-framework", "Cocoa", "-framework", "QuartzCore", INDICATOR_SOURCE_PATH, "-o", INDICATOR_BINARY_PATH], {
660
+ maxBuffer: 1024 * 1024 * 8,
661
+ } as any);
662
+ await chmodIfPossible(INDICATOR_BINARY_PATH, 0o755);
663
+ return INDICATOR_BINARY_PATH;
664
+ } catch (error) {
665
+ const message = error instanceof Error ? error.message : String(error);
666
+ console.warn(`[indicator] build failed: ${message}`);
667
+ return "";
668
+ }
669
+ }
670
+
671
+ async function startIndicatorProcess() {
672
+ try {
673
+ const binaryPath = await ensureIndicatorBinary();
674
+ if (!binaryPath) return null;
675
+ const child = spawn(binaryPath, [], {
451
676
  stdio: ["ignore", "ignore", "pipe"],
452
677
  env: { ...process.env, OPENCORE_INDICATOR_STATE_PATH: INDICATOR_STATE_PATH },
453
678
  });
@@ -506,6 +731,7 @@ async function writeSettingsPatch(patch: Record<string, any>) {
506
731
  const current = await readSettings();
507
732
  const next = { ...current, ...patch };
508
733
  await fs.writeFile(SETTINGS_PATH, `${JSON.stringify(next, null, 2)}\n`, "utf8");
734
+ await enforceOpenCorePermissions();
509
735
  return next;
510
736
  }
511
737
 
@@ -565,6 +791,39 @@ async function sendTelegramMessage(config: TelegramConfig, text: string) {
565
791
  }
566
792
  }
567
793
 
794
+ async function sendTelegramPhoto(config: TelegramConfig, filePath: string, caption = "") {
795
+ if (!config.enabled || !config.chatId || !filePath) return;
796
+ const data = await fs.readFile(filePath);
797
+ const form = new FormData();
798
+ form.append("chat_id", config.chatId);
799
+ if (caption.trim()) form.append("caption", caption.trim().slice(0, 1024));
800
+ const ext = path.extname(filePath).toLowerCase();
801
+ const type = ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" : "image/png";
802
+ form.append("photo", new Blob([data], { type }), path.basename(filePath));
803
+
804
+ const controller = new AbortController();
805
+ const timeoutMs = 12_000;
806
+ const timeout = setTimeout(() => controller.abort(new Error(`Telegram API sendPhoto timed out after ${timeoutMs}ms`)), timeoutMs);
807
+ try {
808
+ const res = await fetch(`https://api.telegram.org/bot${config.botToken}/sendPhoto`, {
809
+ method: "POST",
810
+ body: form,
811
+ signal: controller.signal,
812
+ });
813
+ if (!res.ok) {
814
+ throw new Error(`Telegram API sendPhoto failed with HTTP ${res.status}`);
815
+ }
816
+ const json = await res.json();
817
+ if (!json?.ok) {
818
+ throw new Error(String(json?.description || "Telegram API sendPhoto failed."));
819
+ }
820
+ } catch (error) {
821
+ throw new Error(`Telegram API sendPhoto request failed: ${formatNetworkError(error)}`);
822
+ } finally {
823
+ clearTimeout(timeout);
824
+ }
825
+ }
826
+
568
827
  function publicTelegramConfig(config: TelegramConfig) {
569
828
  return {
570
829
  enabled: config.enabled,
@@ -582,10 +841,12 @@ async function loadPromptContext() {
582
841
  const guidelinesTemplate = await readTemplate("default-guidelines.md", DEFAULT_GUIDELINES);
583
842
  const instructionsTemplate = await readTemplate("default-instructions.md", DEFAULT_INSTRUCTIONS);
584
843
  const heartbeatTemplate = await readTemplate("default-heartbeat.md", DEFAULT_HEARTBEAT);
844
+ const computerProfileTemplate = await readTemplate("default-computer-profile.md", DEFAULT_COMPUTER_PROFILE);
585
845
  const soul = await readTextFile(SOUL_PATH, soulTemplate);
586
846
  const heartbeat = await readTextFile(HEARTBEAT_PATH, heartbeatTemplate);
587
847
  const guidelines = await readTextFile(GUIDELINES_PATH, guidelinesTemplate);
588
848
  const instructions = await readTextFile(INSTRUCTIONS_PATH, instructionsTemplate);
849
+ const computerProfile = await readTextFile(COMPUTER_PROFILE_PATH, computerProfileTemplate);
589
850
  const config = await readSettings();
590
851
  const credentials = await readCredentialStore();
591
852
  const memoryExcerpt = await readMemoryExcerpt(4000);
@@ -595,6 +856,7 @@ async function loadPromptContext() {
595
856
  heartbeat,
596
857
  guidelines,
597
858
  instructions,
859
+ computerProfile,
598
860
  installedSkills: installedSkills || "(no installed skills found)",
599
861
  config: JSON.stringify(maskConfig(config), null, 2),
600
862
  credentialsSummary: summarizeCredentialStoreForPrompt(credentials),
@@ -624,6 +886,8 @@ async function buildComputerSystemPrompt() {
624
886
  ctx.credentialsSummary,
625
887
  "OpenCore memory excerpt (recent):",
626
888
  ctx.memoryExcerpt,
889
+ "OpenCore computer profile:",
890
+ ctx.computerProfile,
627
891
  ].join("\n\n");
628
892
  }
629
893
 
@@ -635,14 +899,14 @@ async function buildManagerSystemPrompt() {
635
899
  "Manager Agent handles everything except direct computer-use actions.",
636
900
  "You receive user tasks first. Decide whether to answer directly, edit local markdown files, or delegate to Computer Agent.",
637
901
  "You also maintain durable OpenCore state. When the user states a lasting rule, preference, permission, restriction, identity update, or computer fact, update the appropriate markdown files automatically.",
638
- "Use guidelines.md for safety, permission, and capability restrictions. Use instructions.md for workflow preferences and operating style. Use soul.md for OpenCore identity/personality. Use memory.md for durable learned facts about the computer, apps, environment, and user preferences.",
902
+ "Use guidelines.md for safety, permission, and capability restrictions. Use instructions.md for workflow preferences and operating style. Use soul.md for OpenCore identity/personality. Use memory.md for durable learned facts and task history. Use computer-profile.md for durable machine facts, installed apps, browser facts, workspace context, and environment inventory.",
639
903
  "Delegate to Computer Agent when UI/computer control is needed.",
640
904
  "Use route=local when the work can be performed locally with shell/file commands without UI control, such as file management, project inspection, text generation to files, and command-line maintenance.",
641
905
  "Do direct answer when task is informational and no computer control is required.",
642
906
  "Do edit_file when user asks to update any markdown/state/config file or when task requires writing files.",
643
907
  "You may edit files and folders anywhere on the user's computer when needed, unless guidelines/instructions explicitly forbid it.",
644
908
  "Return strict JSON only, no markdown, no explanations.",
645
- 'JSON schema: {"route":"direct|computer|edit_file|local","direct_answer":"...","computer_task":"...","file_target":"soul|memory|heartbeat|guidelines|instructions|/absolute/or/relative/path","edit_instructions":"...","local_task":"..."}',
909
+ 'JSON schema: {"route":"direct|computer|edit_file|local","direct_answer":"...","computer_task":"...","file_target":"soul|memory|computer-profile|heartbeat|guidelines|instructions|/absolute/or/relative/path","edit_instructions":"...","local_task":"..."}',
646
910
  "Follow OpenCore guidelines exactly:",
647
911
  ctx.guidelines,
648
912
  "Follow OpenCore user instructions exactly:",
@@ -657,6 +921,8 @@ async function buildManagerSystemPrompt() {
657
921
  ctx.credentialsSummary,
658
922
  "OpenCore memory excerpt (recent):",
659
923
  ctx.memoryExcerpt,
924
+ "OpenCore computer profile:",
925
+ ctx.computerProfile,
660
926
  ].join("\n\n");
661
927
  }
662
928
 
@@ -1114,6 +1380,7 @@ async function managerBuildLocalCommand({
1114
1380
  "Rules:\n" +
1115
1381
  "- Use zsh-compatible syntax.\n" +
1116
1382
  "- Prefer safe file and project commands.\n" +
1383
+ "- Do not use sudo, rm -rf, eval, disk erase commands, reboot commands, or download-and-execute patterns.\n" +
1117
1384
  "- Use absolute cwd when useful, or omit it.\n" +
1118
1385
  "- Do not include explanations or markdown.",
1119
1386
  maxTokens: 700,
@@ -1233,11 +1500,20 @@ async function runLocalCommandTask({
1233
1500
  if (!built || !built.command) {
1234
1501
  return "I could not build a valid local shell command for that task.";
1235
1502
  }
1503
+ const commandSafetyError = getUnsafeLocalCommandReason(built.command);
1504
+ if (commandSafetyError) {
1505
+ await appendMemory(`[manager] blocked_local_command=${built.command}`);
1506
+ return `I refused to run that local command because it violated OpenCore security rules: ${commandSafetyError}`;
1507
+ }
1236
1508
  const cwd = built.cwd
1237
1509
  ? path.isAbsolute(built.cwd)
1238
1510
  ? built.cwd
1239
1511
  : path.resolve(os.homedir(), built.cwd)
1240
1512
  : os.homedir();
1513
+ const cwdSafetyError = await getUnsafeLocalCommandCwdReason(cwd);
1514
+ if (cwdSafetyError) {
1515
+ return `I refused to run that local command because the working directory was not allowed: ${cwdSafetyError}`;
1516
+ }
1241
1517
  await appendMemory(`[manager] local_command=${built.command} cwd=${cwd}`);
1242
1518
  const { stdout, stderr } = await execFileAsync("/bin/zsh", ["-lc", built.command], {
1243
1519
  cwd,
@@ -1248,6 +1524,44 @@ async function runLocalCommandTask({
1248
1524
  return output ? `${summary}\n\n${output}`.trim() : summary;
1249
1525
  }
1250
1526
 
1527
+ function getUnsafeLocalCommandReason(command: string) {
1528
+ const value = String(command || "").trim();
1529
+ if (!value) return "empty command";
1530
+ if (value.includes("\n")) return "multi-line commands are blocked";
1531
+ const checks: Array<[RegExp, string]> = [
1532
+ [/\bsudo\b/i, "sudo escalation is blocked"],
1533
+ [/\brm\s+-rf\b/i, "recursive forced deletion is blocked"],
1534
+ [/\bdiskutil\s+erase/i, "disk erase operations are blocked"],
1535
+ [/\bmkfs\b/i, "filesystem formatting is blocked"],
1536
+ [/\bdd\s+if=/i, "raw disk write patterns are blocked"],
1537
+ [/\bshutdown\b|\breboot\b/i, "system shutdown and reboot are blocked"],
1538
+ [/\blaunchctl\s+bootout\b/i, "launchctl bootout is blocked"],
1539
+ [/\bcurl\b[\s\S]*\|\s*(sh|bash|zsh)\b/i, "pipe-to-shell download execution is blocked"],
1540
+ [/\bwget\b[\s\S]*\|\s*(sh|bash|zsh)\b/i, "pipe-to-shell download execution is blocked"],
1541
+ [/\beval\b/i, "shell eval is blocked"],
1542
+ [/<\(/, "process substitution is blocked"],
1543
+ ];
1544
+ for (const [pattern, reason] of checks) {
1545
+ if (pattern.test(value)) return reason;
1546
+ }
1547
+ return "";
1548
+ }
1549
+
1550
+ async function getUnsafeLocalCommandCwdReason(cwd: string) {
1551
+ const resolved = path.resolve(cwd);
1552
+ const disallowedRoots = ["/System", "/Library", "/private", "/usr"];
1553
+ if (disallowedRoots.some((root) => resolved === root || resolved.startsWith(`${root}/`))) {
1554
+ return "system-owned directories are blocked for manager local tasks";
1555
+ }
1556
+ try {
1557
+ const stat = await fs.stat(resolved);
1558
+ if (!stat.isDirectory()) return "working directory is not a directory";
1559
+ } catch {
1560
+ return "working directory does not exist";
1561
+ }
1562
+ return "";
1563
+ }
1564
+
1251
1565
  function isValidCronExpression(value: string) {
1252
1566
  return /^(\S+\s+){4}\S+$/.test(String(value || "").trim());
1253
1567
  }
@@ -1334,7 +1648,7 @@ async function recoverScheduledTasksIfNeeded({
1334
1648
  last_run_at: new Date().toISOString(),
1335
1649
  last_error: "",
1336
1650
  });
1337
- const answer = await runManagedTask({
1651
+ const result = await runManagedTask({
1338
1652
  runtime,
1339
1653
  controller,
1340
1654
  userPrompt: item.task,
@@ -1349,7 +1663,7 @@ async function recoverScheduledTasksIfNeeded({
1349
1663
  await setScheduleRecordState(item.id, { last_status: "done", last_error: "" });
1350
1664
  }
1351
1665
  results.push(`${item.id}: recovered successfully`);
1352
- await appendMemory(`[schedule_recovered id=${item.id}] ${answer.slice(0, 200)}`);
1666
+ await appendMemory(`[schedule_recovered id=${item.id}] ${result.answer.slice(0, 200)}`);
1353
1667
  } catch (error) {
1354
1668
  const msg = error instanceof Error ? error.message : String(error);
1355
1669
  await setScheduleRecordState(item.id, { last_status: "error", last_error: msg });
@@ -1374,13 +1688,13 @@ async function managerInferPersistentUpdates({
1374
1688
  userText:
1375
1689
  `User message:\n${userPrompt}\n\n` +
1376
1690
  "Determine whether this message contains durable instructions, restrictions, preferences, identity updates, or computer facts that should be persisted for future tasks.\n" +
1377
- 'Return JSON only using schema: {"memory_notes":["..."],"file_updates":[{"target":"guidelines|instructions|soul|memory","instructions":"..."}]}\n' +
1691
+ 'Return JSON only using schema: {"memory_notes":["..."],"file_updates":[{"target":"guidelines|instructions|soul|memory|computer-profile","instructions":"..."}]}\n' +
1378
1692
  "Rules:\n" +
1379
1693
  "- Only include file_updates for durable information that should persist beyond the current task.\n" +
1380
1694
  "- If the user says something cannot be done on this computer or should never be done, update guidelines.\n" +
1381
1695
  "- If the user gives preferred behavior or workflow style, update instructions.\n" +
1382
1696
  "- If the user changes OpenCore identity/personality, update soul.\n" +
1383
- "- Store important learned facts about the computer or apps in memory_notes.\n" +
1697
+ "- Store important learned facts about the computer or apps in memory_notes and/or computer-profile updates.\n" +
1384
1698
  "- If nothing durable should be saved, return empty arrays.",
1385
1699
  maxTokens: 900,
1386
1700
  });
@@ -1425,6 +1739,7 @@ async function applyPersistentUpdatePlan({
1425
1739
  }
1426
1740
  const notes = Array.isArray(plan.memory_notes) ? plan.memory_notes : [];
1427
1741
  await appendLearnedFacts(notes);
1742
+ await appendComputerProfileFacts(notes);
1428
1743
  }
1429
1744
 
1430
1745
  async function managerExtractLearnedFacts({
@@ -1450,7 +1765,9 @@ async function managerExtractLearnedFacts({
1450
1765
  maxTokens: 500,
1451
1766
  });
1452
1767
  const parsed = extractJsonObject(text);
1453
- return Array.isArray((parsed as any)?.memory_notes) ? (parsed as any).memory_notes : [];
1768
+ const notes = Array.isArray((parsed as any)?.memory_notes) ? (parsed as any).memory_notes : [];
1769
+ await appendComputerProfileFacts(notes);
1770
+ return notes;
1454
1771
  }
1455
1772
 
1456
1773
  async function managerWriteHeartbeat({
@@ -1843,11 +2160,13 @@ async function runManagedTask({
1843
2160
  dashboard?: DashboardServer | null;
1844
2161
  publishToConsole?: boolean;
1845
2162
  indicatorMessage?: string;
1846
- }) {
2163
+ }): Promise<{ answer: string; screenshotPath?: string }> {
2164
+ let lastScreenshotPath = "";
1847
2165
  await appendMemory(`[user] ${userPrompt}`);
1848
2166
  await dashboard?.publishChat({ role: "user", source: source === "schedule" ? "manager" : source, text: userPrompt });
1849
2167
  dashboard?.publishEvent({ type: "task_started", source: source === "schedule" ? "manager" : source });
1850
2168
  await writeIndicatorState({ running: true, active: true, message: indicatorMessage });
2169
+ await appendIndicatorEvent(`Task started: ${userPrompt.slice(0, 120)}`, { active: true, message: indicatorMessage });
1851
2170
  await showMacNotification("OpenCore", indicatorMessage);
1852
2171
  if (publishToConsole) console.log(`Running (${source})...`);
1853
2172
 
@@ -1865,8 +2184,9 @@ async function runManagedTask({
1865
2184
  await dashboard?.publishChat({ role: "assistant", source: "system", text: telegramSetup.answer });
1866
2185
  dashboard?.publishEvent({ type: "task_completed", source: source === "schedule" ? "manager" : source });
1867
2186
  await writeIndicatorState({ running: true, active: false, message: "OpenCore is idle" });
2187
+ await appendIndicatorEvent("Updated Telegram settings", { active: false, message: "OpenCore is idle" });
1868
2188
  await showMacNotification("OpenCore", "OpenCore updated Telegram settings.");
1869
- return telegramSetup.answer;
2189
+ return { answer: telegramSetup.answer };
1870
2190
  }
1871
2191
 
1872
2192
  const credentialSetup = await handleCredentialSetupRequest(userPrompt);
@@ -1875,8 +2195,9 @@ async function runManagedTask({
1875
2195
  await dashboard?.publishChat({ role: "assistant", source: "system", text: credentialSetup.answer });
1876
2196
  dashboard?.publishEvent({ type: "task_completed", source: source === "schedule" ? "manager" : source });
1877
2197
  await writeIndicatorState({ running: true, active: false, message: "OpenCore is idle" });
2198
+ await appendIndicatorEvent("Updated local credentials", { active: false, message: "OpenCore is idle" });
1878
2199
  await showMacNotification("OpenCore", "OpenCore updated local credentials.");
1879
- return credentialSetup.answer;
2200
+ return { answer: credentialSetup.answer };
1880
2201
  }
1881
2202
 
1882
2203
  if (wantsHeartbeatAudit(userPrompt)) {
@@ -1892,7 +2213,8 @@ async function runManagedTask({
1892
2213
  await appendMemory(`[assistant] ${answer}`);
1893
2214
  await dashboard?.publishChat({ role: "assistant", source: "system", text: answer });
1894
2215
  await writeIndicatorState({ running: true, active: false, message: "OpenCore is idle" });
1895
- return answer;
2216
+ await appendIndicatorEvent("Heartbeat check completed", { active: false, message: "OpenCore is idle" });
2217
+ return { answer };
1896
2218
  }
1897
2219
 
1898
2220
  const schedulePlan = await managerDetectScheduleRequest({ runtime, userPrompt });
@@ -1908,8 +2230,9 @@ async function runManagedTask({
1908
2230
  await dashboard?.publishChat({ role: "assistant", source: "system", text: scheduleAnswer });
1909
2231
  dashboard?.publishEvent({ type: "task_completed", source: source === "schedule" ? "manager" : source });
1910
2232
  await writeIndicatorState({ running: true, active: false, message: "OpenCore is idle" });
2233
+ await appendIndicatorEvent("Scheduled task created", { active: false, message: "OpenCore is idle" });
1911
2234
  await showMacNotification("OpenCore", "OpenCore scheduled the requested task.");
1912
- return scheduleAnswer;
2235
+ return { answer: scheduleAnswer };
1913
2236
  }
1914
2237
 
1915
2238
  const decision = await managerDecide({ runtime, userPrompt });
@@ -1954,7 +2277,15 @@ async function runManagedTask({
1954
2277
  controller,
1955
2278
  prompt: delegatedTask,
1956
2279
  secretTaskContext: credentialTaskContext,
1957
- onEvent: (evt) => dashboard?.publishEvent(evt),
2280
+ onEvent: (evt) => {
2281
+ if (evt?.type === "screenshot" && evt?.path) {
2282
+ lastScreenshotPath = String(evt.path);
2283
+ void appendIndicatorEvent(`Screenshot captured · step ${evt.step || "?"}`, { active: true, message: indicatorMessage });
2284
+ } else if (evt?.type === "action" && evt?.action) {
2285
+ void appendIndicatorEvent(`Action: ${String(evt.action)}`, { active: true, message: indicatorMessage });
2286
+ }
2287
+ dashboard?.publishEvent(evt);
2288
+ },
1958
2289
  });
1959
2290
  }
1960
2291
 
@@ -1967,18 +2298,29 @@ async function runManagedTask({
1967
2298
  await appendLearnedFacts(await managerExtractLearnedFacts({ runtime, userPrompt, answer }));
1968
2299
  await clearCompletedHeartbeat();
1969
2300
  dashboard?.publishEvent({ type: "file_updated", target: HEARTBEAT_PATH });
1970
- await dashboard?.publishChat({ role: "assistant", source: "system", text: answer });
2301
+ await dashboard?.publishChat({
2302
+ role: "assistant",
2303
+ source: "system",
2304
+ text: answer,
2305
+ screenshot_path: lastScreenshotPath || undefined,
2306
+ });
1971
2307
  dashboard?.publishEvent({ type: "task_completed", source: source === "schedule" ? "manager" : source });
1972
2308
  await writeIndicatorState({ running: true, active: false, message: "OpenCore is idle" });
2309
+ await appendIndicatorEvent(source === "schedule" ? "Scheduled task finished" : "Task finished", {
2310
+ active: false,
2311
+ message: "OpenCore is idle",
2312
+ });
1973
2313
  await showMacNotification("OpenCore", source === "schedule" ? "OpenCore finished a scheduled task." : "OpenCore finished the current task.");
1974
- return answer;
2314
+ return { answer, screenshotPath: lastScreenshotPath || undefined };
1975
2315
  }
1976
2316
 
1977
2317
  async function main() {
1978
2318
  const argv = process.argv.slice(2);
1979
2319
  const scheduledMode = argv[0] === "--scheduled-id" ? String(argv[1] || "").trim() : "";
1980
2320
  await ensureOpenCoreHome();
1981
- await writeIndicatorState({ running: true, active: false, message: "OpenCore is idle" });
2321
+ await refreshComputerProfileSnapshot();
2322
+ await writeIndicatorState({ running: true, active: false, message: "OpenCore is idle", events: [] });
2323
+ await appendIndicatorEvent("OpenCore started", { active: false, message: "OpenCore is idle" });
1982
2324
  const settings = await readSettings();
1983
2325
  const provider = "chatgpt";
1984
2326
  const openaiKey = String(process.env.OPENAI_API_KEY || settings.openai_api_key || "").trim();
@@ -2000,7 +2342,7 @@ async function main() {
2000
2342
  displayHeight: process.env.COMPUTER_USE_DISPLAY_HEIGHT,
2001
2343
  });
2002
2344
  await controller.init();
2003
- let indicator: any = startIndicatorProcess();
2345
+ let indicator: any = await startIndicatorProcess();
2004
2346
 
2005
2347
  if (scheduledMode) {
2006
2348
  const schedules = await readSchedules();
@@ -2028,7 +2370,7 @@ async function main() {
2028
2370
  last_error: "",
2029
2371
  });
2030
2372
  }
2031
- const answer = await runManagedTask({
2373
+ const result = await runManagedTask({
2032
2374
  runtime,
2033
2375
  controller,
2034
2376
  userPrompt: schedule.task,
@@ -2043,7 +2385,7 @@ async function main() {
2043
2385
  } else {
2044
2386
  await setScheduleRecordState(schedule.id, { last_run_at: now, last_status: "done", last_error: "" });
2045
2387
  }
2046
- console.log(answer);
2388
+ console.log(result.answer);
2047
2389
  await writeIndicatorState({ running: false, active: false, message: "OpenCore has stopped" });
2048
2390
  stopIndicatorProcess(indicator);
2049
2391
  process.exit(0);
@@ -2292,16 +2634,27 @@ async function main() {
2292
2634
  const item = queue.shift();
2293
2635
  if (!item) continue;
2294
2636
  try {
2295
- const answer = await runManagedTask({
2637
+ const result = await runManagedTask({
2296
2638
  runtime,
2297
2639
  controller,
2298
2640
  userPrompt: item.text,
2299
2641
  source: item.source,
2300
2642
  dashboard,
2301
2643
  });
2302
- console.log(`\nAssistant:\n${answer}\n`);
2644
+ console.log(`\nAssistant:\n${result.answer}\n`);
2303
2645
  if (item.source === "telegram") {
2304
- await sendTelegramMessage(telegramConfig, answer);
2646
+ await sendTelegramMessage(telegramConfig, result.answer);
2647
+ if (result.screenshotPath) {
2648
+ try {
2649
+ await sendTelegramPhoto(telegramConfig, result.screenshotPath, "Final OpenCore screenshot");
2650
+ } catch (photoError) {
2651
+ console.warn(
2652
+ `[telegram] final screenshot send failed: ${
2653
+ photoError instanceof Error ? photoError.message : String(photoError)
2654
+ }`,
2655
+ );
2656
+ }
2657
+ }
2305
2658
  }
2306
2659
  } catch (error) {
2307
2660
  const msg = error instanceof Error ? error.message : String(error);
@@ -2310,6 +2663,7 @@ async function main() {
2310
2663
  await dashboard.publishChat({ role: "error", source: "system", text: `Request failed: ${msg}` });
2311
2664
  dashboard.publishEvent({ type: "task_failed", source: item.source });
2312
2665
  await writeIndicatorState({ running: true, active: false, message: "OpenCore is idle" });
2666
+ await appendIndicatorEvent(`Task failed: ${msg}`, { active: false, message: "OpenCore is idle" });
2313
2667
  await showMacNotification("OpenCore", "OpenCore stopped because the current task failed.");
2314
2668
  if (item.source === "telegram") {
2315
2669
  await sendTelegramMessage(telegramConfig, `Request failed: ${msg}`);