@heylemon/lemonade 0.0.5 → 0.0.6

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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.0.5",
3
- "commit": "e4c8970d613551e3b0086ba6e128b5624c5f2155",
4
- "builtAt": "2026-02-20T05:39:04.795Z"
2
+ "version": "0.0.6",
3
+ "commit": "726ef3eb29cf96569eaa0380126b995c82e43cf3",
4
+ "builtAt": "2026-02-20T05:47:58.066Z"
5
5
  }
@@ -1 +1 @@
1
- 0084b5bec47a901a88b379363f5f31530feeb541ab85d15e5f944c012ebbef96
1
+ 51eb12e5a38916af8a01151207c0974086f45b5a7c962e824bb8ebd5fe8887d1
@@ -92,6 +92,25 @@ export async function handleSkillsHttpRequest(req, res, opts) {
92
92
  }
93
93
  // ─── Helpers ───────────────────────────────────────────────────────────────────
94
94
  const MANAGED_SKILLS_DIR = path.join(CONFIG_DIR, "skills");
95
+ function getBuiltInSkillNames() {
96
+ const bundledDir = resolveBundledSkillsDir();
97
+ if (!bundledDir || !fs.existsSync(bundledDir))
98
+ return new Set();
99
+ const loadSkills = (params) => {
100
+ const loaded = loadSkillsFromDir(params);
101
+ if (Array.isArray(loaded))
102
+ return loaded;
103
+ if (loaded &&
104
+ typeof loaded === "object" &&
105
+ "skills" in loaded &&
106
+ Array.isArray(loaded.skills)) {
107
+ return loaded.skills;
108
+ }
109
+ return [];
110
+ };
111
+ const skills = loadSkills({ dir: bundledDir, source: "lemonade-bundled" });
112
+ return new Set(skills.map((s) => s.name));
113
+ }
95
114
  function loadAllSkillEntries() {
96
115
  const config = loadConfig();
97
116
  const loadSkills = (params) => {
@@ -175,43 +194,55 @@ async function handleCreateSkill(req, res) {
175
194
  sendInvalidRequest(res, "name is invalid after sanitization");
176
195
  return true;
177
196
  }
178
- const skillDir = path.join(MANAGED_SKILLS_DIR, safeName);
179
- const skillFile = path.join(skillDir, "SKILL.md");
180
- // Check if already exists
181
- if (fs.existsSync(skillFile)) {
197
+ const builtInNames = getBuiltInSkillNames();
198
+ let finalName = safeName;
199
+ if (builtInNames.has(safeName)) {
200
+ finalName = `${safeName}-custom`;
201
+ }
202
+ const customSkillDir = path.join(MANAGED_SKILLS_DIR, finalName);
203
+ const customSkillFile = path.join(customSkillDir, "SKILL.md");
204
+ if (fs.existsSync(customSkillFile)) {
182
205
  sendJson(res, 409, {
183
- error: { message: `Skill '${safeName}' already exists`, type: "conflict" },
206
+ error: {
207
+ message: `You already have a custom skill '${finalName}'. You can replace its content or rename your new skill.`,
208
+ type: "conflict",
209
+ },
184
210
  });
185
211
  return true;
186
212
  }
187
213
  // Build SKILL.md
188
214
  let fileContent;
189
215
  if (content.startsWith("---")) {
190
- // Content already has frontmatter
191
216
  fileContent = content;
192
217
  }
193
218
  else {
194
- fileContent = `---\nname: ${safeName}\ndescription: "${description || "Custom skill"}"\n---\n\n${content}`;
219
+ fileContent = `---\nname: ${finalName}\ndescription: "${description || "Custom skill"}"\n---\n\n${content}`;
195
220
  }
196
- await fsp.mkdir(skillDir, { recursive: true });
197
- await fsp.writeFile(skillFile, fileContent, "utf-8");
198
- // Enable in config
221
+ await fsp.mkdir(customSkillDir, { recursive: true });
222
+ await fsp.writeFile(customSkillFile, fileContent, "utf-8");
199
223
  const config = loadConfig();
200
224
  if (!config.skills)
201
225
  config.skills = {};
202
226
  if (!config.skills.entries)
203
227
  config.skills.entries = {};
204
- config.skills.entries[safeName] = { enabled: true };
228
+ // If this shadows a built-in, disable the built-in and enable the custom one
229
+ if (finalName !== safeName && builtInNames.has(safeName)) {
230
+ config.skills.entries[safeName] = { ...config.skills.entries[safeName], enabled: false };
231
+ }
232
+ config.skills.entries[finalName] = { enabled: true };
205
233
  await writeConfigFile(config);
206
234
  sendJson(res, 201, {
207
235
  ok: true,
208
236
  skill: {
209
- name: safeName,
237
+ name: finalName,
210
238
  description: description || "Custom skill",
211
- path: skillFile,
239
+ path: customSkillFile,
212
240
  isBuiltIn: false,
213
241
  enabled: true,
214
242
  },
243
+ ...(finalName !== safeName
244
+ ? { note: `Renamed to '${finalName}' because '${safeName}' is a built-in skill. The built-in version has been deactivated.` }
245
+ : {}),
215
246
  });
216
247
  return true;
217
248
  }
@@ -219,9 +250,9 @@ async function handleUpdateSkill(req, res, skillName) {
219
250
  const body = (await readJsonBodyOrError(req, res, 1_000_000));
220
251
  if (!body)
221
252
  return true;
253
+ const builtInNames = getBuiltInSkillNames();
222
254
  const config = loadConfig();
223
255
  const updated = {};
224
- // Update enabled state in config
225
256
  if (typeof body.enabled === "boolean") {
226
257
  if (!config.skills)
227
258
  config.skills = {};
@@ -234,12 +265,19 @@ async function handleUpdateSkill(req, res, skillName) {
234
265
  await writeConfigFile(config);
235
266
  updated.enabled = body.enabled;
236
267
  }
237
- // Update content on disk (only for user/managed skills)
238
268
  if (typeof body.content === "string") {
269
+ if (builtInNames.has(skillName)) {
270
+ sendJson(res, 403, {
271
+ error: {
272
+ message: `'${skillName}' is a built-in skill and its content cannot be modified. You can only enable or disable it.`,
273
+ type: "forbidden",
274
+ },
275
+ });
276
+ return true;
277
+ }
239
278
  const skillFile = path.join(MANAGED_SKILLS_DIR, skillName, "SKILL.md");
240
279
  if (fs.existsSync(skillFile)) {
241
280
  let newContent = body.content.trim();
242
- // Ensure frontmatter exists
243
281
  if (!newContent.startsWith("---")) {
244
282
  const desc = typeof body.description === "string" ? body.description : "Custom skill";
245
283
  newContent = `---\nname: ${skillName}\ndescription: "${desc}"\n---\n\n${newContent}`;
@@ -261,6 +299,16 @@ async function handleUpdateSkill(req, res, skillName) {
261
299
  return true;
262
300
  }
263
301
  async function handleDeleteSkill(res, skillName) {
302
+ const builtInNames = getBuiltInSkillNames();
303
+ if (builtInNames.has(skillName)) {
304
+ sendJson(res, 403, {
305
+ error: {
306
+ message: `'${skillName}' is a built-in skill and cannot be deleted. You can only enable or disable it.`,
307
+ type: "forbidden",
308
+ },
309
+ });
310
+ return true;
311
+ }
264
312
  const skillDir = path.join(MANAGED_SKILLS_DIR, skillName);
265
313
  if (!fs.existsSync(skillDir)) {
266
314
  sendJson(res, 404, {
@@ -268,14 +316,21 @@ async function handleDeleteSkill(res, skillName) {
268
316
  });
269
317
  return true;
270
318
  }
271
- // Remove directory
272
319
  await fsp.rm(skillDir, { recursive: true, force: true });
273
- // Remove from config
274
320
  const config = loadConfig();
275
321
  if (config.skills?.entries?.[skillName]) {
276
322
  delete config.skills.entries[skillName];
277
- await writeConfigFile(config);
278
323
  }
324
+ // If deleting a custom override (e.g. "pdf-custom"), re-enable the built-in
325
+ const baseSkillName = skillName.replace(/-custom$/, "");
326
+ if (baseSkillName !== skillName && builtInNames.has(baseSkillName)) {
327
+ if (!config.skills)
328
+ config.skills = {};
329
+ if (!config.skills.entries)
330
+ config.skills.entries = {};
331
+ config.skills.entries[baseSkillName] = { ...config.skills.entries[baseSkillName], enabled: true };
332
+ }
333
+ await writeConfigFile(config);
279
334
  sendJson(res, 200, { ok: true, deleted: skillName });
280
335
  return true;
281
336
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heylemon/lemonade",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "AI gateway CLI for Lemon - local AI assistant with integrations",
5
5
  "publishConfig": {
6
6
  "access": "restricted"