@lumerahq/cli 0.29.1 → 0.30.0-dev.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,7 +4,7 @@ import { existsSync } from "fs";
4
4
  import { join, resolve } from "path";
5
5
  import pc from "picocolors";
6
6
  var SUPPORTED_SUBCOMMANDS = /* @__PURE__ */ new Set(["list", "inspect", "invoke", "test"]);
7
- var FUNCTIONS_SDK_REQUIREMENT = "lumera[functions]>=0.32.0,<0.33.0";
7
+ var FUNCTIONS_SDK_REQUIREMENT = "lumera[functions]>=0.33.0,<0.34.0";
8
8
  var FUNCTIONS_RUNNER_PROTOCOL_VERSION = 1;
9
9
  function errorMessage(error, fallback) {
10
10
  return error instanceof Error && error.message ? error.message : fallback;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  userContextHeaders
3
- } from "./chunk-FHHWIV4C.js";
3
+ } from "./chunk-U3ZLIXGD.js";
4
4
  import {
5
5
  fetchWithRetry
6
6
  } from "./chunk-FJFIWC7G.js";
@@ -299,6 +299,38 @@ var ApiClient = class {
299
299
  method: "DELETE"
300
300
  });
301
301
  }
302
+ // Project skills
303
+ async listAgentSkills(opts) {
304
+ const params = new URLSearchParams({ limit: "100" });
305
+ if (opts?.project_id) params.set("project_id", opts.project_id);
306
+ if (opts?.include_global) params.set("include_global", "true");
307
+ const all = [];
308
+ let offset = 0;
309
+ for (; ; ) {
310
+ params.set("offset", String(offset));
311
+ const result = await this.request(`/api/lm_agent_skills?${params}`);
312
+ const skills = result.skills || [];
313
+ all.push(...skills);
314
+ if (!result.has_more || skills.length === 0) break;
315
+ offset += skills.length;
316
+ }
317
+ return all;
318
+ }
319
+ async createAgentSkill(def) {
320
+ return this.request("/api/lm_agent_skills", {
321
+ method: "POST",
322
+ body: JSON.stringify(def)
323
+ });
324
+ }
325
+ async updateAgentSkill(id, def) {
326
+ return this.request(`/api/lm_agent_skills/${id}`, {
327
+ method: "PATCH",
328
+ body: JSON.stringify(def)
329
+ });
330
+ }
331
+ async deleteAgentSkill(id) {
332
+ await this.request(`/api/lm_agent_skills/${id}`, { method: "DELETE" });
333
+ }
302
334
  // Resolve a list of refs (slugs or IDs) to skill records. Chunks
303
335
  // requests so callers don't have to think about the server-side
304
336
  // per-request cap.
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  createApiClient,
6
6
  isApiErrorStatus
7
- } from "./chunk-FHHWIV4C.js";
7
+ } from "./chunk-U3ZLIXGD.js";
8
8
  import {
9
9
  findProjectRoot,
10
10
  getAppName
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  deps,
3
3
  syncDeps
4
- } from "./chunk-7ZTCXKII.js";
4
+ } from "./chunk-YMWDIAUG.js";
5
5
  import "./chunk-2CR762KB.js";
6
- import "./chunk-FHHWIV4C.js";
6
+ import "./chunk-U3ZLIXGD.js";
7
7
  import "./chunk-JLVVHTBY.js";
8
8
  import "./chunk-FJFIWC7G.js";
9
9
  import "./chunk-PNKVD2UK.js";
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  dev
3
- } from "./chunk-KC3VBP6R.js";
3
+ } from "./chunk-LVIIWE44.js";
4
4
  import {
5
5
  syncDeps
6
- } from "./chunk-7ZTCXKII.js";
6
+ } from "./chunk-YMWDIAUG.js";
7
7
  import {
8
8
  loadEnv
9
9
  } from "./chunk-2CR762KB.js";
10
10
  import {
11
11
  createApiClient
12
- } from "./chunk-FHHWIV4C.js";
12
+ } from "./chunk-U3ZLIXGD.js";
13
13
  import {
14
14
  findProjectRoot,
15
15
  getApiUrl,
@@ -4,7 +4,7 @@ import {
4
4
  findFunctionsProjectRoot,
5
5
  functions,
6
6
  runFunctionRunner
7
- } from "./chunk-QPVRRJFJ.js";
7
+ } from "./chunk-FLFEF2BN.js";
8
8
  import "./chunk-PNKVD2UK.js";
