@heylemon/lemonade 0.0.6 → 0.0.8
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.
|
@@ -109,8 +109,9 @@ function buildConfirmationSection(isMinimal) {
|
|
|
109
109
|
"",
|
|
110
110
|
"## Email Rules (MANDATORY)",
|
|
111
111
|
"1. NEVER fabricate email addresses. When user says a name (e.g. 'masood from my team'), resolve it via: Slack search-users (returns email), Gmail find-contact, or Gmail sent-to. If all fail, ASK the user.",
|
|
112
|
-
"2. ALWAYS attach files when the task involves creating AND sending a file.
|
|
113
|
-
|
|
112
|
+
"2. ALWAYS attach files when the task involves creating AND sending a file.",
|
|
113
|
+
'3. To attach a file to an email: first read the file as base64 (`base64 -i <path>`), then call GMAIL_SEND_EMAIL with the `attachment` parameter: `{"name": "file.docx", "content": "<base64>", "mimetype": "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}`.',
|
|
114
|
+
"4. ALWAYS confirm the recipient email and attachment list BEFORE sending.",
|
|
114
115
|
"",
|
|
115
116
|
];
|
|
116
117
|
}
|
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
924abd5547396c0e3e1642d40a19a47b8437dc16ed9d2aa7cccdd5f28bba3d73
|
|
@@ -8,7 +8,15 @@ import { loadInternalHooks } from "../hooks/loader.js";
|
|
|
8
8
|
import { startPluginServices } from "../plugins/services.js";
|
|
9
9
|
import { startBrowserControlServerIfEnabled } from "./server-browser.js";
|
|
10
10
|
import { scheduleRestartSentinelWake, shouldWakeFromRestartSentinel, } from "./server-restart-sentinel.js";
|
|
11
|
+
import { pullSkillPreferences } from "./skills-sync.js";
|
|
11
12
|
export async function startGatewaySidecars(params) {
|
|
13
|
+
// Restore skill preferences from cloud (non-blocking).
|
|
14
|
+
try {
|
|
15
|
+
await pullSkillPreferences();
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
params.log.warn(`skills-sync: pull failed on startup: ${String(err)}`);
|
|
19
|
+
}
|
|
12
20
|
// Start Lemonade browser control server (unless disabled via config).
|
|
13
21
|
let browserControl = null;
|
|
14
22
|
try {
|
|
@@ -20,6 +20,7 @@ import { resolveBundledSkillsDir } from "../agents/skills/bundled-dir.js";
|
|
|
20
20
|
import { authorizeGatewayConnect } from "./auth.js";
|
|
21
21
|
import { sendJson, sendMethodNotAllowed, sendUnauthorized, sendInvalidRequest, readJsonBodyOrError, } from "./http-common.js";
|
|
22
22
|
import { getBearerToken } from "./http-utils.js";
|
|
23
|
+
import { pushSkillPreferences } from "./skills-sync.js";
|
|
23
24
|
const fsp = fs.promises;
|
|
24
25
|
/**
|
|
25
26
|
* Handle skills API requests.
|
|
@@ -241,9 +242,12 @@ async function handleCreateSkill(req, res) {
|
|
|
241
242
|
enabled: true,
|
|
242
243
|
},
|
|
243
244
|
...(finalName !== safeName
|
|
244
|
-
? {
|
|
245
|
+
? {
|
|
246
|
+
note: `Renamed to '${finalName}' because '${safeName}' is a built-in skill. The built-in version has been deactivated.`,
|
|
247
|
+
}
|
|
245
248
|
: {}),
|
|
246
249
|
});
|
|
250
|
+
void pushSkillPreferences().catch(() => { });
|
|
247
251
|
return true;
|
|
248
252
|
}
|
|
249
253
|
async function handleUpdateSkill(req, res, skillName) {
|
|
@@ -296,6 +300,7 @@ async function handleUpdateSkill(req, res, skillName) {
|
|
|
296
300
|
}
|
|
297
301
|
}
|
|
298
302
|
sendJson(res, 200, { ok: true, updated });
|
|
303
|
+
void pushSkillPreferences().catch(() => { });
|
|
299
304
|
return true;
|
|
300
305
|
}
|
|
301
306
|
async function handleDeleteSkill(res, skillName) {
|
|
@@ -328,9 +333,13 @@ async function handleDeleteSkill(res, skillName) {
|
|
|
328
333
|
config.skills = {};
|
|
329
334
|
if (!config.skills.entries)
|
|
330
335
|
config.skills.entries = {};
|
|
331
|
-
config.skills.entries[baseSkillName] = {
|
|
336
|
+
config.skills.entries[baseSkillName] = {
|
|
337
|
+
...config.skills.entries[baseSkillName],
|
|
338
|
+
enabled: true,
|
|
339
|
+
};
|
|
332
340
|
}
|
|
333
341
|
await writeConfigFile(config);
|
|
334
342
|
sendJson(res, 200, { ok: true, deleted: skillName });
|
|
343
|
+
void pushSkillPreferences().catch(() => { });
|
|
335
344
|
return true;
|
|
336
345
|
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud sync for skill preferences.
|
|
3
|
+
*
|
|
4
|
+
* - pushSkillPreferences() — after local skill changes, push to backend
|
|
5
|
+
* - pullSkillPreferences() — on startup, pull from backend and apply locally
|
|
6
|
+
*
|
|
7
|
+
* Uses the Anthropic provider's baseUrl to derive the backend URL,
|
|
8
|
+
* and the gateway token for auth.
|
|
9
|
+
*/
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { loadConfig, writeConfigFile } from "../config/config.js";
|
|
13
|
+
import { CONFIG_DIR } from "../utils.js";
|
|
14
|
+
import { createSubsystemLogger } from "../logging/subsystem.js";
|
|
15
|
+
const log = createSubsystemLogger("skills-sync");
|
|
16
|
+
const fsp = fs.promises;
|
|
17
|
+
const MANAGED_SKILLS_DIR = path.join(CONFIG_DIR, "skills");
|
|
18
|
+
function resolveSyncEndpoint() {
|
|
19
|
+
const cfg = loadConfig();
|
|
20
|
+
const token = cfg.gateway?.auth?.token ?? process.env.LEMONADE_GATEWAY_TOKEN ?? undefined;
|
|
21
|
+
if (!token)
|
|
22
|
+
return null;
|
|
23
|
+
const providers = cfg.models?.providers ?? {};
|
|
24
|
+
let backendBase;
|
|
25
|
+
for (const key of ["anthropic", "openai"]) {
|
|
26
|
+
const entry = providers[key];
|
|
27
|
+
if (entry?.baseUrl?.trim()) {
|
|
28
|
+
const raw = entry.baseUrl.trim();
|
|
29
|
+
const idx = raw.indexOf("/api/lemonade/proxy");
|
|
30
|
+
if (idx > 0) {
|
|
31
|
+
backendBase = raw.slice(0, idx);
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (!backendBase)
|
|
37
|
+
return null;
|
|
38
|
+
return {
|
|
39
|
+
url: `${backendBase}/api/lemonade/skills/sync`,
|
|
40
|
+
token,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function collectCurrentState() {
|
|
44
|
+
const cfg = loadConfig();
|
|
45
|
+
const preferences = {};
|
|
46
|
+
const customSkills = {};
|
|
47
|
+
const entries = cfg.skills?.entries ?? {};
|
|
48
|
+
for (const [name, entry] of Object.entries(entries)) {
|
|
49
|
+
if (typeof entry?.enabled === "boolean") {
|
|
50
|
+
preferences[name] = { enabled: entry.enabled };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (fs.existsSync(MANAGED_SKILLS_DIR)) {
|
|
54
|
+
try {
|
|
55
|
+
const dirs = fs.readdirSync(MANAGED_SKILLS_DIR, { withFileTypes: true });
|
|
56
|
+
for (const d of dirs) {
|
|
57
|
+
if (!d.isDirectory())
|
|
58
|
+
continue;
|
|
59
|
+
const skillFile = path.join(MANAGED_SKILLS_DIR, d.name, "SKILL.md");
|
|
60
|
+
if (fs.existsSync(skillFile)) {
|
|
61
|
+
try {
|
|
62
|
+
const content = fs.readFileSync(skillFile, "utf-8");
|
|
63
|
+
customSkills[d.name] = { content, description: "" };
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// skip unreadable files
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// skip if dir can't be read
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { preferences, customSkills };
|
|
76
|
+
}
|
|
77
|
+
export async function pushSkillPreferences() {
|
|
78
|
+
try {
|
|
79
|
+
const endpoint = resolveSyncEndpoint();
|
|
80
|
+
if (!endpoint) {
|
|
81
|
+
log.debug("skills-sync: no backend endpoint configured, skipping push");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const { preferences, customSkills } = collectCurrentState();
|
|
85
|
+
const res = await fetch(endpoint.url, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: {
|
|
88
|
+
"Content-Type": "application/json",
|
|
89
|
+
Authorization: `Bearer ${endpoint.token}`,
|
|
90
|
+
},
|
|
91
|
+
body: JSON.stringify({ preferences, customSkills }),
|
|
92
|
+
signal: AbortSignal.timeout(10_000),
|
|
93
|
+
});
|
|
94
|
+
if (!res.ok) {
|
|
95
|
+
log.warn(`skills-sync: push failed (status ${res.status})`);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
log.info("skills-sync: pushed preferences to cloud");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
log.warn(`skills-sync: push error: ${String(err)}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export async function pullSkillPreferences() {
|
|
106
|
+
try {
|
|
107
|
+
const endpoint = resolveSyncEndpoint();
|
|
108
|
+
if (!endpoint) {
|
|
109
|
+
log.debug("skills-sync: no backend endpoint configured, skipping pull");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const res = await fetch(endpoint.url, {
|
|
113
|
+
method: "GET",
|
|
114
|
+
headers: {
|
|
115
|
+
Authorization: `Bearer ${endpoint.token}`,
|
|
116
|
+
},
|
|
117
|
+
signal: AbortSignal.timeout(10_000),
|
|
118
|
+
});
|
|
119
|
+
if (!res.ok) {
|
|
120
|
+
log.warn(`skills-sync: pull failed (status ${res.status})`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const data = (await res.json());
|
|
124
|
+
if (!data.ok)
|
|
125
|
+
return;
|
|
126
|
+
const cloudPrefs = data.preferences ?? {};
|
|
127
|
+
const cloudCustom = data.customSkills ?? {};
|
|
128
|
+
if (Object.keys(cloudPrefs).length === 0 && Object.keys(cloudCustom).length === 0) {
|
|
129
|
+
log.info("skills-sync: no cloud preferences to apply");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const cfg = loadConfig();
|
|
133
|
+
let changed = false;
|
|
134
|
+
// Apply preference overrides (enabled/disabled)
|
|
135
|
+
if (Object.keys(cloudPrefs).length > 0) {
|
|
136
|
+
if (!cfg.skills)
|
|
137
|
+
cfg.skills = {};
|
|
138
|
+
if (!cfg.skills.entries)
|
|
139
|
+
cfg.skills.entries = {};
|
|
140
|
+
for (const [name, pref] of Object.entries(cloudPrefs)) {
|
|
141
|
+
const current = cfg.skills.entries[name];
|
|
142
|
+
if (!current || current.enabled !== pref.enabled) {
|
|
143
|
+
cfg.skills.entries[name] = { ...current, enabled: pref.enabled };
|
|
144
|
+
changed = true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Restore custom skills (write SKILL.md files)
|
|
149
|
+
let customCount = 0;
|
|
150
|
+
for (const [name, skill] of Object.entries(cloudCustom)) {
|
|
151
|
+
if (!skill.content?.trim())
|
|
152
|
+
continue;
|
|
153
|
+
const skillDir = path.join(MANAGED_SKILLS_DIR, name);
|
|
154
|
+
const skillFile = path.join(skillDir, "SKILL.md");
|
|
155
|
+
// Only write if the file doesn't already exist locally
|
|
156
|
+
if (!fs.existsSync(skillFile)) {
|
|
157
|
+
await fsp.mkdir(skillDir, { recursive: true });
|
|
158
|
+
await fsp.writeFile(skillFile, skill.content, "utf-8");
|
|
159
|
+
customCount++;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (changed) {
|
|
163
|
+
await writeConfigFile(cfg);
|
|
164
|
+
}
|
|
165
|
+
log.info(`skills-sync: pulled preferences from cloud (${Object.keys(cloudPrefs).length} prefs, ${customCount} custom skills restored)`);
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
log.warn(`skills-sync: pull error: ${String(err)}`);
|
|
169
|
+
}
|
|
170
|
+
}
|