@lumerahq/cli 0.29.1 → 0.30.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-5YWEX6DG.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-5YWEX6DG.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-5YWEX6DG.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-5YWEX6DG.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-5YWEX6DG.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-5YWEX6DG.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-5YWEX6DG.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, localSourceSkillSlugs = /* @__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 && localSourceSkillSlugs.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,45 +2795,47 @@ 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, localSourceSkillSlugs = /* @__PURE__ */ new Set(), pendingLocalSkillSlugs = /* @__PURE__ */ new Set(), appName) {
2549
2799
  const changes = [];
2800
+ let validationErrors = 0;
2550
2801
  const remoteAgents = await api.listAgents(projectId ? { project_id: projectId } : void 0);
2551
2802
  const remoteByExternalId = new Map(
2552
2803
  remoteAgents.filter((a) => a.external_id && !a.managed).map((a) => [a.external_id, a])
2553
2804
  );
2554
- const refToId = /* @__PURE__ */ new Map();
2555
- const skillIdToSlug = /* @__PURE__ */ new Map();
2556
- const blockedRefs = /* @__PURE__ */ new Set();
2557
2805
  const planRefs = [
2558
2806
  .../* @__PURE__ */ new Set([
2559
2807
  ...localAgents.flatMap((a) => a.agent.skills ?? []),
2560
2808
  ...remoteAgents.flatMap((a) => a.skill_ids ?? [])
2561
2809
  ])
2562
2810
  ];
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
- }
2811
+ let resolution = {
2812
+ refToId: /* @__PURE__ */ new Map(),
2813
+ skillIdToSlug: /* @__PURE__ */ new Map(),
2814
+ blockedRefs: /* @__PURE__ */ new Set(),
2815
+ outsideProjectRefs: /* @__PURE__ */ new Set()
2816
+ };
2817
+ try {
2818
+ resolution = await resolveAgentSkillRefs(api, planRefs, projectId, appName, localSourceSkillSlugs);
2819
+ } catch (e) {
2820
+ console.log(pc2.yellow(` \u26A0 Could not resolve skills for plan: ${e}`));
2579
2821
  }