9
9
  export {
10
10
  buildFunctionRunnerArgs,
package/dist/index.js CHANGED
@@ -240,49 +240,49 @@ async function main() {
240
240
  switch (command) {
241
241
  // Resource commands
242
242
  case "plan":
243
- await import("./resources-P4VA2JPE.js").then((m) => m.plan(args.slice(1)));
243
+ await import("./resources-YEOQ7YN6.js").then((m) => m.plan(args.slice(1)));
244
244
  break;
245
245
  case "apply":
246
- await import("./resources-P4VA2JPE.js").then((m) => m.apply(args.slice(1)));
246
+ await import("./resources-YEOQ7YN6.js").then((m) => m.apply(args.slice(1)));
247
247
  break;
248
248
  case "pull":
249
- await import("./resources-P4VA2JPE.js").then((m) => m.pull(args.slice(1)));
249
+ await import("./resources-YEOQ7YN6.js").then((m) => m.pull(args.slice(1)));
250
250
  break;
251
251
  case "destroy":
252
- await import("./resources-P4VA2JPE.js").then((m) => m.destroy(args.slice(1)));
252
+ await import("./resources-YEOQ7YN6.js").then((m) => m.destroy(args.slice(1)));
253
253
  break;
254
254
  case "list":
255
- await import("./resources-P4VA2JPE.js").then((m) => m.list(args.slice(1)));
255
+ await import("./resources-YEOQ7YN6.js").then((m) => m.list(args.slice(1)));
256
256
  break;
257
257
  case "show":
258
- await import("./resources-P4VA2JPE.js").then((m) => m.show(args.slice(1)));
258
+ await import("./resources-YEOQ7YN6.js").then((m) => m.show(args.slice(1)));
259
259
  break;
260
260
  case "diff":
261
- await import("./resources-P4VA2JPE.js").then((m) => m.diff(args.slice(1)));
261
+ await import("./resources-YEOQ7YN6.js").then((m) => m.diff(args.slice(1)));
262
262
  break;
263
263
  // Development
264
264
  case "dev":
265
- await import("./dev-OJEV76DA.js").then((m) => m.dev(args.slice(1)));
265
+ await import("./dev-IWTA5G5M.js").then((m) => m.dev(args.slice(1)));
266
266
  break;
267
267
  case "run":
268
- await import("./run-WHVUVIYB.js").then((m) => m.run(args.slice(1)));
268
+ await import("./run-SLLDX3RF.js").then((m) => m.run(args.slice(1)));
269
269
  break;
270
270
  case "functions":
271
- await import("./functions-KP2NO4RC.js").then(
271
+ await import("./functions-55J6E2RF.js").then(
272
272
  (m) => m.functions(subcommand, args.slice(2))
273
273
  );
274
274
  break;
275
275
  case "project":
276
- await import("./project-OPA5UUMQ.js").then(
276
+ await import("./project-6POVRBZL.js").then(
277
277
  (m) => m.project(subcommand, args.slice(2))
278
278
  );
279
279
  break;
280
280
  // Project
281
281
  case "init":
282
- await import("./init-ZR2SLJ3K.js").then((m) => m.init(args.slice(1)));
282
+ await import("./init-I4FECTEL.js").then((m) => m.init(args.slice(1)));
283
283
  break;
284
284
  case "register":
285
- await import("./register-HRLBT4FI.js").then((m) => m.register(args.slice(1)));
285
+ await import("./register-OBQZPOGC.js").then((m) => m.register(args.slice(1)));
286
286
  break;
287
287
  case "templates":
288
288
  await import("./templates-LNUOTNLN.js").then((m) => m.templates(subcommand, args.slice(2)));
@@ -299,7 +299,7 @@ async function main() {
299
299
  break;
300
300
  // Dependencies
301
301
  case "deps":
302
- await import("./deps-IYUORT55.js").then((m) => m.deps(args.slice(1)));
302
+ await import("./deps-NHVL4DMD.js").then((m) => m.deps(args.slice(1)));
303
303
  break;
304
304
  // Feature flags (read the in-sandbox snapshot)
305
305
  case "flags":
@@ -12,7 +12,7 @@ import {
12
12
  import {
13
13
  createApiClient,
14
14
  validateProjectNameLength
15
- } from "./chunk-FHHWIV4C.js";
15
+ } from "./chunk-U3ZLIXGD.js";
16
16
  import {
17
17
  getToken,
18
18
  init_auth,
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  findFunctionsProjectRoot
3
- } from "./chunk-QPVRRJFJ.js";
3
+ } from "./chunk-FLFEF2BN.js";
4
4
  import "./chunk-PNKVD2UK.js";
5
5
 
6
6
  // src/commands/project.ts
7
7
  import { spawn } from "child_process";
8
8
  import pc from "picocolors";
9
- var RELEASE_SDK_REQUIREMENT = "lumera[release]>=0.32.0,<0.33.0";
9
+ var RELEASE_SDK_REQUIREMENT = "lumera[release]>=0.33.0,<0.34.0";
10
10
  var RELEASE_PROTOCOL_VERSION = 1;
11
11
  var RELEASE_COMMANDS = /* @__PURE__ */ new Set([
12
12
  "build",
@@ -7,7 +7,7 @@ import {
7
7
  import {
8
8
  createApiClient,
9
9
  validateProjectExternalId
10
- } from "./chunk-FHHWIV4C.js";
10
+ } from "./chunk-U3ZLIXGD.js";
11
11
  import {
12
12
  findProjectRoot,
13
13
  getAppName,
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  deploy
3
- } from "./chunk-KC3VBP6R.js";
3
+ } from "./chunk-LVIIWE44.js";
4
4
  import {
5
5
  syncDeps
6
- } from "./chunk-7ZTCXKII.js";
6
+ } from "./chunk-YMWDIAUG.js";
7
7
  import {
8
8
  loadEnv
9
9
  } from "./chunk-2CR762KB.js";
@@ -12,7 +12,7 @@ import {
12
12
  createApiClient,
13
13
  isApiErrorStatus,
14
14
  validateCollectionNameLength
15
- } from "./chunk-FHHWIV4C.js";
15
+ } from "./chunk-U3ZLIXGD.js";
16
16
  import {
17
17
  findProjectRoot,
18
18
  getApiUrl,
@@ -1287,6 +1287,8 @@ ${pc2.dim("Resources:")}
1287
1287
  hooks Plan only hooks
1288
1288
  agents Plan only agents
1289
1289
  agents/<name> Plan single agent
1290
+ skills Plan only project skills
1291
+ skills/<slug> Plan single project skill
1290
1292
  mailboxes Plan only mailboxes
1291
1293
  mailboxes/<slug> Plan single mailbox
1292
1294
  app Plan app deployment
@@ -1315,6 +1317,8 @@ ${pc2.dim("Resources:")}
1315
1317
  hooks Apply only hooks
1316
1318
  agents Apply only agents
1317
1319
  agents/<name> Apply single agent
1320
+ skills Apply only project skills
1321
+ skills/<slug> Apply single project skill
1318
1322
  mailboxes Apply only mailboxes
1319
1323
  mailboxes/<slug> Apply single mailbox
1320
1324
  app Deploy the frontend app
@@ -1359,6 +1363,8 @@ ${pc2.dim("Resources:")}
1359
1363
  hooks Pull only hooks
1360
1364
  agents Pull only agents
1361
1365
  agents/<name> Pull single agent
1366
+ skills Pull only project skills
1367
+ skills/<slug> Pull single project skill
1362
1368
  mailboxes Pull only mailboxes
1363
1369
  mailboxes/<slug> Pull single mailbox
1364
1370
 
@@ -1386,6 +1392,8 @@ ${pc2.dim("Resources:")}
1386
1392
  hooks Destroy only hooks
1387
1393
  agents Destroy only agents
1388
1394
  agents/<name> Destroy single agent
1395
+ skills Destroy only project skills
1396
+ skills/<slug> Destroy single project skill
1389
1397
  app Delete app registration
1390
1398
 
1391
1399
  ${pc2.dim("Options:")}
@@ -1412,6 +1420,7 @@ ${pc2.dim("Types:")}
1412
1420
  automations List only automations
1413
1421
  hooks List only hooks
1414
1422
  agents List only agents
1423
+ skills List only project skills
1415
1424
  mailboxes List only mailboxes
1416
1425
 
1417
1426
  ${pc2.dim("Options:")}
@@ -1436,6 +1445,7 @@ ${pc2.dim("Resources:")}
1436
1445
  automations/<name> Show automation details
1437
1446
  hooks/<name> Show hook details
1438
1447
  agents/<name> Show agent details
1448
+ skills/<slug> Show project skill details
1439
1449
  mailboxes/<slug> Show mailbox details
1440
1450
  app Show app details
1441
1451
 
@@ -1452,7 +1462,7 @@ function parseResource(resourcePath) {
1452
1462
  const parts = resourcePath.split("/");
1453
1463
  const type = parts[0];
1454
1464
  const name = parts.slice(1).join("/") || null;
1455
- const validTypes = ["collections", "automations", "hooks", "agents", "mailboxes", "app"];
1465
+ const validTypes = ["collections", "automations", "hooks", "agents", "skills", "mailboxes", "app"];
1456
1466
  if (!validTypes.includes(type)) {
1457
1467
  console.log(pc2.red(` Unknown resource type "${type}". Valid types: ${validTypes.join(", ")}`));
1458
1468
  process.exit(1);
@@ -2475,6 +2485,246 @@ ${hook.script.split("\n").map((line) => " " + line).join("\n")}
2475
2485
  console.log(pc2.green(" \u2713"), `${hook.name} \u2192 hooks/${fileName}`);
2476
2486
  }
2477
2487
  }
2488
+ var SKILL_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2489
+ var SKILL_NAME_MAX_LENGTH = 100;
2490
+ var SKILL_SLUG_MAX_LENGTH = 100;
2491
+ var SKILL_SUMMARY_MAX_LENGTH = 500;
2492
+ function loadLocalSkills(platformDir, filterName) {
2493
+ const skillsDir = join2(platformDir, "skills");
2494
+ if (!existsSync2(skillsDir)) return [];
2495
+ const skills = [];
2496
+ const errors = [];
2497
+ for (const entry of readdirSync2(skillsDir, { withFileTypes: true })) {
2498
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
2499
+ if (filterName && entry.name !== filterName) continue;
2500
+ if (!SKILL_SLUG_RE.test(entry.name)) {
2501
+ errors.push(`${entry.name}: directory name must be a lowercase slug`);
2502
+ continue;
2503
+ }
2504
+ const path = join2(skillsDir, entry.name, "SKILL.md");
2505
+ if (!existsSync2(path)) {
2506
+ errors.push(`${entry.name}: missing SKILL.md`);
2507
+ continue;
2508
+ }
2509
+ const content = readFileSync2(path, "utf-8");
2510
+ const separator = /\r?\n---\r?\n/.exec(content);
2511
+ const parts = separator ? [content.slice(0, separator.index), content.slice(separator.index + separator[0].length)] : [content];
2512
+ if (parts.length !== 2) {
2513
+ errors.push(`${entry.name}: SKILL.md must separate the summary and instructions with ---`);
2514
+ continue;
2515
+ }
2516
+ const headerLines = parts[0].trim().split(/\r?\n/);
2517
+ const titleLine = headerLines[0]?.trim().startsWith("# ") ? headerLines[0] : "";
2518
+ const name = titleLine.trim().slice(2).trim();
2519
+ const summaryLines = [];
2520
+ for (const line of headerLines.slice(1)) {
2521
+ const trimmed = line.trim();
2522
+ if (summaryLines.length === 0 && trimmed === "") continue;
2523
+ if (summaryLines.length > 0 && trimmed === "") break;
2524
+ summaryLines.push(trimmed);
2525
+ }
2526
+ const summary = summaryLines.join(" ");
2527
+ const instructions = parts[1].trim();
2528
+ if (!name) errors.push(`${entry.name}: SKILL.md must start with a # Title`);
2529
+ if (name.length > SKILL_NAME_MAX_LENGTH) errors.push(`${entry.name}: title must be at most ${SKILL_NAME_MAX_LENGTH} characters`);
2530
+ if (!summary) errors.push(`${entry.name}: SKILL.md must include a one-paragraph summary`);
2531
+ if (summary.length > SKILL_SUMMARY_MAX_LENGTH) errors.push(`${entry.name}: summary must be at most ${SKILL_SUMMARY_MAX_LENGTH} characters`);
2532
+ if (!instructions) errors.push(`${entry.name}: SKILL.md must include instructions after ---`);
2533
+ if (name && name.length <= SKILL_NAME_MAX_LENGTH && summary && summary.length <= SKILL_SUMMARY_MAX_LENGTH && instructions) {
2534
+ skills.push({ slug: entry.name, name, summary, instructions });
2535
+ }
2536
+ }
2537
+ if (errors.length > 0) {
2538
+ console.log(pc2.red(" Skill errors:"));
2539
+ for (const error3 of errors) console.log(pc2.red(` \u2717 ${error3}`));
2540
+ throw new Error(`Found ${errors.length} skill error(s)`);
2541
+ }
2542
+ return skills;
2543
+ }
2544
+ function requireProjectSkillsProjectID(projectId) {
2545
+ if (!projectId) {
2546
+ throw new Error("Project skills require lumera.project_id in package.json. Register the project before applying skills.");
2547
+ }
2548
+ return projectId;
2549
+ }
2550
+ function skillMarkdown(skill) {
2551
+ return `# ${skill.name}
2552
+
2553
+ ${skill.summary}
2554
+
2555
+ ---
2556
+
2557
+ ${skill.instructions}
2558
+ `;
2559
+ }
2560
+ function scopedProjectSkillSlug(appName, localSlug) {
2561
+ return appName ? `${appName}:${localSlug}` : localSlug;
2562
+ }
2563
+ function localProjectSkillSlug(appName, storedSlug) {
2564
+ const prefix = appName ? `${appName}:` : "";
2565
+ return prefix && storedSlug.startsWith(prefix) ? storedSlug.slice(prefix.length) : storedSlug;
2566
+ }
2567
+ function remoteSkillsByLocalSlug(remoteSkills, appName) {
2568
+ const byLocalSlug = /* @__PURE__ */ new Map();
2569
+ for (const remote of remoteSkills) {
2570
+ const localSlug = localProjectSkillSlug(appName, remote.slug);
2571
+ const existing = byLocalSlug.get(localSlug);
2572
+ if (!existing || remote.slug === scopedProjectSkillSlug(appName, localSlug)) {
2573
+ byLocalSlug.set(localSlug, remote);
2574
+ }
2575
+ }
2576
+ return byLocalSlug;
2577
+ }
2578
+ function projectSkillRefCandidates(refs, appName) {
2579
+ return [...new Set(refs.flatMap((ref) => appName && SKILL_SLUG_RE.test(ref) ? [scopedProjectSkillSlug(appName, ref), ref] : [ref]))];
2580
+ }
2581
+ async function resolveAgentSkillRefs(api, refs, projectId, appName, pendingLocalSkillSlugs = /* @__PURE__ */ new Set()) {
2582
+ const refToId = /* @__PURE__ */ new Map();
2583
+ const skillIdToSlug = /* @__PURE__ */ new Map();
2584
+ const blockedRefs = /* @__PURE__ */ new Set();
2585
+ const outsideProjectRefs = /* @__PURE__ */ new Set();
2586
+ if (refs.length === 0) return { refToId, skillIdToSlug, blockedRefs, outsideProjectRefs };
2587
+ const skills = await api.lookupAgentSkills(projectSkillRefCandidates(refs, appName));
2588
+ const byID = new Map(skills.map((skill) => [skill.id, skill]));
2589
+ const bySlug = new Map(skills.map((skill) => [skill.slug, skill]));
2590
+ const projectByLocalSlug = /* @__PURE__ */ new Map();
2591
+ for (const skill of skills) {
2592
+ const inRuntimeProject = !!projectId && skill.project_id === projectId;
2593
+ const inScope = !skill.project_id || inRuntimeProject;
2594
+ if (inScope && isAvailableToCustomAgents(skill)) {
2595
+ skillIdToSlug.set(skill.id, inRuntimeProject ? localProjectSkillSlug(appName, skill.slug) : skill.slug);
2596
+ }
2597
+ if (!inRuntimeProject) continue;
2598
+ const localSlug = localProjectSkillSlug(appName, skill.slug);
2599
+ const existing = projectByLocalSlug.get(localSlug);
2600
+ if (!existing || skill.slug === scopedProjectSkillSlug(appName, localSlug)) {
2601
+ projectByLocalSlug.set(localSlug, skill);
2602
+ }
2603
+ }
2604
+ for (const ref of refs) {
2605
+ let selected = byID.get(ref);
2606
+ if (!selected && SKILL_SLUG_RE.test(ref)) {
2607
+ selected = projectByLocalSlug.get(ref);
2608
+ if (!selected && pendingLocalSkillSlugs.has(ref)) {
2609
+ continue;
2610
+ }
2611
+ }
2612
+ selected ||= bySlug.get(ref);
2613
+ if (!selected) continue;
2614
+ if (selected.project_id && selected.project_id !== projectId) {
2615
+ outsideProjectRefs.add(ref);
2616
+ continue;
2617
+ }
2618
+ if (!isAvailableToCustomAgents(selected)) {
2619
+ blockedRefs.add(ref);
2620
+ continue;
2621
+ }
2622
+ refToId.set(ref, selected.id);
2623
+ }
2624
+ return { refToId, skillIdToSlug, blockedRefs, outsideProjectRefs };
2625
+ }
2626
+ function validateProjectSkillStorageSlugs(localSkills, appName) {
2627
+ for (const skill of localSkills) {
2628
+ const storedSlug = scopedProjectSkillSlug(appName, skill.slug);
2629
+ if (storedSlug.length > SKILL_SLUG_MAX_LENGTH) {
2630
+ throw new Error(`Project skill slug "${storedSlug}" must be at most ${SKILL_SLUG_MAX_LENGTH} characters including the project prefix`);
2631
+ }
2632
+ }
2633
+ }
2634
+ async function planSkills(api, localSkills, projectId, appName) {
2635
+ if (localSkills.length === 0) return [];
2636
+ validateProjectSkillStorageSlugs(localSkills, appName);
2637
+ const scopedProjectID = requireProjectSkillsProjectID(projectId);
2638
+ const remoteSkills = await api.listAgentSkills({ project_id: scopedProjectID });
2639
+ const remoteBySlug = remoteSkillsByLocalSlug(remoteSkills, appName);
2640
+ const changes = [];
2641
+ for (const skill of localSkills) {
2642
+ const remote = remoteBySlug.get(skill.slug);
2643
+ if (!remote) {
2644
+ changes.push({ type: "create", resource: "skill", id: skill.slug, name: skill.name });
2645
+ continue;
2646
+ }
2647
+ const changed = [];
2648
+ if (remote.name !== skill.name) changed.push("name");
2649
+ if ((remote.summary || "").trim() !== skill.summary.trim()) changed.push("summary");
2650
+ if ((remote.instructions || "").trim() !== skill.instructions.trim()) changed.push("instructions");
2651
+ if (changed.length > 0) {
2652
+ changes.push({
2653
+ type: "update",
2654
+ resource: "skill",
2655
+ id: skill.slug,
2656
+ name: skill.name,
2657
+ details: `changed: ${changed.join(", ")}`,
2658
+ textDiffs: changed.includes("instructions") || changed.includes("summary") ? [{ field: "SKILL.md", oldText: skillMarkdown({ slug: skill.slug, name: remote.name, summary: remote.summary || "", instructions: remote.instructions || "" }), newText: skillMarkdown(skill) }] : void 0
2659
+ });
2660
+ }
2661
+ }
2662
+ return changes;
2663
+ }
2664
+ async function applySkills(api, localSkills, projectId, appName) {
2665
+ if (localSkills.length === 0) return 0;
2666
+ validateProjectSkillStorageSlugs(localSkills, appName);
2667
+ const scopedProjectID = requireProjectSkillsProjectID(projectId);
2668
+ let errors = 0;
2669
+ const remoteSkills = await api.listAgentSkills({ project_id: scopedProjectID });
2670
+ const remoteBySlug = remoteSkillsByLocalSlug(remoteSkills, appName);
2671
+ for (const skill of localSkills) {
2672
+ const remote = remoteBySlug.get(skill.slug);
2673
+ try {
2674
+ if (remote) {
2675
+ await api.updateAgentSkill(remote.id, {
2676
+ name: skill.name,
2677
+ summary: skill.summary,
2678
+ instructions: skill.instructions
2679
+ });
2680
+ console.log(pc2.green(" \u2713"), `${skill.name} (updated)`);
2681
+ } else {
2682
+ await api.createAgentSkill({
2683
+ ...skill,
2684
+ slug: scopedProjectSkillSlug(appName, skill.slug),
2685
+ project_id: scopedProjectID
2686
+ });
2687
+ console.log(pc2.green(" \u2713"), `${skill.name} (created)`);
2688
+ }
2689
+ } catch (error3) {
2690
+ console.log(pc2.red(" \u2717"), `${skill.name}: ${error3}`);
2691
+ errors++;
2692
+ }
2693
+ }
2694
+ return errors;
2695
+ }
2696
+ async function pullSkills(api, platformDir, filterName, projectId, appName) {
2697
+ if (!projectId) {
2698
+ console.log(pc2.dim(" Skipped \u2014 project not registered (run `lumera register`)"));
2699
+ return;
2700
+ }
2701
+ const scopedProjectID = requireProjectSkillsProjectID(projectId);
2702
+ const skillsDir = join2(platformDir, "skills");
2703
+ const remoteSkills = remoteSkillsByLocalSlug(
2704
+ await api.listAgentSkills({ project_id: scopedProjectID }),
2705
+ appName
2706
+ );
2707
+ for (const [localSlug, skill] of remoteSkills) {
2708
+ if (!SKILL_SLUG_RE.test(localSlug)) {
2709
+ console.log(pc2.yellow(` \u26A0 Skipping ${skill.name}: skill slug ${skill.slug} cannot be represented in platform/skills`));
2710
+ continue;
2711
+ }
2712
+ if (filterName && localSlug !== filterName && skill.slug !== filterName && skill.name !== filterName) continue;
2713
+ if (!(skill.summary || "").trim() || !(skill.instructions || "").trim()) {
2714
+ console.log(pc2.yellow(` \u26A0 Skipping ${skill.name}: source-managed skills require both summary and instructions`));
2715
+ continue;
2716
+ }
2717
+ const skillDir = join2(skillsDir, localSlug);
2718
+ mkdirSync(skillDir, { recursive: true });
2719
+ writeFileSync(join2(skillDir, "SKILL.md"), skillMarkdown({
2720
+ slug: localSlug,
2721
+ name: skill.name,
2722
+ summary: skill.summary || "",
2723
+ instructions: skill.instructions || ""
2724
+ }));
2725
+ console.log(pc2.green(" \u2713"), `${skill.name} \u2192 skills/${localSlug}/`);
2726
+ }
2727
+ }
2478
2728
  function loadLocalAgents(platformDir, filterName, appName) {
2479
2729
  const agentsDir = join2(platformDir, "agents");
2480
2730
  if (!existsSync2(agentsDir)) return [];
@@ -2545,44 +2795,38 @@ function loadLocalAgents(platformDir, filterName, appName) {
2545
2795
  }
2546
2796
  return agents;
2547
2797
  }
2548
- async function planAgents(api, localAgents, projectId) {
2798
+ async function planAgents(api, localAgents, projectId, localSkillSlugs = /* @__PURE__ */ new Set(), appName) {
2549
2799
  const changes = [];
2550
2800
  const remoteAgents = await api.listAgents(projectId ? { project_id: projectId } : void 0);
2551
2801
  const remoteByExternalId = new Map(
2552
2802
  remoteAgents.filter((a) => a.external_id && !a.managed).map((a) => [a.external_id, a])
2553
2803
  );
2554
- const refToId = /* @__PURE__ */ new Map();
2555
- const skillIdToSlug = /* @__PURE__ */ new Map();
2556
- const blockedRefs = /* @__PURE__ */ new Set();
2557
2804
  const planRefs = [
2558
2805
  .../* @__PURE__ */ new Set([
2559
2806
  ...localAgents.flatMap((a) => a.agent.skills ?? []),
2560
2807
  ...remoteAgents.flatMap((a) => a.skill_ids ?? [])
2561
2808
  ])
2562
2809
  ];
2563
- if (planRefs.length > 0) {
2564
- try {
2565
- const skills = await api.lookupAgentSkills(planRefs);
2566
- for (const s of skills) {
2567
- skillIdToSlug.set(s.id, s.slug);
2568
- if (isAvailableToCustomAgents(s)) {
2569
- refToId.set(s.slug, s.id);
2570
- refToId.set(s.id, s.id);
2571
- } else {
2572
- blockedRefs.add(s.slug);
2573
- blockedRefs.add(s.id);
2574
- }
2575
- }
2576
- } catch (e) {
2577
- console.log(pc2.yellow(` \u26A0 Could not resolve skills for plan: ${e}`));
2578
- }
2810
+ let resolution = {
2811
+ refToId: /* @__PURE__ */ new Map(),
2812
+ skillIdToSlug: /* @__PURE__ */ new Map(),
2813
+ blockedRefs: /* @__PURE__ */ new Set(),
2814
+ outsideProjectRefs: /* @__PURE__ */ new Set()
2815
+ };
2816
+ try {
2817
+ resolution = await resolveAgentSkillRefs(api, planRefs, projectId, appName, localSkillSlugs);
2818
+ } catch (e) {
2819
+ console.log(pc2.yellow(` \u26A0 Could not resolve skills for plan: ${e}`));
2579
2820
  }
2821
+ const { refToId, skillIdToSlug, blockedRefs, outsideProjectRefs } = resolution;
2580
2822
  for (const { agent, systemPrompt, policyScript } of localAgents) {
2581
2823
  const remote = remoteByExternalId.get(agent.external_id);
2582
2824
  for (const ref of agent.skills ?? []) {
2583
- if (blockedRefs.has(ref)) {
2825
+ if (outsideProjectRefs.has(ref)) {
2826
+ console.log(pc2.red(` \u2717 Skill "${ref}" belongs to another project \u2014 apply will fail for this agent`));
2827
+ } else if (blockedRefs.has(ref)) {
2584
2828
  console.log(pc2.red(` \u2717 Skill "${ref}" is a Lumera platform skill, only available to Lumera-built agents \u2014 apply will fail for this agent`));
2585
- } else if (!refToId.has(ref)) {
2829
+ } else if (!refToId.has(ref) && !localSkillSlugs.has(ref)) {
2586
2830
  console.log(pc2.red(` \u2717 Skill "${ref}" not found \u2014 apply will fail for this agent`));
2587
2831
  }
2588
2832
  }
@@ -2600,12 +2844,14 @@ async function planAgents(api, localAgents, projectId) {
2600
2844
  if ((remote.policy_enabled || false) !== (agent.policy_enabled || false)) diffs.push("policy_enabled");
2601
2845
  if ((remote.policy_description || "") !== (agent.policy_description || "")) diffs.push("policy_description");
2602
2846
  const localSkillIds = (agent.skills || []).map((s) => refToId.get(s)).filter((id) => !!id).sort();
2847
+ const pendingLocalSkillSlugs = (agent.skills || []).filter((ref) => localSkillSlugs.has(ref) && !refToId.has(ref));
2603
2848
  const remoteSkillIds = [...remote.skill_ids || []].sort();
2604
- if (localSkillIds.join(",") !== remoteSkillIds.join(",")) {
2849
+ if (localSkillIds.join(",") !== remoteSkillIds.join(",") || pendingLocalSkillSlugs.length > 0) {
2605
2850
  const addedSlugs = localSkillIds.filter((id) => !remoteSkillIds.includes(id)).map((id) => skillIdToSlug.get(id) || id);
2606
2851
  const removedSlugs = remoteSkillIds.filter((id) => !localSkillIds.includes(id)).map((id) => skillIdToSlug.get(id) || id);
2607
2852
  const parts = [];
2608
2853
  if (addedSlugs.length) parts.push(`+${addedSlugs.join(", +")}`);
2854
+ if (pendingLocalSkillSlugs.length) parts.push(`+${pendingLocalSkillSlugs.join(", +")}`);
2609
2855
  if (removedSlugs.length) parts.push(`-${removedSlugs.join(", -")}`);
2610
2856
  diffs.push(`skills (${parts.join(", ")})`);
2611
2857
  }
@@ -2628,34 +2874,29 @@ function isAvailableToCustomAgents(skill) {
2628
2874
  if (!skill.managed) return true;
2629
2875
  return (skill.tags ?? []).includes(TAG_AVAILABLE_TO_CUSTOM_AGENTS);
2630
2876
  }
2631
- async function applyAgents(api, localAgents, projectId) {
2877
+ async function applyAgents(api, localAgents, projectId, localSkillSlugs = /* @__PURE__ */ new Set(), appName) {
2632
2878
  let errors = 0;
2633
2879
  const remoteAgents = await api.listAgents(projectId ? { project_id: projectId } : void 0);
2634
2880
  const remoteByExternalId = new Map(
2635
2881
  remoteAgents.filter((a) => a.external_id && !a.managed).map((a) => [a.external_id, a])
2636
2882
  );
2637
- const refToId = /* @__PURE__ */ new Map();
2638
- const blockedRefs = /* @__PURE__ */ new Set();
2639
2883
  const refs = [...new Set(localAgents.flatMap((a) => a.agent.skills ?? []))];
2640
- if (refs.length > 0) {
2641
- try {
2642
- const skills = await api.lookupAgentSkills(refs);
2643
- for (const s of skills) {
2644
- if (isAvailableToCustomAgents(s)) {
2645
- refToId.set(s.slug, s.id);
2646
- refToId.set(s.id, s.id);
2647
- } else {
2648
- blockedRefs.add(s.slug);
2649
- blockedRefs.add(s.id);
2650
- }
2651
- }
2652
- } catch (e) {
2653
- console.log(pc2.yellow(` \u26A0 Could not resolve skills: ${e}`));
2654
- }
2884
+ let resolution = {
2885
+ refToId: /* @__PURE__ */ new Map(),
2886
+ skillIdToSlug: /* @__PURE__ */ new Map(),
2887
+ blockedRefs: /* @__PURE__ */ new Set(),
2888
+ outsideProjectRefs: /* @__PURE__ */ new Set()
2889
+ };
2890
+ try {
2891
+ resolution = await resolveAgentSkillRefs(api, refs, projectId, appName, localSkillSlugs);
2892
+ } catch (e) {
2893
+ console.log(pc2.yellow(` \u26A0 Could not resolve skills: ${e}`));
2655
2894
  }
2895
+ const { refToId, blockedRefs, outsideProjectRefs } = resolution;
2656
2896
  for (const { agent, systemPrompt, policyScript } of localAgents) {
2657
2897
  const remote = remoteByExternalId.get(agent.external_id);
2658
2898
  const skillIds = [];
2899
+ const outsideProjectForAgent = [];
2659
2900
  const blockedForAgent = [];
2660
2901
  const notFoundForAgent = [];
2661
2902
  if (agent.skills) {
@@ -2663,6 +2904,8 @@ async function applyAgents(api, localAgents, projectId) {
2663
2904
  const id = refToId.get(ref);
2664
2905
  if (id) {
2665
2906
  skillIds.push(id);
2907
+ } else if (outsideProjectRefs.has(ref)) {
2908
+ outsideProjectForAgent.push(ref);
2666
2909
  } else if (blockedRefs.has(ref)) {
2667
2910
  blockedForAgent.push(ref);
2668
2911
  } else {
@@ -2670,8 +2913,11 @@ async function applyAgents(api, localAgents, projectId) {
2670
2913
  }
2671
2914
  }
2672
2915
  }
2673
- if (blockedForAgent.length > 0 || notFoundForAgent.length > 0) {
2916
+ if (outsideProjectForAgent.length > 0 || blockedForAgent.length > 0 || notFoundForAgent.length > 0) {
2674
2917
  console.log(pc2.red(" \u2717"), `${agent.name}:`);
2918
+ if (outsideProjectForAgent.length > 0) {
2919
+ console.log(pc2.red(` skills outside the agent project: ${outsideProjectForAgent.join(", ")}`));
2920
+ }
2675
2921
  if (blockedForAgent.length > 0) {
2676
2922
  console.log(pc2.red(` skills not available for custom agents: ${blockedForAgent.join(", ")}`));
2677
2923
  }
@@ -2711,7 +2957,7 @@ async function applyAgents(api, localAgents, projectId) {
2711
2957
  }
2712
2958
  return errors;
2713
2959
  }
2714
- async function pullAgents(api, platformDir, filterName, projectId) {
2960
+ async function pullAgents(api, platformDir, filterName, projectId, appName) {
2715
2961
  const agentsDir = join2(platformDir, "agents");
2716
2962
  mkdirSync(agentsDir, { recursive: true });
2717
2963
  const agents = await api.listAgents(projectId ? { project_id: projectId } : void 0);
@@ -2720,7 +2966,9 @@ async function pullAgents(api, platformDir, filterName, projectId) {
2720
2966
  if (pullRefs.length > 0) {
2721
2967
  try {
2722
2968
  const skills = await api.lookupAgentSkills(pullRefs);
2723
- for (const s of skills) skillIdToSlug.set(s.id, s.slug);
2969
+ for (const s of skills) {
2970
+ skillIdToSlug.set(s.id, s.project_id === projectId ? localProjectSkillSlug(appName, s.slug) : s.slug);
2971
+ }
2724
2972
  } catch (e) {
2725
2973
  console.log(pc2.yellow(` \u26A0 Could not resolve skills for pull: ${e}`));
2726
2974
  console.log(pc2.yellow(` Pulled configs may omit skills \u2014 review before re-applying.`));
@@ -2875,6 +3123,28 @@ async function listResources(api, platformDir, filterType, appName, projectId) {
2875
3123
  }
2876
3124
  }
2877
3125
  }
3126
+ if (!filterType || filterType === "skills") {
3127
+ const localSkills = loadLocalSkills(platformDir);
3128
+ if (localSkills.length > 0) requireProjectSkillsProjectID(projectId);
3129
+ const remoteSkills = projectId ? await api.listAgentSkills({ project_id: projectId }) : [];
3130
+ const remoteBySlug = remoteSkillsByLocalSlug(remoteSkills, appName);
3131
+ const localSlugs = new Set(localSkills.map((skill) => skill.slug));
3132
+ for (const skill of localSkills) {
3133
+ const remote = remoteBySlug.get(skill.slug);
3134
+ if (!remote) {
3135
+ results.push({ name: skill.name, type: "skills", status: "local-only" });
3136
+ } else if (remote.name !== skill.name || (remote.summary || "").trim() !== skill.summary.trim() || (remote.instructions || "").trim() !== skill.instructions.trim()) {
3137
+ results.push({ name: skill.name, type: "skills", status: "changed", details: "SKILL.md" });
3138
+ } else {
3139
+ results.push({ name: skill.name, type: "skills", status: "synced" });
3140
+ }
3141
+ }
3142
+ for (const remote of remoteSkills) {
3143
+ if (!localSlugs.has(localProjectSkillSlug(appName, remote.slug))) {
3144
+ results.push({ name: remote.name, type: "skills", status: "remote-only" });
3145
+ }
3146
+ }
3147
+ }
2878
3148
  if (!filterType || filterType === "mailboxes") {
2879
3149
  const localMailboxes = loadLocalMailboxes(platformDir);
2880
3150
  const remoteMailboxes = await api.listMailboxes();
@@ -3022,6 +3292,20 @@ async function destroyResources(api, platformDir, resourceType, resourceName, sk
3022
3292
  } catch {
3023
3293
  }
3024
3294
  }
3295
+ if (!resourceType || resourceType === "skills") {
3296
+ const localSkills = loadLocalSkills(platformDir, resourceName || void 0);
3297
+ if (localSkills.length > 0) {
3298
+ const scopedProjectID = requireProjectSkillsProjectID(projectId);
3299
+ const remoteSkills = await api.listAgentSkills({ project_id: scopedProjectID });
3300
+ const remoteBySlug = remoteSkillsByLocalSlug(remoteSkills, appName);
3301
+ for (const skill of localSkills) {
3302
+ const remote = remoteBySlug.get(skill.slug);
3303
+ if (remote) {
3304
+ toDelete.push({ type: "skill", id: skill.slug, name: skill.name, remoteId: remote.id });
3305
+ }
3306
+ }
3307
+ }
3308
+ }
3025
3309
  if (toDelete.length === 0) {
3026
3310
  console.log(pc2.green(" \u2713 No resources found to delete"));
3027
3311
  return;
@@ -3045,6 +3329,7 @@ async function destroyResources(api, platformDir, resourceType, resourceName, sk
3045
3329
  }
3046
3330
  }
3047
3331
  const agents = toDelete.filter((r) => r.type === "agent");
3332
+ const skills = toDelete.filter((r) => r.type === "skill");
3048
3333
  const hooks = toDelete.filter((r) => r.type === "hook");
3049
3334
  const automations = toDelete.filter((r) => r.type === "automation");
3050
3335
  const collections = toDelete.filter((r) => r.type === "collection");
@@ -3058,6 +3343,15 @@ async function destroyResources(api, platformDir, resourceType, resourceName, sk
3058
3343
  errors++;
3059
3344
  }
3060
3345
  }
3346
+ for (const resource of skills) {
3347
+ try {
3348
+ await api.deleteAgentSkill(resource.remoteId);
3349
+ console.log(pc2.green(" \u2713"), `Deleted skill: ${resource.name}`);
3350
+ } catch (e) {
3351
+ console.log(pc2.red(" \u2717"), `Failed to delete skill ${resource.name}: ${e}`);
3352
+ errors++;
3353
+ }
3354
+ }
3061
3355
  for (const resource of hooks) {
3062
3356
  try {
3063
3357
  await api.deleteHook(resource.remoteId);
@@ -3314,6 +3608,31 @@ async function showResource(api, platformDir, resourceType, resourceName, appNam
3314
3608
  console.log(` Skills: ${skills.join(", ")}`);
3315
3609
  }
3316
3610
  console.log();
3611
+ } else if (resourceType === "skills") {
3612
+ const localSkills = loadLocalSkills(platformDir, resourceName);
3613
+ if (localSkills.length > 0) requireProjectSkillsProjectID(projectId);
3614
+ const remoteSkills = projectId ? await api.listAgentSkills({ project_id: projectId }) : [];
3615
+ const local = localSkills[0];
3616
+ const remote = remoteSkillsByLocalSlug(remoteSkills, appName).get(resourceName) || remoteSkills.find((skill) => skill.name === resourceName);
3617
+ if (!local && !remote) {
3618
+ console.log(pc2.red(` Skill "${resourceName}" not found`));
3619
+ process.exit(1);
3620
+ }
3621
+ console.log();
3622
+ console.log(pc2.bold(` Skill: ${local?.name || remote?.name}`));
3623
+ console.log();
3624
+ if (local && remote) {
3625
+ const changed = local.name !== remote.name || local.summary.trim() !== (remote.summary || "").trim() || local.instructions.trim() !== (remote.instructions || "").trim();
3626
+ console.log(` Status: ${changed ? pc2.yellow("changed") : pc2.green("synced")}`);
3627
+ } else if (local) {
3628
+ console.log(` Status: ${pc2.yellow("local only")}`);
3629
+ } else {
3630
+ console.log(` Status: ${pc2.cyan("remote only")}`);
3631
+ }
3632
+ console.log(` Slug: ${local?.slug || (remote ? localProjectSkillSlug(appName, remote.slug) : "")}`);
3633
+ const summary = local?.summary || remote?.summary;
3634
+ if (summary) console.log(` Summary: ${summary}`);
3635
+ console.log();
3317
3636
  } else if (resourceType === "mailboxes") {
3318
3637
  const localMailboxes = loadLocalMailboxes(platformDir, resourceName);
3319
3638
  const remoteMailboxes = await api.listMailboxes();
@@ -3430,10 +3749,15 @@ async function plan(args) {
3430
3749
  allChanges.push(...changes);
3431
3750
  }
3432
3751
  }
3752
+ const localSkills = !type || type === "skills" ? loadLocalSkills(platformDir, name || void 0) : [];
3753
+ const localSkillSlugs = new Set(!type || type === "skills" || type === "agents" ? (localSkills.length > 0 || type !== "agents" ? localSkills : loadLocalSkills(platformDir)).map((skill) => skill.slug) : []);
3754
+ if (localSkills.length > 0) {
3755
+ allChanges.push(...await planSkills(api, localSkills, projectId, appName));
3756
+ }
3433
3757
  if (!type || type === "agents") {
3434
3758
  const localAgents = loadLocalAgents(platformDir, name || void 0, appName);
3435
3759
  if (localAgents.length > 0) {
3436
- const changes = await planAgents(api, localAgents, projectId);
3760
+ const changes = await planAgents(api, localAgents, projectId, localSkillSlugs, appName);
3437
3761
  allChanges.push(...changes);
3438
3762
  }
3439
3763
  }
@@ -3556,15 +3880,18 @@ async function apply(args) {
3556
3880
  const localCollections = !type || type === "collections" ? loadLocalCollections(platformDir, name || void 0) : [];
3557
3881
  const localAutomations = !type || type === "automations" ? loadLocalAutomations(platformDir, name || void 0, appName) : [];
3558
3882
  const localHooks = !type || type === "hooks" ? loadLocalHooks(platformDir, name || void 0, appName) : [];
3883
+ const localSkills = !type || type === "skills" ? loadLocalSkills(platformDir, name || void 0) : [];
3884
+ const localSkillSlugs = new Set(!type || type === "skills" || type === "agents" ? (localSkills.length > 0 || type !== "agents" ? localSkills : loadLocalSkills(platformDir)).map((skill) => skill.slug) : []);
3559
3885
  const localAgents = !type || type === "agents" ? loadLocalAgents(platformDir, name || void 0, appName) : [];
3560
3886
  const localMailboxes = !type || type === "mailboxes" ? loadLocalMailboxes(platformDir, name || void 0) : [];
3561
3887
  if (localCollections.length > 0) allChanges.push(...await planCollections(api, localCollections, appName, projectId));
3562
3888
  if (localAutomations.length > 0) allChanges.push(...await planAutomations(api, localAutomations, projectId));
3563
3889
  if (localHooks.length > 0) allChanges.push(...await planHooks(api, localHooks, collections, projectId));
3564
- if (localAgents.length > 0) allChanges.push(...await planAgents(api, localAgents, projectId));
3890
+ if (localSkills.length > 0) allChanges.push(...await planSkills(api, localSkills, projectId, appName));
3891
+ if (localAgents.length > 0) allChanges.push(...await planAgents(api, localAgents, projectId, localSkillSlugs, appName));
3565
3892
  if (localMailboxes.length > 0) allChanges.push(...await planMailboxes(api, localMailboxes));
3566
3893
  if (name) {
3567
- const hasLocal = localCollections.length > 0 || localAutomations.length > 0 || localHooks.length > 0 || localAgents.length > 0 || localMailboxes.length > 0;
3894
+ const hasLocal = localCollections.length > 0 || localAutomations.length > 0 || localHooks.length > 0 || localSkills.length > 0 || localAgents.length > 0 || localMailboxes.length > 0;
3568
3895
  if (!hasLocal) {
3569
3896
  console.log();
3570
3897
  console.log(pc2.red(` Resource "${name}" not found locally`));
@@ -3651,9 +3978,14 @@ async function apply(args) {
3651
3978
  totalErrors += await applyHooks(api, localHooks, collections, projectId);
3652
3979
  console.log();
3653
3980
  }
3981
+ if (localSkills.length > 0) {
3982
+ console.log(pc2.bold(" Skills:"));
3983
+ totalErrors += await applySkills(api, localSkills, projectId, appName);
3984
+ console.log();
3985
+ }
3654
3986
  if (localAgents.length > 0) {
3655
3987
  console.log(pc2.bold(" Agents:"));
3656
- totalErrors += await applyAgents(api, localAgents, projectId);
3988
+ totalErrors += await applyAgents(api, localAgents, projectId, localSkillSlugs, appName);
3657
3989
  console.log();
3658
3990
  }
3659
3991
  if (localMailboxes.length > 0) {
@@ -3706,10 +4038,25 @@ async function pull(args) {
3706
4038
  } catch {
3707
4039
  collections = /* @__PURE__ */ new Map();
3708
4040
  }
4041
+ if (projectId && (!type || type === "skills")) {
4042
+ const localSkills = loadLocalSkills(platformDir, name || void 0);
4043
+ if (localSkills.length > 0) {
4044
+ const changes = await planSkills(api, localSkills, projectId, appName);
4045
+ for (const change of changes) {
4046
+ if (change.type === "update") conflicts.push(`skills/${change.id}`);
4047
+ }
4048
+ }
4049
+ }
3709
4050
  if (!type || type === "agents") {
3710
4051
  const localAgents = loadLocalAgents(platformDir, name || void 0, appName);
3711
4052
  if (localAgents.length > 0) {
3712
- const changes = await planAgents(api, localAgents, projectId);
4053
+ const changes = await planAgents(
4054
+ api,
4055
+ localAgents,
4056
+ projectId,
4057
+ new Set(loadLocalSkills(platformDir).map((skill) => skill.slug)),
4058
+ appName
4059
+ );
3713
4060
  for (const c of changes) {
3714
4061
  if (c.type === "update") conflicts.push(`agents/${c.name}`);
3715
4062
  }
@@ -3770,9 +4117,14 @@ async function pull(args) {
3770
4117
  await pullHooks(api, platformDir, name || void 0, appName, projectId);
3771
4118
  console.log();
3772
4119
  }
4120
+ if (!type || type === "skills") {
4121
+ console.log(pc2.bold(" Skills:"));
4122
+ await pullSkills(api, platformDir, name || void 0, projectId, appName);
4123
+ console.log();
4124
+ }
3773
4125
  if (!type || type === "agents") {
3774
4126
  console.log(pc2.bold(" Agents:"));
3775
- await pullAgents(api, platformDir, name || void 0, projectId);
4127
+ await pullAgents(api, platformDir, name || void 0, projectId, appName);
3776
4128
  console.log();
3777
4129
  }
3778
4130
  if (!type || type === "mailboxes") {
@@ -3924,6 +4276,7 @@ ${pc2.dim("Usage:")}
3924
4276
 
3925
4277
  ${pc2.dim("Resources:")}
3926
4278
  agents/<name> Diff agent (system_prompt, policy_script)
4279
+ skills/<slug> Diff project skill instructions
3927
4280
  automations/<name> Diff automation code
3928
4281
  hooks/<name> Diff hook script
3929
4282
 
@@ -4072,23 +4425,70 @@ async function diff(args) {
4072
4425
  } else if (scriptChanged) {
4073
4426
  renderFullDiff(local.fileName, remote.script || "", local.script);
4074
4427
  }
4428
+ } else if (type === "skills") {
4429
+ const localSkills = loadLocalSkills(platformDir, name);
4430
+ if (localSkills.length > 0) requireProjectSkillsProjectID(projectId);
4431
+ const remoteSkills = projectId ? await api.listAgentSkills({ project_id: projectId }) : [];
4432
+ const local = localSkills[0];
4433
+ const remote = remoteSkillsByLocalSlug(remoteSkills, appName).get(name) || remoteSkills.find((skill) => skill.name === name);
4434
+ if (!local && !remote) {
4435
+ console.log(pc2.red(` Skill "${name}" not found locally or remotely`));
4436
+ process.exit(1);
4437
+ }
4438
+ if (!local) {
4439
+ console.log(pc2.cyan(` Skill "${name}" exists only remotely`));
4440
+ process.exit(0);
4441
+ }
4442
+ if (!remote) {
4443
+ console.log(pc2.yellow(` Skill "${name}" exists only locally (not yet deployed)`));
4444
+ process.exit(0);
4445
+ }
4446
+ console.log(pc2.bold(` Skill: ${local.name}`));
4447
+ console.log();
4448
+ if (remote.name !== local.name) {
4449
+ console.log(` name: ${pc2.red(remote.name)} \u2192 ${pc2.green(local.name)}`);
4450
+ }
4451
+ if ((remote.summary || "").trim() !== local.summary.trim()) {
4452
+ console.log(` summary: ${pc2.red(remote.summary || "(empty)")} \u2192 ${pc2.green(local.summary)}`);
4453
+ }
4454
+ const remoteMarkdown = skillMarkdown({
4455
+ slug: localProjectSkillSlug(appName, remote.slug),
4456
+ name: remote.name,
4457
+ summary: remote.summary || "",
4458
+ instructions: remote.instructions || ""
4459
+ });
4460
+ const localMarkdown = skillMarkdown(local);
4461
+ if (remoteMarkdown === localMarkdown) {
4462
+ console.log(pc2.green(" \u2713 No changes"));
4463
+ } else {
4464
+ renderFullDiff("SKILL.md", remoteMarkdown, localMarkdown);
4465
+ }
4075
4466
  } else {
4076
- console.log(pc2.red(` Diff not supported for "${type}" \u2014 use agents, automations, or hooks`));
4467
+ console.log(pc2.red(` Diff not supported for "${type}" \u2014 use agents, skills, automations, or hooks`));
4077
4468
  process.exit(1);
4078
4469
  }
4079
4470
  console.log();
4080
4471
  }
4081
4472
  export {
4082
4473
  apply,
4474
+ applyAgents,
4083
4475
  applyCollections,
4476
+ applySkills,
4084
4477
  convertCollectionToApiFormat,
4085
4478
  destroy,
4086
4479
  diff,
4087
4480
  list,
4088
4481
  loadLocalCollections,
4482
+ loadLocalSkills,
4483
+ localProjectSkillSlug,
4089
4484
  plan,
4090
4485
  planCollections,
4486
+ planSkills,
4487
+ projectSkillRefCandidates,
4091
4488
  pull,
4092
4489
  pullCollections,
4490
+ pullSkills,
4491
+ resolveAgentSkillRefs,
4492
+ scopedProjectSkillSlug,
4093
4493
  show
4094
4494
  };
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-2CR762KB.js";
4
4
  import {
5
5
  createApiClient
6
- } from "./chunk-FHHWIV4C.js";
6
+ } from "./chunk-U3ZLIXGD.js";
7
7
  import {
8
8
  findProjectRoot,
9
9
  getApiUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumerahq/cli",
3
- "version": "0.29.1",
3
+ "version": "0.30.0-dev.0",
4
4
  "description": "CLI for building and deploying Lumera apps",
5
5
  "type": "module",
6
6
  "engines": {
File without changes
@@ -4,7 +4,7 @@ version = "0.1.0"
4
4
  description = "{{projectTitle}} - Lumera custom app"
5
5
  requires-python = ">=3.11"
6
6
  dependencies = [
7
- "lumera[functions]>=0.32.0,<0.33.0",
7
+ "lumera[functions]>=0.33.0,<0.34.0",
8
8
  ]
9
9
 
10
10
  [project.optional-dependencies]