@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
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@opencoreai/opencore",
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "LicenseRef-OpenCore-Personal-Use-1.0",
|
|
7
|
+
"description": "OpenCore terminal agent for ChatGPT computer use on macOS.",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"src/credential-store.mjs",
|
|
14
|
+
"src/dashboard-server.ts",
|
|
15
|
+
"src/index.ts",
|
|
16
|
+
"src/mac-controller.mjs",
|
|
17
|
+
"src/opencore-indicator.js",
|
|
18
|
+
"src/skill-catalog.mjs",
|
|
19
|
+
"scripts",
|
|
20
|
+
"templates",
|
|
21
|
+
"opencore dashboard",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"bin": {
|
|
26
|
+
"opencore": "bin/opencore.mjs"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"start": "tsx src/index.ts",
|
|
30
|
+
"engage": "node bin/opencore.mjs engage"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"dotenv": "^16.4.5",
|
|
34
|
+
"express": "^4.21.2",
|
|
35
|
+
"figlet": "^1.10.0",
|
|
36
|
+
"openai": "^5.20.1",
|
|
37
|
+
"react": "^18.3.1",
|
|
38
|
+
"react-dom": "^18.3.1",
|
|
39
|
+
"tsx": "^4.21.0",
|
|
40
|
+
"ws": "^8.18.3"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^24.6.0",
|
|
44
|
+
"typescript": "^5.9.3"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { promises as fs } from "node:fs";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import * as readlineCore from "node:readline";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { SKILL_CATALOG } from "../src/skill-catalog.mjs";
|
|
11
|
+
import { ensureCredentialStore } from "../src/credential-store.mjs";
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
const ROOT_DIR = path.resolve(__dirname, "..");
|
|
16
|
+
const TEMPLATE_DIR = path.join(ROOT_DIR, "templates");
|
|
17
|
+
const OPENCORE_HOME = path.join(os.homedir(), ".opencore");
|
|
18
|
+
const SETTINGS_PATH = path.join(OPENCORE_HOME, "configs", "settings.json");
|
|
19
|
+
const GUIDELINES_PATH = path.join(OPENCORE_HOME, "guidelines.md");
|
|
20
|
+
const INSTRUCTIONS_PATH = path.join(OPENCORE_HOME, "instructions.md");
|
|
21
|
+
const HEARTBEAT_PATH = path.join(OPENCORE_HOME, "heartbeat.md");
|
|
22
|
+
const SOUL_PATH = path.join(OPENCORE_HOME, "soul.md");
|
|
23
|
+
const PROFILE_SECTION_START = "<!-- OPENCORE_INSTALL_PROFILE_START -->";
|
|
24
|
+
const PROFILE_SECTION_END = "<!-- OPENCORE_INSTALL_PROFILE_END -->";
|
|
25
|
+
const DIRECTORIES = [
|
|
26
|
+
OPENCORE_HOME,
|
|
27
|
+
path.join(OPENCORE_HOME, "configs"),
|
|
28
|
+
path.join(OPENCORE_HOME, "logs"),
|
|
29
|
+
path.join(OPENCORE_HOME, "cache"),
|
|
30
|
+
path.join(OPENCORE_HOME, "skills"),
|
|
31
|
+
];
|
|
32
|
+
const execFileAsync = promisify(execFile);
|
|
33
|
+
|
|
34
|
+
const LEGACY_DEFAULT_MEMORY = `# OpenCore Memory
|
|
35
|
+
|
|
36
|
+
This file stores durable notes and user preferences for both the Manager Agent and Computer Agent.
|
|
37
|
+
|
|
38
|
+
## Notes
|
|
39
|
+
- Reserved for future memory features
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
const DEFAULT_CONFIG = {
|
|
43
|
+
name: "OpenCore",
|
|
44
|
+
platform: "macOS",
|
|
45
|
+
provider: "chatgpt",
|
|
46
|
+
openai_api_key: "",
|
|
47
|
+
telegram_enabled: false,
|
|
48
|
+
telegram_bot_token: "",
|
|
49
|
+
telegram_chat_id: "",
|
|
50
|
+
telegram_user_id: "",
|
|
51
|
+
telegram_pairing_code: "",
|
|
52
|
+
telegram_paired: false,
|
|
53
|
+
telegram_last_update_id: 0,
|
|
54
|
+
user_display_name: "",
|
|
55
|
+
assistant_tone: "",
|
|
56
|
+
created_at: new Date().toISOString(),
|
|
57
|
+
schema_version: 1,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
async function readSettingsOrDefault() {
|
|
61
|
+
try {
|
|
62
|
+
const raw = await fs.readFile(SETTINGS_PATH, "utf8");
|
|
63
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(raw || "{}") };
|
|
64
|
+
} catch {
|
|
65
|
+
return { ...DEFAULT_CONFIG };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function writeSettings(next) {
|
|
70
|
+
await fs.writeFile(SETTINGS_PATH, `${JSON.stringify({ ...DEFAULT_CONFIG, ...next }, null, 2)}\n`, "utf8");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function upsertMarkedSection(text, startMarker, endMarker, body) {
|
|
74
|
+
const nextSection = `${startMarker}\n${body.trim()}\n${endMarker}`;
|
|
75
|
+
const pattern = new RegExp(`${startMarker}[\\s\\S]*?${endMarker}`, "m");
|
|
76
|
+
if (pattern.test(text)) {
|
|
77
|
+
return text.replace(pattern, nextSection);
|
|
78
|
+
}
|
|
79
|
+
const base = String(text || "").trimEnd();
|
|
80
|
+
return `${base}\n\n${nextSection}\n`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function applyInstallProfile(settings) {
|
|
84
|
+
const displayName = String(settings.user_display_name || "").trim();
|
|
85
|
+
const tone = String(settings.assistant_tone || "").trim();
|
|
86
|
+
if (!displayName && !tone) return;
|
|
87
|
+
|
|
88
|
+
const soulRaw = await fs.readFile(SOUL_PATH, "utf8").catch(() => "# OpenCore Soul\n");
|
|
89
|
+
const instructionsRaw = await fs.readFile(INSTRUCTIONS_PATH, "utf8").catch(() => "# OpenCore Instructions\n");
|
|
90
|
+
|
|
91
|
+
const soulSection = `## Installed User Defaults\n- Call the user: ${displayName || "User"}\n- Preferred response tone: ${tone || "clear, direct, calm"}`;
|
|
92
|
+
const instructionsSection = `## Installed User Preferences\n- Address the user as: ${displayName || "User"}.\n- Default tone: ${tone || "clear, direct, calm"}.\n- If the user does not specify a different style, keep responses aligned with this default tone.`;
|
|
93
|
+
|
|
94
|
+
await fs.writeFile(
|
|
95
|
+
SOUL_PATH,
|
|
96
|
+
upsertMarkedSection(soulRaw, PROFILE_SECTION_START, PROFILE_SECTION_END, soulSection),
|
|
97
|
+
"utf8",
|
|
98
|
+
);
|
|
99
|
+
await fs.writeFile(
|
|
100
|
+
INSTRUCTIONS_PATH,
|
|
101
|
+
upsertMarkedSection(instructionsRaw, PROFILE_SECTION_START, PROFILE_SECTION_END, instructionsSection),
|
|
102
|
+
"utf8",
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function promptOnboardingProfile(currentSettings) {
|
|
107
|
+
if (!(input.isTTY && output.isTTY)) return currentSettings;
|
|
108
|
+
const rl = readlineCore.createInterface({ input, output });
|
|
109
|
+
const settings = { ...currentSettings };
|
|
110
|
+
try {
|
|
111
|
+
const currentName = String(settings.user_display_name || "").trim();
|
|
112
|
+
const currentTone = String(settings.assistant_tone || "").trim();
|
|
113
|
+
console.log("[OpenCore] A few defaults help OpenCore stay consistent across sessions.");
|
|
114
|
+
const displayName = (
|
|
115
|
+
await new Promise((resolve) =>
|
|
116
|
+
rl.question(
|
|
117
|
+
`OpenCore> What should I call you?${currentName ? ` [${currentName}]` : ""} `,
|
|
118
|
+
resolve,
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
).trim();
|
|
122
|
+
const tone = (
|
|
123
|
+
await new Promise((resolve) =>
|
|
124
|
+
rl.question(
|
|
125
|
+
`OpenCore> What tone should I use?${currentTone ? ` [${currentTone}]` : " [clear, direct, calm]"} `,
|
|
126
|
+
resolve,
|
|
127
|
+
),
|
|
128
|
+
)
|
|
129
|
+
).trim();
|
|
130
|
+
settings.user_display_name = displayName || currentName;
|
|
131
|
+
settings.assistant_tone = tone || currentTone || "clear, direct, calm";
|
|
132
|
+
return settings;
|
|
133
|
+
} finally {
|
|
134
|
+
rl.close();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function telegramApi(token, method, params = {}, signal) {
|
|
139
|
+
const url = new URL(`https://api.telegram.org/bot${token}/${method}`);
|
|
140
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
141
|
+
if (value === undefined || value === null || value === "") return;
|
|
142
|
+
url.searchParams.set(key, String(value));
|
|
143
|
+
});
|
|
144
|
+
const res = await fetch(url, { signal });
|
|
145
|
+
if (!res.ok) {
|
|
146
|
+
throw new Error(`Telegram API ${method} failed with HTTP ${res.status}`);
|
|
147
|
+
}
|
|
148
|
+
const json = await res.json();
|
|
149
|
+
if (!json?.ok) {
|
|
150
|
+
throw new Error(json?.description || `Telegram API ${method} failed.`);
|
|
151
|
+
}
|
|
152
|
+
return json.result;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function configureTelegram(currentSettings) {
|
|
156
|
+
if (!(input.isTTY && output.isTTY)) return currentSettings;
|
|
157
|
+
const rl = readlineCore.createInterface({ input, output });
|
|
158
|
+
let settings = { ...currentSettings };
|
|
159
|
+
try {
|
|
160
|
+
const existingEnabled = Boolean(settings.telegram_enabled && settings.telegram_bot_token);
|
|
161
|
+
const enableAnswer = (
|
|
162
|
+
await new Promise((resolve) =>
|
|
163
|
+
rl.question(
|
|
164
|
+
`Connect Telegram so you can send tasks from your phone? ${existingEnabled ? "[Y/n]" : "[y/N]"} `,
|
|
165
|
+
resolve,
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
.trim()
|
|
170
|
+
.toLowerCase();
|
|
171
|
+
|
|
172
|
+
const wantsTelegram = existingEnabled
|
|
173
|
+
? !["n", "no"].includes(enableAnswer)
|
|
174
|
+
: ["y", "yes"].includes(enableAnswer);
|
|
175
|
+
|
|
176
|
+
if (!wantsTelegram) {
|
|
177
|
+
settings.telegram_enabled = false;
|
|
178
|
+
settings.telegram_bot_token = "";
|
|
179
|
+
settings.telegram_chat_id = "";
|
|
180
|
+
settings.telegram_user_id = "";
|
|
181
|
+
settings.telegram_pairing_code = "";
|
|
182
|
+
settings.telegram_paired = false;
|
|
183
|
+
settings.telegram_last_update_id = 0;
|
|
184
|
+
console.log("[OpenCore] Telegram setup skipped.");
|
|
185
|
+
return settings;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let botToken = String(settings.telegram_bot_token || "").trim();
|
|
189
|
+
while (!botToken) {
|
|
190
|
+
botToken = (
|
|
191
|
+
await new Promise((resolve) => rl.question("Enter your Telegram bot token: ", resolve))
|
|
192
|
+
).trim();
|
|
193
|
+
if (!botToken) console.log("[OpenCore] Telegram bot token cannot be blank.");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const me = await telegramApi(botToken, "getMe");
|
|
197
|
+
console.log(`[OpenCore] Connected bot: @${me?.username || "unknown"}`);
|
|
198
|
+
settings.telegram_enabled = true;
|
|
199
|
+
settings.telegram_bot_token = botToken;
|
|
200
|
+
settings.telegram_chat_id = "";
|
|
201
|
+
settings.telegram_user_id = "";
|
|
202
|
+
settings.telegram_pairing_code = "";
|
|
203
|
+
settings.telegram_paired = false;
|
|
204
|
+
settings.telegram_last_update_id = 0;
|
|
205
|
+
console.log("[OpenCore] Telegram bot token saved.");
|
|
206
|
+
console.log("[OpenCore] Next: start OpenCore, send /start to the bot, then give the Telegram user ID and pairing code from Telegram back to OpenCore.");
|
|
207
|
+
return settings;
|
|
208
|
+
} finally {
|
|
209
|
+
rl.close();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function installSelectedSkills(skillIds) {
|
|
214
|
+
await ensureDir(path.join(OPENCORE_HOME, "skills"));
|
|
215
|
+
const installed = [];
|
|
216
|
+
const skipped = [];
|
|
217
|
+
for (const id of skillIds) {
|
|
218
|
+
const skill = SKILL_CATALOG.find((item) => item.id === id);
|
|
219
|
+
if (!skill) continue;
|
|
220
|
+
const dir = path.join(OPENCORE_HOME, "skills", skill.id);
|
|
221
|
+
const skillFile = path.join(dir, "SKILL.md");
|
|
222
|
+
const configFile = path.join(dir, "config.json");
|
|
223
|
+
try {
|
|
224
|
+
await fs.access(skillFile);
|
|
225
|
+
skipped.push(skill.id);
|
|
226
|
+
continue;
|
|
227
|
+
} catch {}
|
|
228
|
+
await ensureDir(dir);
|
|
229
|
+
await fs.writeFile(skillFile, `${skill.markdown.trim()}\n`, "utf8");
|
|
230
|
+
await fs.writeFile(
|
|
231
|
+
configFile,
|
|
232
|
+
`${JSON.stringify(
|
|
233
|
+
{
|
|
234
|
+
id: skill.id,
|
|
235
|
+
name: skill.name,
|
|
236
|
+
description: skill.description,
|
|
237
|
+
...skill.config,
|
|
238
|
+
},
|
|
239
|
+
null,
|
|
240
|
+
2,
|
|
241
|
+
)}\n`,
|
|
242
|
+
"utf8",
|
|
243
|
+
);
|
|
244
|
+
installed.push(skill.id);
|
|
245
|
+
}
|
|
246
|
+
return { installed, skipped };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function promptSkillInstallation() {
|
|
250
|
+
if (!(input.isTTY && output.isTTY)) return;
|
|
251
|
+
const choices = [
|
|
252
|
+
...SKILL_CATALOG.filter((skill) => !skill.config?.builtin).map((skill) => ({ ...skill, isSkip: false })),
|
|
253
|
+
{ id: "__skip__", name: "Skip for now", description: "Continue without installing optional skills.", isSkip: true },
|
|
254
|
+
];
|
|
255
|
+
let cursor = 0;
|
|
256
|
+
const selected = new Set();
|
|
257
|
+
|
|
258
|
+
const render = () => {
|
|
259
|
+
readlineCore.cursorTo(output, 0, 0);
|
|
260
|
+
readlineCore.clearScreenDown(output);
|
|
261
|
+
const lines = [
|
|
262
|
+
"[OpenCore] Optional skills available to install",
|
|
263
|
+
"Use Up/Down to move, Space to select, Enter to confirm.",
|
|
264
|
+
"",
|
|
265
|
+
];
|
|
266
|
+
choices.forEach((choice, idx) => {
|
|
267
|
+
const isActive = idx === cursor;
|
|
268
|
+
const isSelected = selected.has(choice.id);
|
|
269
|
+
const pointer = isActive ? ">" : " ";
|
|
270
|
+
const circle = isSelected ? "◉" : "◯";
|
|
271
|
+
lines.push(`${pointer} ${circle} ${choice.name}${choice.isSkip ? "" : ` (${choice.id})`}`);
|
|
272
|
+
lines.push(` ${choice.description}`);
|
|
273
|
+
});
|
|
274
|
+
output.write(`${lines.join("\n")}\n`);
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const chosenIds = await new Promise((resolve, reject) => {
|
|
278
|
+
const onData = (buf) => {
|
|
279
|
+
const key = String(buf);
|
|
280
|
+
if (key === "\u0003") {
|
|
281
|
+
cleanup();
|
|
282
|
+
reject(new Error("Skill selection interrupted by user."));
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (key === "\r" || key === "\n") {
|
|
286
|
+
cleanup();
|
|
287
|
+
resolve(Array.from(selected));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (key === "\x1b[A") {
|
|
291
|
+
cursor = (cursor - 1 + choices.length) % choices.length;
|
|
292
|
+
render();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (key === "\x1b[B") {
|
|
296
|
+
cursor = (cursor + 1) % choices.length;
|
|
297
|
+
render();
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (key === " ") {
|
|
301
|
+
const current = choices[cursor];
|
|
302
|
+
if (current.isSkip) {
|
|
303
|
+
selected.clear();
|
|
304
|
+
selected.add(current.id);
|
|
305
|
+
} else {
|
|
306
|
+
selected.delete("__skip__");
|
|
307
|
+
if (selected.has(current.id)) selected.delete(current.id);
|
|
308
|
+
else selected.add(current.id);
|
|
309
|
+
}
|
|
310
|
+
render();
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const cleanup = () => {
|
|
315
|
+
input.off("data", onData);
|
|
316
|
+
if (typeof input.setRawMode === "function") input.setRawMode(false);
|
|
317
|
+
input.pause();
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
if (typeof input.setRawMode === "function") input.setRawMode(true);
|
|
322
|
+
input.resume();
|
|
323
|
+
input.on("data", onData);
|
|
324
|
+
render();
|
|
325
|
+
} catch (error) {
|
|
326
|
+
cleanup();
|
|
327
|
+
reject(error);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const ids = chosenIds.filter((id) => id !== "__skip__");
|
|
332
|
+
if (!ids.length) {
|
|
333
|
+
console.log("[OpenCore] Skill install skipped for now.");
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const result = await installSelectedSkills(ids);
|
|
337
|
+
if (result.installed.length) {
|
|
338
|
+
console.log(`[OpenCore] Installed skills: ${result.installed.join(", ")}`);
|
|
339
|
+
} else {
|
|
340
|
+
console.log("[OpenCore] No new skills installed.");
|
|
341
|
+
}
|
|
342
|
+
if (result.skipped.length) {
|
|
343
|
+
console.log(`[OpenCore] Already installed (unchanged): ${result.skipped.join(", ")}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function ensureDefaultSkillsInstalled() {
|
|
348
|
+
const defaultIds = SKILL_CATALOG.filter((skill) => skill.config?.builtin).map((skill) => skill.id);
|
|
349
|
+
if (!defaultIds.length) return;
|
|
350
|
+
await installSelectedSkills(defaultIds);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function ensureDir(dirPath) {
|
|
354
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function ensureFile(filePath, contents) {
|
|
358
|
+
try {
|
|
359
|
+
await fs.access(filePath);
|
|
360
|
+
} catch {
|
|
361
|
+
await fs.writeFile(filePath, contents, "utf8");
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function readTemplate(name, fallback) {
|
|
366
|
+
try {
|
|
367
|
+
return await fs.readFile(path.join(TEMPLATE_DIR, name), "utf8");
|
|
368
|
+
} catch {
|
|
369
|
+
return fallback;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function ensureTemplateApplied(filePath, template, legacyDefaults = []) {
|
|
374
|
+
try {
|
|
375
|
+
const existing = await fs.readFile(filePath, "utf8");
|
|
376
|
+
const normalized = existing.trim();
|
|
377
|
+
const isLegacy = legacyDefaults.some((value) => normalized === String(value || "").trim());
|
|
378
|
+
if (isLegacy) {
|
|
379
|
+
await fs.writeFile(filePath, template, "utf8");
|
|
380
|
+
}
|
|
381
|
+
} catch {
|
|
382
|
+
await fs.writeFile(filePath, template, "utf8");
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function run() {
|
|
387
|
+
const defaultSoul = await readTemplate("default-soul.md", "# OpenCore Soul\nName: OpenCore\n");
|
|
388
|
+
const defaultMemory = await readTemplate("default-memory.md", LEGACY_DEFAULT_MEMORY);
|
|
389
|
+
const defaultGuidelines = await readTemplate("default-guidelines.md", "# OpenCore Guidelines\n");
|
|
390
|
+
const defaultInstructions = await readTemplate("default-instructions.md", "# OpenCore Instructions\n");
|
|
391
|
+
const defaultHeartbeat = await readTemplate("default-heartbeat.md", "# OpenCore Heartbeat\n");
|
|
392
|
+
|
|
393
|
+
for (const dir of DIRECTORIES) {
|
|
394
|
+
await ensureDir(dir);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
await ensureTemplateApplied(path.join(OPENCORE_HOME, "soul.md"), defaultSoul, ["# OpenCore Soul\nName: OpenCore\n"]);
|
|
398
|
+
await ensureTemplateApplied(path.join(OPENCORE_HOME, "memory.md"), defaultMemory, [LEGACY_DEFAULT_MEMORY]);
|
|
399
|
+
await ensureTemplateApplied(HEARTBEAT_PATH, defaultHeartbeat, ["# OpenCore Heartbeat\n"]);
|
|
400
|
+
await ensureTemplateApplied(GUIDELINES_PATH, defaultGuidelines, ["# OpenCore Guidelines\n"]);
|
|
401
|
+
await ensureTemplateApplied(INSTRUCTIONS_PATH, defaultInstructions, ["# OpenCore Instructions\n"]);
|
|
402
|
+
await ensureFile(SETTINGS_PATH, `${JSON.stringify(DEFAULT_CONFIG, null, 2)}\n`);
|
|
403
|
+
await ensureCredentialStore();
|
|
404
|
+
await ensureDefaultSkillsInstalled();
|
|
405
|
+
|
|
406
|
+
let settings = DEFAULT_CONFIG;
|
|
407
|
+
try {
|
|
408
|
+
const raw = await fs.readFile(SETTINGS_PATH, "utf8");
|
|
409
|
+
settings = { ...DEFAULT_CONFIG, ...JSON.parse(raw || "{}") };
|
|
410
|
+
} catch {
|
|
411
|
+
settings = { ...DEFAULT_CONFIG };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
settings.provider = "chatgpt";
|
|
415
|
+
let openaiKey = String(process.env.OPENAI_API_KEY || settings.openai_api_key || "").trim();
|
|
416
|
+
if (!openaiKey) {
|
|
417
|
+
if (!(input.isTTY && output.isTTY)) {
|
|
418
|
+
throw new Error("Missing OPENAI_API_KEY. Re-run install in interactive terminal to enter a key.");
|
|
419
|
+
}
|
|
420
|
+
const rl = readlineCore.createInterface({ input, output });
|
|
421
|
+
try {
|
|
422
|
+
while (!openaiKey) {
|
|
423
|
+
openaiKey = (
|
|
424
|
+
await new Promise((resolve) => rl.question("Enter your OpenAI API key (required): ", resolve))
|
|
425
|
+
).trim();
|
|
426
|
+
if (!openaiKey) console.log("[OpenCore] API key cannot be blank.");
|
|
427
|
+
}
|
|
428
|
+
} finally {
|
|
429
|
+
rl.close();
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
settings.openai_api_key = openaiKey;
|
|
433
|
+
settings = await promptOnboardingProfile(settings);
|
|
434
|
+
await writeSettings(settings);
|
|
435
|
+
await applyInstallProfile(settings);
|
|
436
|
+
console.log("[OpenCore] ChatGPT configuration saved to ~/.opencore/configs/settings.json");
|
|
437
|
+
|
|
438
|
+
settings = await configureTelegram(settings);
|
|
439
|
+
await writeSettings(settings);
|
|
440
|
+
|
|
441
|
+
await promptSkillInstallation();
|
|
442
|
+
|
|
443
|
+
console.log(`[OpenCore] Initialized local home at ${OPENCORE_HOME}`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
run().catch((error) => {
|
|
447
|
+
console.warn(`[OpenCore] postinstall setup warning: ${error instanceof Error ? error.message : String(error)}`);
|
|
448
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { promises as fs } from "node:fs";
|
|
4
|
+
|
|
5
|
+
const OPENCORE_HOME = path.join(os.homedir(), ".opencore");
|
|
6
|
+
const CREDENTIALS_PATH = path.join(OPENCORE_HOME, "configs", "credentials.json");
|
|
7
|
+
|
|
8
|
+
export function defaultCredentialsStore() {
|
|
9
|
+
return {
|
|
10
|
+
default_email: "",
|
|
11
|
+
default_email_provider: "",
|
|
12
|
+
auto_email_activation_enabled: false,
|
|
13
|
+
entries: [],
|
|
14
|
+
schema_version: 1,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function credentialStorePath() {
|
|
19
|
+
return CREDENTIALS_PATH;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function ensureCredentialStore() {
|
|
23
|
+
await fs.mkdir(path.dirname(CREDENTIALS_PATH), { recursive: true });
|
|
24
|
+
try {
|
|
25
|
+
await fs.access(CREDENTIALS_PATH);
|
|
26
|
+
} catch {
|
|
27
|
+
await fs.writeFile(CREDENTIALS_PATH, `${JSON.stringify(defaultCredentialsStore(), null, 2)}\n`, "utf8");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function readCredentialStore() {
|
|
32
|
+
await ensureCredentialStore();
|
|
33
|
+
try {
|
|
34
|
+
const raw = await fs.readFile(CREDENTIALS_PATH, "utf8");
|
|
35
|
+
const parsed = JSON.parse(raw || "{}");
|
|
36
|
+
return normalizeCredentialStore(parsed);
|
|
37
|
+
} catch {
|
|
38
|
+
return defaultCredentialsStore();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function writeCredentialStore(next) {
|
|
43
|
+
const normalized = normalizeCredentialStore(next);
|
|
44
|
+
await fs.writeFile(CREDENTIALS_PATH, `${JSON.stringify(normalized, null, 2)}\n`, "utf8");
|
|
45
|
+
return normalized;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function normalizeWebsite(value) {
|
|
49
|
+
const raw = String(value || "").trim().toLowerCase();
|
|
50
|
+
if (!raw) return "";
|
|
51
|
+
try {
|
|
52
|
+
const withProto = raw.includes("://") ? raw : `https://${raw}`;
|
|
53
|
+
const hostname = new URL(withProto).hostname.toLowerCase();
|
|
54
|
+
return hostname.replace(/^www\./, "");
|
|
55
|
+
} catch {
|
|
56
|
+
return raw
|
|
57
|
+
.replace(/^https?:\/\//, "")
|
|
58
|
+
.replace(/^www\./, "")
|
|
59
|
+
.split(/[/?#]/)[0]
|
|
60
|
+
.trim();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeEntry(entry) {
|
|
65
|
+
const website = normalizeWebsite(entry?.website || entry?.site || "");
|
|
66
|
+
return {
|
|
67
|
+
id: String(entry?.id || `${website || "credential"}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`).trim(),
|
|
68
|
+
website,
|
|
69
|
+
email: String(entry?.email || "").trim(),
|
|
70
|
+
password: String(entry?.password || "").trim(),
|
|
71
|
+
email_provider: String(entry?.email_provider || "").trim(),
|
|
72
|
+
notes: String(entry?.notes || "").trim(),
|
|
73
|
+
generated_by_ai: Boolean(entry?.generated_by_ai),
|
|
74
|
+
created_at: String(entry?.created_at || new Date().toISOString()),
|
|
75
|
+
updated_at: String(entry?.updated_at || new Date().toISOString()),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function normalizeCredentialStore(store) {
|
|
80
|
+
const base = defaultCredentialsStore();
|
|
81
|
+
const entries = Array.isArray(store?.entries) ? store.entries.map(normalizeEntry).filter((item) => item.website) : [];
|
|
82
|
+
return {
|
|
83
|
+
...base,
|
|
84
|
+
...store,
|
|
85
|
+
default_email: String(store?.default_email || "").trim(),
|
|
86
|
+
default_email_provider: String(store?.default_email_provider || "").trim(),
|
|
87
|
+
auto_email_activation_enabled: Boolean(store?.auto_email_activation_enabled),
|
|
88
|
+
entries,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function upsertCredentialEntry(entry) {
|
|
93
|
+
const store = await readCredentialStore();
|
|
94
|
+
const normalized = normalizeEntry(entry);
|
|
95
|
+
const byIdIndex = store.entries.findIndex((item) => item.id === normalized.id);
|
|
96
|
+
const byWebsiteIndex = store.entries.findIndex((item) => item.website === normalized.website);
|
|
97
|
+
const index = byIdIndex >= 0 ? byIdIndex : byWebsiteIndex;
|
|
98
|
+
if (index >= 0) {
|
|
99
|
+
const existing = store.entries[index];
|
|
100
|
+
store.entries[index] = {
|
|
101
|
+
...existing,
|
|
102
|
+
...normalized,
|
|
103
|
+
created_at: existing.created_at,
|
|
104
|
+
updated_at: new Date().toISOString(),
|
|
105
|
+
};
|
|
106
|
+
} else {
|
|
107
|
+
store.entries.unshift(normalized);
|
|
108
|
+
}
|
|
109
|
+
return writeCredentialStore(store);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function deleteCredentialEntry(idOrWebsite) {
|
|
113
|
+
const needle = String(idOrWebsite || "").trim();
|
|
114
|
+
const store = await readCredentialStore();
|
|
115
|
+
store.entries = store.entries.filter((item) => item.id !== needle && item.website !== normalizeWebsite(needle));
|
|
116
|
+
return writeCredentialStore(store);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function updateCredentialDefaults(patch) {
|
|
120
|
+
const store = await readCredentialStore();
|
|
121
|
+
const next = {
|
|
122
|
+
...store,
|
|
123
|
+
default_email: String(patch?.default_email ?? store.default_email ?? "").trim(),
|
|
124
|
+
default_email_provider: String(patch?.default_email_provider ?? store.default_email_provider ?? "").trim(),
|
|
125
|
+
auto_email_activation_enabled:
|
|
126
|
+
patch?.auto_email_activation_enabled === undefined
|
|
127
|
+
? Boolean(store.auto_email_activation_enabled)
|
|
128
|
+
: Boolean(patch.auto_email_activation_enabled),
|
|
129
|
+
};
|
|
130
|
+
return writeCredentialStore(next);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function summarizeCredentialStoreForPrompt(store) {
|
|
134
|
+
const normalized = normalizeCredentialStore(store);
|
|
135
|
+
const siteLines = normalized.entries
|
|
136
|
+
.slice(0, 40)
|
|
137
|
+
.map((entry) => `- ${entry.website} | email=${entry.email ? "***saved***" : "none"} | password=${entry.password ? "***saved***" : "none"} | provider=${entry.email_provider || "none"} | generated_by_ai=${entry.generated_by_ai ? "yes" : "no"}`);
|
|
138
|
+
return [
|
|
139
|
+
`Default email saved: ${normalized.default_email ? "***saved***" : "no"}`,
|
|
140
|
+
`Default email provider: ${normalized.default_email_provider || "none"}`,
|
|
141
|
+
`Automatic account email activation enabled: ${normalized.auto_email_activation_enabled ? "yes" : "no"}`,
|
|
142
|
+
"Stored website credentials:",
|
|
143
|
+
...(siteLines.length ? siteLines : ["- none"]),
|
|
144
|
+
].join("\n");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function buildCredentialExecutionContext(store, taskText) {
|
|
148
|
+
const normalized = normalizeCredentialStore(store);
|
|
149
|
+
const text = String(taskText || "").toLowerCase();
|
|
150
|
+
const matched = normalized.entries.filter((entry) => {
|
|
151
|
+
if (!entry.website) return false;
|
|
152
|
+
const site = entry.website.toLowerCase();
|
|
153
|
+
const root = site.split(".")[0];
|
|
154
|
+
return text.includes(site) || (root && text.includes(root));
|
|
155
|
+
});
|
|
156
|
+
const signUpLike =
|
|
157
|
+
text.includes("sign up") ||
|
|
158
|
+
text.includes("signup") ||
|
|
159
|
+
text.includes("create account") ||
|
|
160
|
+
text.includes("register");
|
|
161
|
+
|
|
162
|
+
const lines = [];
|
|
163
|
+
if (matched.length) {
|
|
164
|
+
lines.push("Stored credentials relevant to this task:");
|
|
165
|
+
for (const entry of matched.slice(0, 3)) {
|
|
166
|
+
lines.push(`- website: ${entry.website}`);
|
|
167
|
+
lines.push(` email: ${entry.email || "(none saved)"}`);
|
|
168
|
+
lines.push(` password: ${entry.password || "(none saved)"}`);
|
|
169
|
+
lines.push(` email_provider: ${entry.email_provider || "(none saved)"}`);
|
|
170
|
+
if (entry.notes) lines.push(` notes: ${entry.notes}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (signUpLike && normalized.default_email) {
|
|
174
|
+
lines.push("Default account-creation email available:");
|
|
175
|
+
lines.push(`- email: ${normalized.default_email}`);
|
|
176
|
+
lines.push(`- provider: ${normalized.default_email_provider || "(none set)"}`);
|
|
177
|
+
lines.push(
|
|
178
|
+
`- automatic email activation enabled: ${normalized.auto_email_activation_enabled ? "yes" : "no"}`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
if (!lines.length) return "";
|
|
182
|
+
lines.push(
|
|
183
|
+
"If you create a new account or update credentials during this task, include one line in the final response exactly in this format:",
|
|
184
|
+
);
|
|
185
|
+
lines.push(
|
|
186
|
+
'CREDENTIAL_SAVE {"website":"example.com","email":"user@example.com","password":"secret","email_provider":"gmail","notes":"optional","generated_by_ai":true}',
|
|
187
|
+
);
|
|
188
|
+
return lines.join("\n");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function extractCredentialSaveCandidates(text) {
|
|
192
|
+
const lines = String(text || "")
|
|
193
|
+
.split("\n")
|
|
194
|
+
.map((line) => line.trim())
|
|
195
|
+
.filter(Boolean);
|
|
196
|
+
const candidates = [];
|
|
197
|
+
for (const line of lines) {
|
|
198
|
+
const match = line.match(/^CREDENTIAL_SAVE\s+(\{.+\})$/);
|
|
199
|
+
if (!match) continue;
|
|
200
|
+
try {
|
|
201
|
+
const parsed = JSON.parse(match[1]);
|
|
202
|
+
const normalized = normalizeEntry(parsed);
|
|
203
|
+
if (normalized.website && (normalized.email || normalized.password)) {
|
|
204
|
+
candidates.push(normalized);
|
|
205
|
+
}
|
|
206
|
+
} catch {}
|
|
207
|
+
}
|
|
208
|
+
return candidates;
|
|
209
|
+
}
|