2822
+ const { refToId, skillIdToSlug, blockedRefs, outsideProjectRefs } = resolution;
2580
2823
  for (const { agent, systemPrompt, policyScript } of localAgents) {
2581
2824
  const remote = remoteByExternalId.get(agent.external_id);
2582
2825
  for (const ref of agent.skills ?? []) {
2583
- if (blockedRefs.has(ref)) {
2826
+ if (outsideProjectRefs.has(ref)) {
2827
+ console.log(pc2.red(` \u2717 Skill "${ref}" belongs to another project \u2014 apply will fail for this agent`));
2828
+ validationErrors++;
2829
+ } else if (blockedRefs.has(ref)) {
2584
2830
  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)) {
2586
- console.log(pc2.red(` \u2717 Skill "${ref}" not found \u2014 apply will fail for this agent`));
2831
+ validationErrors++;
2832
+ } else if (!refToId.has(ref) && !pendingLocalSkillSlugs.has(ref)) {
2833
+ if (localSourceSkillSlugs.has(ref)) {
2834
+ console.log(pc2.red(` \u2717 Skill "${ref}" exists in project source but is not deployed \u2014 run 'lumera apply skills' first or use 'lumera apply'`));
2835
+ } else {
2836
+ console.log(pc2.red(` \u2717 Skill "${ref}" not found \u2014 apply will fail for this agent`));
2837
+ }
2838
+ validationErrors++;
2587
2839
  }
2588
2840
  }
2589
2841
  if (!remote) {
@@ -2600,12 +2852,14 @@ async function planAgents(api, localAgents, projectId) {
2600
2852
  if ((remote.policy_enabled || false) !== (agent.policy_enabled || false)) diffs.push("policy_enabled");
2601
2853
  if ((remote.policy_description || "") !== (agent.policy_description || "")) diffs.push("policy_description");
2602
2854
  const localSkillIds = (agent.skills || []).map((s) => refToId.get(s)).filter((id) => !!id).sort();
2855
+ const pendingSkillSlugs = (agent.skills || []).filter((ref) => pendingLocalSkillSlugs.has(ref) && !refToId.has(ref));
2603
2856
  const remoteSkillIds = [...remote.skill_ids || []].sort();
2604
- if (localSkillIds.join(",") !== remoteSkillIds.join(",")) {
2857
+ if (localSkillIds.join(",") !== remoteSkillIds.join(",") || pendingSkillSlugs.length > 0) {
2605
2858
  const addedSlugs = localSkillIds.filter((id) => !remoteSkillIds.includes(id)).map((id) => skillIdToSlug.get(id) || id);
2606
2859
  const removedSlugs = remoteSkillIds.filter((id) => !localSkillIds.includes(id)).map((id) => skillIdToSlug.get(id) || id);
2607
2860
  const parts = [];
2608
2861
  if (addedSlugs.length) parts.push(`+${addedSlugs.join(", +")}`);
2862
+ if (pendingSkillSlugs.length) parts.push(`+${pendingSkillSlugs.join(", +")}`);
2609
2863
  if (removedSlugs.length) parts.push(`-${removedSlugs.join(", -")}`);
2610
2864
  diffs.push(`skills (${parts.join(", ")})`);
2611
2865
  }
@@ -2621,64 +2875,71 @@ async function planAgents(api, localAgents, projectId) {
2621
2875
  }
2622
2876
  }
2623
2877
  }
2624
- return changes;
2878
+ return { changes, validationErrors };
2625
2879
  }
2626
2880
  var TAG_AVAILABLE_TO_CUSTOM_AGENTS = "available_to_custom_agents";
2627
2881
  function isAvailableToCustomAgents(skill) {
2628
2882
  if (!skill.managed) return true;
2629
2883
  return (skill.tags ?? []).includes(TAG_AVAILABLE_TO_CUSTOM_AGENTS);
2630
2884
  }
2631
- async function applyAgents(api, localAgents, projectId) {
2885
+ async function applyAgents(api, localAgents, projectId, localSourceSkillSlugs = /* @__PURE__ */ new Set(), appName) {
2632
2886
  let errors = 0;
2633
2887
  const remoteAgents = await api.listAgents(projectId ? { project_id: projectId } : void 0);
2634
2888
  const remoteByExternalId = new Map(
2635
2889
  remoteAgents.filter((a) => a.external_id && !a.managed).map((a) => [a.external_id, a])
2636
2890
  );
2637
- const refToId = /* @__PURE__ */ new Map();
2638
- const blockedRefs = /* @__PURE__ */ new Set();
2639
2891
  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
- }
2892
+ let resolution = {
2893
+ refToId: /* @__PURE__ */ new Map(),
2894
+ skillIdToSlug: /* @__PURE__ */ new Map(),
2895
+ blockedRefs: /* @__PURE__ */ new Set(),
2896
+ outsideProjectRefs: /* @__PURE__ */ new Set()
2897
+ };
2898
+ try {
2899
+ resolution = await resolveAgentSkillRefs(api, refs, projectId, appName, localSourceSkillSlugs);
2900
+ } catch (e) {
2901
+ console.log(pc2.yellow(` \u26A0 Could not resolve skills: ${e}`));
2655
2902
  }
2903
+ const { refToId, blockedRefs, outsideProjectRefs } = resolution;
2656
2904
  for (const { agent, systemPrompt, policyScript } of localAgents) {
2657
2905
  const remote = remoteByExternalId.get(agent.external_id);
2658
2906
  const skillIds = [];
2907
+ const outsideProjectForAgent = [];
2659
2908
  const blockedForAgent = [];
2909
+ const undeployedLocalForAgent = [];
2660
2910
  const notFoundForAgent = [];
2661
2911
  if (agent.skills) {
2662
2912
  for (const ref of agent.skills) {
2663
2913
  const id = refToId.get(ref);
2664
2914
  if (id) {
2665
2915
  skillIds.push(id);
2916
+ } else if (outsideProjectRefs.has(ref)) {
2917
+ outsideProjectForAgent.push(ref);
2666
2918
  } else if (blockedRefs.has(ref)) {
2667
2919
  blockedForAgent.push(ref);
2920
+ } else if (localSourceSkillSlugs.has(ref)) {
2921
+ undeployedLocalForAgent.push(ref);
2668
2922
  } else {
2669
2923
  notFoundForAgent.push(ref);
2670
2924
  }
2671
2925
  }
2672
2926
  }
2673
- if (blockedForAgent.length > 0 || notFoundForAgent.length > 0) {
2927
+ if (outsideProjectForAgent.length > 0 || blockedForAgent.length > 0 || undeployedLocalForAgent.length > 0 || notFoundForAgent.length > 0) {
2674
2928
  console.log(pc2.red(" \u2717"), `${agent.name}:`);
2929
+ if (outsideProjectForAgent.length > 0) {
2930
+ console.log(pc2.red(` skills outside the agent project: ${outsideProjectForAgent.join(", ")}`));
2931
+ }
2675
2932
  if (blockedForAgent.length > 0) {
2676
2933
  console.log(pc2.red(` skills not available for custom agents: ${blockedForAgent.join(", ")}`));
2677
2934
  }
2935
+ if (undeployedLocalForAgent.length > 0) {
2936
+ console.log(pc2.red(` locally authored skills not deployed: ${undeployedLocalForAgent.join(", ")}`));
2937
+ console.log(pc2.dim(` run 'lumera apply skills' first, or use 'lumera apply' to deploy skills and agents together`));
2938
+ }
2678
2939
  if (notFoundForAgent.length > 0) {
2679
2940
  console.log(pc2.red(` skills not found: ${notFoundForAgent.join(", ")}`));
2941
+ console.log(pc2.dim(` pick skills from GET /api/skills (tags includes "available_to_custom_agents")`));
2680
2942
  }
2681
- console.log(pc2.dim(` pick skills from GET /api/skills (tags includes "available_to_custom_agents")`));
2682
2943
  errors++;
2683
2944
  continue;
2684
2945
  }
@@ -2711,7 +2972,7 @@ async function applyAgents(api, localAgents, projectId) {
2711
2972
  }
2712
2973
  return errors;
2713
2974
  }
2714
- async function pullAgents(api, platformDir, filterName, projectId) {
2975
+ async function pullAgents(api, platformDir, filterName, projectId, appName) {
2715
2976
  const agentsDir = join2(platformDir, "agents");
2716
2977
  mkdirSync(agentsDir, { recursive: true });
2717
2978
  const agents = await api.listAgents(projectId ? { project_id: projectId } : void 0);
@@ -2720,7 +2981,9 @@ async function pullAgents(api, platformDir, filterName, projectId) {
2720
2981
  if (pullRefs.length > 0) {
2721
2982
  try {
2722
2983
  const skills = await api.lookupAgentSkills(pullRefs);
2723
- for (const s of skills) skillIdToSlug.set(s.id, s.slug);
2984
+ for (const s of skills) {
2985
+ skillIdToSlug.set(s.id, s.project_id === projectId ? localProjectSkillSlug(appName, s.slug) : s.slug);
2986
+ }
2724
2987
  } catch (e) {
2725
2988
  console.log(pc2.yellow(` \u26A0 Could not resolve skills for pull: ${e}`));
2726
2989
  console.log(pc2.yellow(` Pulled configs may omit skills \u2014 review before re-applying.`));
@@ -2875,6 +3138,28 @@ async function listResources(api, platformDir, filterType, appName, projectId) {
2875
3138
  }
2876
3139
  }
2877
3140
  }
3141
+ if (!filterType || filterType === "skills") {
3142
+ const localSkills = loadLocalSkills(platformDir);
3143
+ if (localSkills.length > 0) requireProjectSkillsProjectID(projectId);
3144
+ const remoteSkills = projectId ? await api.listAgentSkills({ project_id: projectId }) : [];
3145
+ const remoteBySlug = remoteSkillsByLocalSlug(remoteSkills, appName);
3146
+ const localSlugs = new Set(localSkills.map((skill) => skill.slug));
3147
+ for (const skill of localSkills) {
3148
+ const remote = remoteBySlug.get(skill.slug);
3149
+ if (!remote) {
3150
+ results.push({ name: skill.name, type: "skills", status: "local-only" });
3151
+ } else if (remote.name !== skill.name || (remote.summary || "").trim() !== skill.summary.trim() || (remote.instructions || "").trim() !== skill.instructions.trim()) {
3152
+ results.push({ name: skill.name, type: "skills", status: "changed", details: "SKILL.md" });
3153
+ } else {
3154
+ results.push({ name: skill.name, type: "skills", status: "synced" });
3155
+ }
3156
+ }
3157
+ for (const remote of remoteSkills) {
3158
+ if (!localSlugs.has(localProjectSkillSlug(appName, remote.slug))) {
3159
+ results.push({ name: remote.name, type: "skills", status: "remote-only" });
3160
+ }
3161
+ }
3162
+ }
2878
3163
  if (!filterType || filterType === "mailboxes") {
2879
3164
  const localMailboxes = loadLocalMailboxes(platformDir);
2880
3165
  const remoteMailboxes = await api.listMailboxes();
@@ -3022,6 +3307,20 @@ async function destroyResources(api, platformDir, resourceType, resourceName, sk
3022
3307
  } catch {
3023
3308
  }
3024
3309
  }
3310
+ if (!resourceType || resourceType === "skills") {
3311
+ const localSkills = loadLocalSkills(platformDir, resourceName || void 0);
3312
+ if (localSkills.length > 0) {
3313
+ const scopedProjectID = requireProjectSkillsProjectID(projectId);
3314
+ const remoteSkills = await api.listAgentSkills({ project_id: scopedProjectID });
3315
+ const remoteBySlug = remoteSkillsByLocalSlug(remoteSkills, appName);
3316
+ for (const skill of localSkills) {
3317
+ const remote = remoteBySlug.get(skill.slug);
3318
+ if (remote) {
3319
+ toDelete.push({ type: "skill", id: skill.slug, name: skill.name, remoteId: remote.id });
3320
+ }
3321
+ }
3322
+ }
3323
+ }
3025
3324
  if (toDelete.length === 0) {
3026
3325
  console.log(pc2.green(" \u2713 No resources found to delete"));
3027
3326
  return;
@@ -3045,6 +3344,7 @@ async function destroyResources(api, platformDir, resourceType, resourceName, sk
3045
3344
  }
3046
3345
  }
3047
3346
  const agents = toDelete.filter((r) => r.type === "agent");
3347
+ const skills = toDelete.filter((r) => r.type === "skill");
3048
3348
  const hooks = toDelete.filter((r) => r.type === "hook");
3049
3349
  const automations = toDelete.filter((r) => r.type === "automation");
3050
3350
  const collections = toDelete.filter((r) => r.type === "collection");
@@ -3058,6 +3358,15 @@ async function destroyResources(api, platformDir, resourceType, resourceName, sk
3058
3358
  errors++;
3059
3359
  }
3060
3360
  }
3361
+ for (const resource of skills) {
3362
+ try {
3363
+ await api.deleteAgentSkill(resource.remoteId);
3364
+ console.log(pc2.green(" \u2713"), `Deleted skill: ${resource.name}`);
3365
+ } catch (e) {
3366
+ console.log(pc2.red(" \u2717"), `Failed to delete skill ${resource.name}: ${e}`);
3367
+ errors++;
3368
+ }
3369
+ }
3061
3370
  for (const resource of hooks) {
3062
3371
  try {
3063
3372
  await api.deleteHook(resource.remoteId);
@@ -3314,6 +3623,31 @@ async function showResource(api, platformDir, resourceType, resourceName, appNam
3314
3623
  console.log(` Skills: ${skills.join(", ")}`);
3315
3624
  }
3316
3625
  console.log();
3626
+ } else if (resourceType === "skills") {
3627
+ const localSkills = loadLocalSkills(platformDir, resourceName);
3628
+ if (localSkills.length > 0) requireProjectSkillsProjectID(projectId);
3629
+ const remoteSkills = projectId ? await api.listAgentSkills({ project_id: projectId }) : [];
3630
+ const local = localSkills[0];
3631
+ const remote = remoteSkillsByLocalSlug(remoteSkills, appName).get(resourceName) || remoteSkills.find((skill) => skill.name === resourceName);
3632
+ if (!local && !remote) {
3633
+ console.log(pc2.red(` Skill "${resourceName}" not found`));
3634
+ process.exit(1);
3635
+ }
3636
+ console.log();
3637
+ console.log(pc2.bold(` Skill: ${local?.name || remote?.name}`));
3638
+ console.log();
3639
+ if (local && remote) {
3640
+ const changed = local.name !== remote.name || local.summary.trim() !== (remote.summary || "").trim() || local.instructions.trim() !== (remote.instructions || "").trim();
3641
+ console.log(` Status: ${changed ? pc2.yellow("changed") : pc2.green("synced")}`);
3642
+ } else if (local) {
3643
+ console.log(` Status: ${pc2.yellow("local only")}`);
3644
+ } else {
3645
+ console.log(` Status: ${pc2.cyan("remote only")}`);
3646
+ }
3647
+ console.log(` Slug: ${local?.slug || (remote ? localProjectSkillSlug(appName, remote.slug) : "")}`);
3648
+ const summary = local?.summary || remote?.summary;
3649
+ if (summary) console.log(` Summary: ${summary}`);
3650
+ console.log();
3317
3651
  } else if (resourceType === "mailboxes") {
3318
3652
  const localMailboxes = loadLocalMailboxes(platformDir, resourceName);
3319
3653
  const remoteMailboxes = await api.listMailboxes();
@@ -3401,6 +3735,7 @@ async function plan(args) {
3401
3735
  }
3402
3736
  await syncDeps(projectRoot, { ignorePermissionDenied: true });
3403
3737
  const allChanges = [];
3738
+ let planValidationErrors = 0;
3404
3739
  let collections;
3405
3740
  try {
3406
3741
  const remoteCollections = await api.listCollections();
@@ -3430,11 +3765,19 @@ async function plan(args) {
3430
3765
  allChanges.push(...changes);
3431
3766
  }
3432
3767
  }
3768
+ const localSkills = !type || type === "skills" ? loadLocalSkills(platformDir, name || void 0) : [];
3769
+ const sourceSkillsForAgentResolution = type === "agents" ? loadLocalSkills(platformDir) : localSkills;
3770
+ const localSourceSkillSlugs = new Set(sourceSkillsForAgentResolution.map((skill) => skill.slug));
3771
+ const pendingLocalSkillSlugs = new Set(localSkills.map((skill) => skill.slug));
3772
+ if (localSkills.length > 0) {
3773
+ allChanges.push(...await planSkills(api, localSkills, projectId, appName));
3774
+ }
3433
3775
  if (!type || type === "agents") {
3434
3776
  const localAgents = loadLocalAgents(platformDir, name || void 0, appName);
3435
3777
  if (localAgents.length > 0) {
3436
- const changes = await planAgents(api, localAgents, projectId);
3437
- allChanges.push(...changes);
3778
+ const result = await planAgents(api, localAgents, projectId, localSourceSkillSlugs, pendingLocalSkillSlugs, appName);
3779
+ allChanges.push(...result.changes);
3780
+ planValidationErrors += result.validationErrors;
3438
3781
  }
3439
3782
  }
3440
3783
  if (!type || type === "mailboxes") {
@@ -3447,7 +3790,14 @@ async function plan(args) {
3447
3790
  const lintWarnings = await safeLint(projectRoot, localAutomations);
3448
3791
  if (allChanges.length === 0) {
3449
3792
  if (jsonOutput) {
3450
- console.log(JSON.stringify({ changes: [], warnings: [], lintWarnings: serializeLintWarnings(lintWarnings, projectRoot) }));
3793
+ console.log(JSON.stringify({ changes: [], warnings: [], validationErrors: planValidationErrors, lintWarnings: serializeLintWarnings(lintWarnings, projectRoot) }));
3794
+ if (planValidationErrors > 0) process.exitCode = 1;
3795
+ return;
3796
+ }
3797
+ if (planValidationErrors > 0) {
3798
+ console.log(pc2.red(` \u2717 Plan has ${planValidationErrors} agent skill validation error${planValidationErrors === 1 ? "" : "s"}.`));
3799
+ console.log();
3800
+ process.exitCode = 1;
3451
3801
  return;
3452
3802
  }
3453
3803
  console.log(pc2.green(" \u2713 No changes detected"));
@@ -3465,7 +3815,8 @@ async function plan(args) {
3465
3815
  }
3466
3816
  }
3467
3817
  }
3468
- console.log(JSON.stringify({ changes: allChanges, warnings, lintWarnings: serializeLintWarnings(lintWarnings, projectRoot) }));
3818
+ console.log(JSON.stringify({ changes: allChanges, warnings, validationErrors: planValidationErrors, lintWarnings: serializeLintWarnings(lintWarnings, projectRoot) }));
3819
+ if (planValidationErrors > 0) process.exitCode = 1;
3469
3820
  return;
3470
3821
  }
3471
3822
  console.log(pc2.bold(" Changes:"));
@@ -3506,7 +3857,12 @@ async function plan(args) {
3506
3857
  }
3507
3858
  console.log();
3508
3859
  safePrintLint(lintWarnings, projectRoot);
3509
- console.log(pc2.dim(` Run 'lumera apply' to apply these changes.`));
3860
+ if (planValidationErrors > 0) {
3861
+ console.log(pc2.red(` \u2717 Fix ${planValidationErrors} agent skill validation error${planValidationErrors === 1 ? "" : "s"} before applying.`));
3862
+ process.exitCode = 1;
3863
+ } else {
3864
+ console.log(pc2.dim(` Run 'lumera apply' to apply these changes.`));
3865
+ }
3510
3866
  console.log();
3511
3867
  }
3512
3868
  async function apply(args) {
@@ -3553,24 +3909,40 @@ async function apply(args) {
3553
3909
  collections = /* @__PURE__ */ new Map();
3554
3910
  }
3555
3911
  const allChanges = [];
3912
+ let planValidationErrors = 0;
3556
3913
  const localCollections = !type || type === "collections" ? loadLocalCollections(platformDir, name || void 0) : [];
3557
3914
  const localAutomations = !type || type === "automations" ? loadLocalAutomations(platformDir, name || void 0, appName) : [];
3558
3915
  const localHooks = !type || type === "hooks" ? loadLocalHooks(platformDir, name || void 0, appName) : [];
3916
+ const localSkills = !type || type === "skills" ? loadLocalSkills(platformDir, name || void 0) : [];
3917
+ const sourceSkillsForAgentResolution = type === "agents" ? loadLocalSkills(platformDir) : localSkills;
3918
+ const localSourceSkillSlugs = new Set(sourceSkillsForAgentResolution.map((skill) => skill.slug));
3919
+ const pendingLocalSkillSlugs = new Set(localSkills.map((skill) => skill.slug));
3559
3920
  const localAgents = !type || type === "agents" ? loadLocalAgents(platformDir, name || void 0, appName) : [];
3560
3921
  const localMailboxes = !type || type === "mailboxes" ? loadLocalMailboxes(platformDir, name || void 0) : [];
3561
3922
  if (localCollections.length > 0) allChanges.push(...await planCollections(api, localCollections, appName, projectId));
3562
3923
  if (localAutomations.length > 0) allChanges.push(...await planAutomations(api, localAutomations, projectId));
3563
3924
  if (localHooks.length > 0) allChanges.push(...await planHooks(api, localHooks, collections, projectId));
3564
- if (localAgents.length > 0) allChanges.push(...await planAgents(api, localAgents, projectId));
3925
+ if (localSkills.length > 0) allChanges.push(...await planSkills(api, localSkills, projectId, appName));
3926
+ if (localAgents.length > 0) {
3927
+ const result = await planAgents(api, localAgents, projectId, localSourceSkillSlugs, pendingLocalSkillSlugs, appName);
3928
+ allChanges.push(...result.changes);
3929
+ planValidationErrors += result.validationErrors;
3930
+ }
3565
3931
  if (localMailboxes.length > 0) allChanges.push(...await planMailboxes(api, localMailboxes));
3566
3932
  if (name) {
3567
- const hasLocal = localCollections.length > 0 || localAutomations.length > 0 || localHooks.length > 0 || localAgents.length > 0 || localMailboxes.length > 0;
3933
+ const hasLocal = localCollections.length > 0 || localAutomations.length > 0 || localHooks.length > 0 || localSkills.length > 0 || localAgents.length > 0 || localMailboxes.length > 0;
3568
3934
  if (!hasLocal) {
3569
3935
  console.log();
3570
3936
  console.log(pc2.red(` Resource "${name}" not found locally`));
3571
3937
  process.exit(1);
3572
3938
  }
3573
3939
  }
3940
+ if (planValidationErrors > 0) {
3941
+ console.log();
3942
+ console.log(pc2.red(` \u2717 Apply aborted: fix ${planValidationErrors} agent skill validation error${planValidationErrors === 1 ? "" : "s"} first.`));
3943
+ console.log();
3944
+ process.exit(1);
3945
+ }
3574
3946
  let willDeployApp = false;
3575
3947
  if (!type && !skipApp) {
3576
3948
  try {
@@ -3651,9 +4023,14 @@ async function apply(args) {
3651
4023
  totalErrors += await applyHooks(api, localHooks, collections, projectId);
3652
4024
  console.log();
3653
4025
  }
4026
+ if (localSkills.length > 0) {
4027
+ console.log(pc2.bold(" Skills:"));
4028
+ totalErrors += await applySkills(api, localSkills, projectId, appName);
4029
+ console.log();
4030
+ }
3654
4031
  if (localAgents.length > 0) {
3655
4032
  console.log(pc2.bold(" Agents:"));
3656
- totalErrors += await applyAgents(api, localAgents, projectId);
4033
+ totalErrors += await applyAgents(api, localAgents, projectId, localSourceSkillSlugs, appName);
3657
4034
  console.log();
3658
4035
  }
3659
4036
  if (localMailboxes.length > 0) {
@@ -3706,10 +4083,27 @@ async function pull(args) {
3706
4083
  } catch {
3707
4084
  collections = /* @__PURE__ */ new Map();
3708
4085
  }
4086
+ if (projectId && (!type || type === "skills")) {
4087
+ const localSkills = loadLocalSkills(platformDir, name || void 0);
4088
+ if (localSkills.length > 0) {
4089
+ const changes = await planSkills(api, localSkills, projectId, appName);
4090
+ for (const change of changes) {
4091
+ if (change.type === "update") conflicts.push(`skills/${change.id}`);
4092
+ }
4093
+ }
4094
+ }
3709
4095
  if (!type || type === "agents") {
3710
4096
  const localAgents = loadLocalAgents(platformDir, name || void 0, appName);
3711
4097
  if (localAgents.length > 0) {
3712
- const changes = await planAgents(api, localAgents, projectId);
4098
+ const localSkillSlugs = new Set(loadLocalSkills(platformDir).map((skill) => skill.slug));
4099
+ const { changes } = await planAgents(
4100
+ api,
4101
+ localAgents,
4102
+ projectId,
4103
+ localSkillSlugs,
4104
+ localSkillSlugs,
4105
+ appName
4106
+ );
3713
4107
  for (const c of changes) {
3714
4108
  if (c.type === "update") conflicts.push(`agents/${c.name}`);
3715
4109
  }
@@ -3770,9 +4164,14 @@ async function pull(args) {
3770
4164
  await pullHooks(api, platformDir, name || void 0, appName, projectId);
3771
4165
  console.log();
3772
4166
  }
4167
+ if (!type || type === "skills") {
4168
+ console.log(pc2.bold(" Skills:"));
4169
+ await pullSkills(api, platformDir, name || void 0, projectId, appName);
4170
+ console.log();
4171
+ }
3773
4172
  if (!type || type === "agents") {
3774
4173
  console.log(pc2.bold(" Agents:"));
3775
- await pullAgents(api, platformDir, name || void 0, projectId);
4174
+ await pullAgents(api, platformDir, name || void 0, projectId, appName);
3776
4175
  console.log();
3777
4176
  }
3778
4177
  if (!type || type === "mailboxes") {
@@ -3924,6 +4323,7 @@ ${pc2.dim("Usage:")}
3924
4323
 
3925
4324
  ${pc2.dim("Resources:")}
3926
4325
  agents/<name> Diff agent (system_prompt, policy_script)
4326
+ skills/<slug> Diff project skill instructions
3927
4327
  automations/<name> Diff automation code
3928
4328
  hooks/<name> Diff hook script
3929
4329
 
@@ -4072,23 +4472,71 @@ async function diff(args) {
4072
4472
  } else if (scriptChanged) {
4073
4473
  renderFullDiff(local.fileName, remote.script || "", local.script);
4074
4474
  }
4475
+ } else if (type === "skills") {
4476
+ const localSkills = loadLocalSkills(platformDir, name);
4477
+ if (localSkills.length > 0) requireProjectSkillsProjectID(projectId);
4478
+ const remoteSkills = projectId ? await api.listAgentSkills({ project_id: projectId }) : [];
4479
+ const local = localSkills[0];
4480
+ const remote = remoteSkillsByLocalSlug(remoteSkills, appName).get(name) || remoteSkills.find((skill) => skill.name === name);
4481
+ if (!local && !remote) {
4482
+ console.log(pc2.red(` Skill "${name}" not found locally or remotely`));
4483
+ process.exit(1);
4484
+ }
4485
+ if (!local) {
4486
+ console.log(pc2.cyan(` Skill "${name}" exists only remotely`));
4487
+ process.exit(0);
4488
+ }
4489
+ if (!remote) {
4490
+ console.log(pc2.yellow(` Skill "${name}" exists only locally (not yet deployed)`));
4491
+ process.exit(0);
4492
+ }
4493
+ console.log(pc2.bold(` Skill: ${local.name}`));
4494
+ console.log();
4495
+ if (remote.name !== local.name) {
4496
+ console.log(` name: ${pc2.red(remote.name)} \u2192 ${pc2.green(local.name)}`);
4497
+ }
4498
+ if ((remote.summary || "").trim() !== local.summary.trim()) {
4499
+ console.log(` summary: ${pc2.red(remote.summary || "(empty)")} \u2192 ${pc2.green(local.summary)}`);
4500
+ }
4501
+ const remoteMarkdown = skillMarkdown({
4502
+ slug: localProjectSkillSlug(appName, remote.slug),
4503
+ name: remote.name,
4504
+ summary: remote.summary || "",
4505
+ instructions: remote.instructions || ""
4506
+ });
4507
+ const localMarkdown = skillMarkdown(local);
4508
+ if (remoteMarkdown === localMarkdown) {
4509
+ console.log(pc2.green(" \u2713 No changes"));
4510
+ } else {
4511
+ renderFullDiff("SKILL.md", remoteMarkdown, localMarkdown);
4512
+ }
4075
4513
  } else {
4076
- console.log(pc2.red(` Diff not supported for "${type}" \u2014 use agents, automations, or hooks`));
4514
+ console.log(pc2.red(` Diff not supported for "${type}" \u2014 use agents, skills, automations, or hooks`));
4077
4515
  process.exit(1);
4078
4516
  }
4079
4517
  console.log();
4080
4518
  }
4081
4519
  export {
4082
4520
  apply,
4521
+ applyAgents,
4083
4522
  applyCollections,
4523
+ applySkills,
4084
4524
  convertCollectionToApiFormat,
4085
4525
  destroy,
4086
4526
  diff,
4087
4527
  list,
4088
4528
  loadLocalCollections,
4529
+ loadLocalSkills,
4530
+ localProjectSkillSlug,
4089
4531
  plan,
4532
+ planAgents,
4090
4533
  planCollections,
4534
+ planSkills,
4535
+ projectSkillRefCandidates,
4091
4536
  pull,
4092
4537
  pullCollections,
4538
+ pullSkills,
4539
+ resolveAgentSkillRefs,
4540
+ scopedProjectSkillSlug,
4093
4541
  show
4094
4542
  };
@@ -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",
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]