@lumerahq/cli 0.28.0 → 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.31.0,<0.32.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;
@@ -50,14 +50,18 @@ ${pc.dim("Usage:")}
50
50
  lumera functions list
51
51
  lumera functions inspect <function-id>
52
52
  lumera functions invoke <function-id> --input <path|-> --local
53
+ lumera functions invoke <function-id> --input <path|-> --remote --project <project>
53
54
  lumera functions test [-- <pytest-options>]
54
55
 
55
56
  ${pc.dim("Description:")}
56
- Discover, inspect, invoke, and test configured Functions in a fresh local
57
- Python process. Phase 1 does not deploy or invoke Functions remotely.
57
+ Discover, inspect, invoke, and test configured Functions. Invocation can run
58
+ in a fresh local process or create a durable remote execution.
58
59
 
59
60
  ${pc.dim("Options:")}
60
- --local Required for invoke; remote invocation is unavailable
61
+ --local Run from the local project source
62
+ --remote Create a durable execution from the active release
63
+ --project <project> Project ID or external ID for remote invocation
64
+ --idempotency-key <k> Deduplicate matching remote invocation requests
61
65
  --input <path|-> Read a JSON object from a project file or stdin (-)
62
66
  --help, -h Show this help
63
67
 
@@ -65,6 +69,7 @@ ${pc.dim("Examples:")}
65
69
  lumera functions list
66
70
  lumera functions inspect invoices:approve
67
71
  lumera functions invoke invoices:approve --input input.json --local
72
+ lumera functions invoke invoices:approve --input input.json --remote --project finance
68
73
  cat input.json | lumera functions invoke invoices:approve --input - --local
69
74
  lumera functions test
70
75
  lumera functions test -- -k approve
@@ -74,10 +79,17 @@ function buildFunctionRunnerArgs(subcommand, args, projectRoot) {
74
79
  if (!SUPPORTED_SUBCOMMANDS.has(subcommand)) {
75
80
  throw new Error(`Unknown functions subcommand: ${subcommand}`);
76
81
  }
77
- if (subcommand === "invoke" && !args.includes("--local")) {
78
- throw new Error(
79
- "Function invocation is local-only in Phase 1; pass --local explicitly."
80
- );
82
+ if (subcommand === "invoke") {
83
+ const local = args.includes("--local");
84
+ const remote = args.includes("--remote");
85
+ if (local === remote) {
86
+ throw new Error(
87
+ "Pass exactly one of --local or --remote for Function invocation."
88
+ );
89
+ }
90
+ if (remote && !args.includes("--project")) {
91
+ throw new Error("--project is required for remote Function invocation.");
92
+ }
81
93
  }
82
94
  const runnerPrefix = ["run"];
83
95
  if (existsSync(join(projectRoot, "uv.lock"))) {
@@ -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-NIVQLH6V.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-R53XKJL2.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-R53XKJL2.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-R53XKJL2.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-R53XKJL2.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-R53XKJL2.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-R53XKJL2.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-R53XKJL2.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-JUOVWQQH.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-2PYA24JV.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,14 +1,21 @@
1
1
  import {
2
2
  findFunctionsProjectRoot
3
- } from "./chunk-NIVQLH6V.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.31.0,<0.32.0";
9
+ var RELEASE_SDK_REQUIREMENT = "lumera[release]>=0.33.0,<0.34.0";
10
10
  var RELEASE_PROTOCOL_VERSION = 1;
11
- var RELEASE_COMMANDS = /* @__PURE__ */ new Set(["build", "inspect", "contract-test", "deploy"]);
11
+ var RELEASE_COMMANDS = /* @__PURE__ */ new Set([
12
+ "build",
13
+ "inspect",
14
+ "contract-test",
15
+ "deploy",
16
+ "stage",
17
+ "activate"
18
+ ]);
12
19
  function showHelp() {
13
20
  console.log(`
14
21
  ${pc.dim("Usage:")}
@@ -16,10 +23,13 @@ ${pc.dim("Usage:")}
16
23
  lumera project release inspect <archive> [--expected-digest <sha256>]
17
24
  lumera project release contract-test <archive> [--mode api|worker|all]
18
25
  lumera project release deploy <archive> --project <project-id>
26
+ lumera project release stage <release-id> --project <project-id> --expected-generation <n>
27
+ lumera project release activate <deployment-id> --project <project-id> --expected-generation <n>
19
28
 
20
29
  ${pc.dim("Description:")}
21
30
  Build, verify, and contract-test immutable Python 3.13/Linux/ARM64 project
22
- releases, or upload a verified release as an inactive project resource.
31
+ releases, upload an inactive resource, stage a healthy runtime, and activate
32
+ it atomically with an expected generation.
23
33
  `);
24
34
  }
25
35
  function buildReleaseRunnerArgs(command, args, projectRoot) {
@@ -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,
@@ -93,10 +93,132 @@ var automationConfigRule = {
93
93
  }
94
94
  };
95
95
 
96
- // src/lib/lint/rules/collection-schema.ts
96
+ // src/lib/lint/rules/automation-main-guard.ts
97
+ import { spawnSync } from "child_process";
98
+ var PYTHON_AST_SCRIPT = String.raw`
99
+ import ast
100
+ import json
101
+ import sys
102
+
103
+ source = sys.stdin.read()
104
+ try:
105
+ module = ast.parse(source)
106
+ except SyntaxError:
107
+ print("null")
108
+ raise SystemExit(0)
109
+
110
+
111
+ def is_name(node):
112
+ return isinstance(node, ast.Name) and node.id == "__name__"
113
+
114
+
115
+ def is_main_string(node):
116
+ if isinstance(node, ast.Str):
117
+ return node.s == "__main__"
118
+ return isinstance(node, ast.Constant) and node.value == "__main__"
119
+
120
+
121
+ def is_main_guard(node):
122
+ test = node.test
123
+ if not isinstance(test, ast.Compare):
124
+ return False
125
+ if len(test.ops) != 1 or not isinstance(test.ops[0], ast.Eq):
126
+ return False
127
+ if len(test.comparators) != 1:
128
+ return False
129
+ right = test.comparators[0]
130
+ return (is_name(test.left) and is_main_string(right)) or (
131
+ is_main_string(test.left) and is_name(right)
132
+ )
133
+
134
+
135
+ for statement in module.body:
136
+ if not isinstance(statement, ast.If) or not is_main_guard(statement):
137
+ continue
138
+ get_segment = getattr(ast, "get_source_segment", None)
139
+ condition = get_segment(source, statement.test) if get_segment else None
140
+ if not condition:
141
+ condition = '__name__ == "__main__"'
142
+ snippet = "if " + " ".join(condition.split()) + ":"
143
+ print(json.dumps({"line": statement.lineno, "snippet": snippet}))
144
+ break
145
+ else:
146
+ print("null")
147
+ `;
148
+ var cachedPython3;
149
+ function findPython3() {
150
+ if (cachedPython3 !== void 0) return cachedPython3;
151
+ for (const executable of ["python3", "python"]) {
152
+ const probe = spawnSync(executable, ["-I", "-S", "--version"], {
153
+ encoding: "utf8",
154
+ timeout: 2e3,
155
+ windowsHide: true
156
+ });
157
+ const version = `${probe.stdout ?? ""}${probe.stderr ?? ""}`.trim();
158
+ if (probe.status === 0 && /^Python 3(?:\.|\s|$)/.test(version)) {
159
+ cachedPython3 = executable;
160
+ return executable;
161
+ }
162
+ }
163
+ cachedPython3 = null;
164
+ return null;
165
+ }
97
166
  function isRecord2(value) {
98
167
  return typeof value === "object" && value !== null && !Array.isArray(value);
99
168
  }
169
+ function functionEntrypoint(config) {
170
+ if (!isRecord2(config) || !isRecord2(config.function)) {
171
+ return null;
172
+ }
173
+ const name = config.function.name;
174
+ return typeof name === "string" && name.trim() !== "" ? name.trim() : null;
175
+ }
176
+ function findTopLevelMainGuard(source) {
177
+ const python = findPython3();
178
+ if (python === null) return null;
179
+ const parsed = spawnSync(python, ["-I", "-S", "-c", PYTHON_AST_SCRIPT], {
180
+ input: source,
181
+ encoding: "utf8",
182
+ maxBuffer: 1024 * 1024,
183
+ timeout: 5e3,
184
+ windowsHide: true
185
+ });
186
+ if (parsed.status !== 0) return null;
187
+ try {
188
+ const value = JSON.parse(parsed.stdout.trim());
189
+ if (!isRecord2(value) || typeof value.line !== "number" || typeof value.snippet !== "string") {
190
+ return null;
191
+ }
192
+ return { line: value.line, snippet: value.snippet };
193
+ } catch {
194
+ return null;
195
+ }
196
+ }
197
+ var automationMainGuardRule = {
198
+ id: "automation-main-guard",
199
+ description: "Warns when a function automation contains a top-level __main__ guard.",
200
+ appliesTo: ["automation"],
201
+ check(target) {
202
+ const entrypoint = functionEntrypoint(target.config);
203
+ if (entrypoint === null) return [];
204
+ const guard = findTopLevelMainGuard(target.source);
205
+ if (guard === null) return [];
206
+ const warning2 = {
207
+ ruleId: "automation-main-guard",
208
+ target,
209
+ severity: "warning",
210
+ line: guard.line,
211
+ snippet: guard.snippet,
212
+ message: `Function automations must not contain a top-level __main__ guard. Lumera invokes "${entrypoint}" automatically; remove the guard.`
213
+ };
214
+ return [warning2];
215
+ }
216
+ };
217
+
218
+ // src/lib/lint/rules/collection-schema.ts
219
+ function isRecord3(value) {
220
+ return typeof value === "object" && value !== null && !Array.isArray(value);
221
+ }
100
222
  function error(target, message, snippet) {
101
223
  return {
102
224
  ruleId: "collection-schema",
@@ -118,7 +240,7 @@ var collectionSchemaRule = {
118
240
  const message = err instanceof Error ? err.message : String(err);
119
241
  return [error(target, `Invalid JSON: ${message}`)];
120
242
  }
121
- if (!isRecord2(parsed)) {
243
+ if (!isRecord3(parsed)) {
122
244
  return [error(target, "Collection file must contain a JSON object.")];
123
245
  }
124
246
  const issues = [];
@@ -131,7 +253,7 @@ var collectionSchemaRule = {
131
253
  const localFieldTypes = /* @__PURE__ */ new Map();
132
254
  if (Array.isArray(fields)) {
133
255
  for (const field of fields) {
134
- if (isRecord2(field) && typeof field.name === "string") {
256
+ if (isRecord3(field) && typeof field.name === "string") {
135
257
  localFieldTypes.set(field.name, field.type);
136
258
  }
137
259
  }
@@ -155,7 +277,7 @@ var collectionSchemaRule = {
155
277
  } else {
156
278
  for (let i = 0; i < fields.length; i++) {
157
279
  const field = fields[i];
158
- if (!isRecord2(field)) {
280
+ if (!isRecord3(field)) {
159
281
  issues.push(error(target, `Field at index ${i} must be an object.`));
160
282
  continue;
161
283
  }
@@ -176,7 +298,7 @@ var collectionSchemaRule = {
176
298
  } else {
177
299
  for (let i = 0; i < indexes.length; i++) {
178
300
  const index = indexes[i];
179
- if (!isRecord2(index)) {
301
+ if (!isRecord3(index)) {
180
302
  const hint = typeof index === "string" ? ' Indexes are objects like { "fields": ["title"], "unique": true }, not CREATE INDEX SQL strings.' : "";
181
303
  issues.push(error(target, `Index at index ${i} must be an object.${hint}`));
182
304
  continue;
@@ -193,7 +315,7 @@ var collectionSchemaRule = {
193
315
  }
194
316
  }
195
317
  if (search !== void 0) {
196
- if (!isRecord2(search)) {
318
+ if (!isRecord3(search)) {
197
319
  issues.push(error(target, 'Optional field "search" must be an object when present.'));
198
320
  } else {
199
321
  const unknownSearchKeys = Object.keys(search).filter((key) => key !== "fulltext" && key !== "semantic");
@@ -205,14 +327,14 @@ var collectionSchemaRule = {
205
327
  if (fulltext === void 0 && semantic === void 0) {
206
328
  issues.push(error(target, '"search" must include "fulltext" or "semantic".'));
207
329
  }
208
- if (fulltext !== void 0 && !isRecord2(fulltext)) {
330
+ if (fulltext !== void 0 && !isRecord3(fulltext)) {
209
331
  issues.push(error(target, '"search.fulltext" must be an object when present.'));
210
- } else if (isRecord2(fulltext)) {
332
+ } else if (isRecord3(fulltext)) {
211
333
  const allowed = /* @__PURE__ */ new Set(["fields", "language", "fuzzy_fields"]);
212
334
  for (const key of Object.keys(fulltext).filter((key2) => !allowed.has(key2))) {
213
335
  issues.push(error(target, `Unknown search.fulltext option "${key}".`));
214
336
  }
215
- if (!isRecord2(fulltext.fields) || Object.keys(fulltext.fields).length === 0) {
337
+ if (!isRecord3(fulltext.fields) || Object.keys(fulltext.fields).length === 0) {
216
338
  issues.push(error(target, '"search.fulltext.fields" must be a non-empty field-to-weight object.'));
217
339
  } else {
218
340
  if (Object.keys(fulltext.fields).length > 8) {
@@ -244,9 +366,9 @@ var collectionSchemaRule = {
244
366
  }
245
367
  }
246
368
  }
247
- if (semantic !== void 0 && !isRecord2(semantic)) {
369
+ if (semantic !== void 0 && !isRecord3(semantic)) {
248
370
  issues.push(error(target, '"search.semantic" must be an object when present.'));
249
- } else if (isRecord2(semantic)) {
371
+ } else if (isRecord3(semantic)) {
250
372
  const allowed = /* @__PURE__ */ new Set(["fields", "model"]);
251
373
  for (const key of Object.keys(semantic).filter((key2) => !allowed.has(key2))) {
252
374
  issues.push(error(target, `Unknown search.semantic option "${key}".`));
@@ -653,7 +775,13 @@ var llmImportRule = {
653
775
  };
654
776
 
655
777
  // src/lib/lint/registry.ts
656
- var ALL_RULES = [automationConfigRule, collectionSchemaRule, hookVerifyRule, llmImportRule];
778
+ var ALL_RULES = [
779
+ automationConfigRule,
780
+ automationMainGuardRule,
781
+ collectionSchemaRule,
782
+ hookVerifyRule,
783
+ llmImportRule
784
+ ];
657
785
 
658
786
  // src/lib/lint/format.ts
659
787
  import { relative } from "path";
@@ -1159,6 +1287,8 @@ ${pc2.dim("Resources:")}
1159
1287
  hooks Plan only hooks
1160
1288
  agents Plan only agents
1161
1289
  agents/<name> Plan single agent
1290
+ skills Plan only project skills
1291
+ skills/<slug> Plan single project skill
1162
1292
  mailboxes Plan only mailboxes
1163
1293
  mailboxes/<slug> Plan single mailbox
1164
1294
  app Plan app deployment
@@ -1187,6 +1317,8 @@ ${pc2.dim("Resources:")}
1187
1317
  hooks Apply only hooks
1188
1318
  agents Apply only agents
1189
1319
  agents/<name> Apply single agent
1320
+ skills Apply only project skills
1321
+ skills/<slug> Apply single project skill
1190
1322
  mailboxes Apply only mailboxes
1191
1323
  mailboxes/<slug> Apply single mailbox
1192
1324
  app Deploy the frontend app
@@ -1231,6 +1363,8 @@ ${pc2.dim("Resources:")}
1231
1363
  hooks Pull only hooks
1232
1364
  agents Pull only agents
1233
1365
  agents/<name> Pull single agent
1366
+ skills Pull only project skills
1367
+ skills/<slug> Pull single project skill
1234
1368
  mailboxes Pull only mailboxes
1235
1369
  mailboxes/<slug> Pull single mailbox
1236
1370
 
@@ -1258,6 +1392,8 @@ ${pc2.dim("Resources:")}
1258
1392
  hooks Destroy only hooks
1259
1393
  agents Destroy only agents
1260
1394
  agents/<name> Destroy single agent
1395
+ skills Destroy only project skills
1396
+ skills/<slug> Destroy single project skill
1261
1397
  app Delete app registration
1262
1398
 
1263
1399
  ${pc2.dim("Options:")}
@@ -1284,6 +1420,7 @@ ${pc2.dim("Types:")}
1284
1420
  automations List only automations
1285
1421
  hooks List only hooks
1286
1422
  agents List only agents
1423
+ skills List only project skills
1287
1424
  mailboxes List only mailboxes
1288
1425
 
1289
1426
  ${pc2.dim("Options:")}
@@ -1308,6 +1445,7 @@ ${pc2.dim("Resources:")}
1308
1445
  automations/<name> Show automation details
1309
1446
  hooks/<name> Show hook details
1310
1447
  agents/<name> Show agent details
1448
+ skills/<slug> Show project skill details
1311
1449
  mailboxes/<slug> Show mailbox details
1312
1450
  app Show app details
1313
1451
 
@@ -1324,7 +1462,7 @@ function parseResource(resourcePath) {
1324
1462
  const parts = resourcePath.split("/");
1325
1463
  const type = parts[0];
1326
1464
  const name = parts.slice(1).join("/") || null;
1327
- const validTypes = ["collections", "automations", "hooks", "agents", "mailboxes", "app"];
1465
+ const validTypes = ["collections", "automations", "hooks", "agents", "skills", "mailboxes", "app"];
1328
1466
  if (!validTypes.includes(type)) {
1329
1467
  console.log(pc2.red(` Unknown resource type "${type}". Valid types: ${validTypes.join(", ")}`));
1330
1468
  process.exit(1);
@@ -2347,6 +2485,246 @@ ${hook.script.split("\n").map((line) => " " + line).join("\n")}
2347
2485
  console.log(pc2.green(" \u2713"), `${hook.name} \u2192 hooks/${fileName}`);
2348
2486
  }
2349
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
+ }
2350
2728
  function loadLocalAgents(platformDir, filterName, appName) {
2351
2729
  const agentsDir = join2(platformDir, "agents");
2352
2730
  if (!existsSync2(agentsDir)) return [];
@@ -2417,44 +2795,38 @@ function loadLocalAgents(platformDir, filterName, appName) {
2417
2795
  }
2418
2796
  return agents;
2419
2797
  }
2420
- async function planAgents(api, localAgents, projectId) {
2798
+ async function planAgents(api, localAgents, projectId, localSkillSlugs = /* @__PURE__ */ new Set(), appName) {
2421
2799
  const changes = [];
2422
2800
  const remoteAgents = await api.listAgents(projectId ? { project_id: projectId } : void 0);
2423
2801
  const remoteByExternalId = new Map(
2424
2802
  remoteAgents.filter((a) => a.external_id && !a.managed).map((a) => [a.external_id, a])
2425
2803
  );
2426
- const refToId = /* @__PURE__ */ new Map();
2427
- const skillIdToSlug = /* @__PURE__ */ new Map();
2428
- const blockedRefs = /* @__PURE__ */ new Set();
2429
2804
  const planRefs = [
2430
2805
  .../* @__PURE__ */ new Set([
2431
2806
  ...localAgents.flatMap((a) => a.agent.skills ?? []),
2432
2807
  ...remoteAgents.flatMap((a) => a.skill_ids ?? [])
2433
2808
  ])
2434
2809
  ];
2435
- if (planRefs.length > 0) {
2436
- try {
2437
- const skills = await api.lookupAgentSkills(planRefs);
2438
- for (const s of skills) {
2439
- skillIdToSlug.set(s.id, s.slug);
2440
- if (isAvailableToCustomAgents(s)) {
2441
- refToId.set(s.slug, s.id);
2442
- refToId.set(s.id, s.id);
2443
- } else {
2444
- blockedRefs.add(s.slug);
2445
- blockedRefs.add(s.id);
2446
- }
2447
- }
2448
- } catch (e) {
2449
- console.log(pc2.yellow(` \u26A0 Could not resolve skills for plan: ${e}`));
2450
- }
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}`));
2451
2820
  }
2821
+ const { refToId, skillIdToSlug, blockedRefs, outsideProjectRefs } = resolution;
2452
2822
  for (const { agent, systemPrompt, policyScript } of localAgents) {
2453
2823
  const remote = remoteByExternalId.get(agent.external_id);
2454
2824
  for (const ref of agent.skills ?? []) {
2455
- 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)) {
2456
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`));
2457
- } else if (!refToId.has(ref)) {
2829
+ } else if (!refToId.has(ref) && !localSkillSlugs.has(ref)) {
2458
2830
  console.log(pc2.red(` \u2717 Skill "${ref}" not found \u2014 apply will fail for this agent`));
2459
2831
  }
2460
2832
  }
@@ -2472,12 +2844,14 @@ async function planAgents(api, localAgents, projectId) {
2472
2844
  if ((remote.policy_enabled || false) !== (agent.policy_enabled || false)) diffs.push("policy_enabled");
2473
2845
  if ((remote.policy_description || "") !== (agent.policy_description || "")) diffs.push("policy_description");
2474
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));
2475
2848
  const remoteSkillIds = [...remote.skill_ids || []].sort();
2476
- if (localSkillIds.join(",") !== remoteSkillIds.join(",")) {
2849
+ if (localSkillIds.join(",") !== remoteSkillIds.join(",") || pendingLocalSkillSlugs.length > 0) {
2477
2850
  const addedSlugs = localSkillIds.filter((id) => !remoteSkillIds.includes(id)).map((id) => skillIdToSlug.get(id) || id);
2478
2851
  const removedSlugs = remoteSkillIds.filter((id) => !localSkillIds.includes(id)).map((id) => skillIdToSlug.get(id) || id);
2479
2852
  const parts = [];
2480
2853
  if (addedSlugs.length) parts.push(`+${addedSlugs.join(", +")}`);
2854
+ if (pendingLocalSkillSlugs.length) parts.push(`+${pendingLocalSkillSlugs.join(", +")}`);
2481
2855
  if (removedSlugs.length) parts.push(`-${removedSlugs.join(", -")}`);
2482
2856
  diffs.push(`skills (${parts.join(", ")})`);
2483
2857
  }
@@ -2500,34 +2874,29 @@ function isAvailableToCustomAgents(skill) {
2500
2874
  if (!skill.managed) return true;
2501
2875
  return (skill.tags ?? []).includes(TAG_AVAILABLE_TO_CUSTOM_AGENTS);
2502
2876
  }
2503
- async function applyAgents(api, localAgents, projectId) {
2877
+ async function applyAgents(api, localAgents, projectId, localSkillSlugs = /* @__PURE__ */ new Set(), appName) {
2504
2878
  let errors = 0;
2505
2879
  const remoteAgents = await api.listAgents(projectId ? { project_id: projectId } : void 0);
2506
2880
  const remoteByExternalId = new Map(
2507
2881
  remoteAgents.filter((a) => a.external_id && !a.managed).map((a) => [a.external_id, a])
2508
2882
  );
2509
- const refToId = /* @__PURE__ */ new Map();
2510
- const blockedRefs = /* @__PURE__ */ new Set();
2511
2883
  const refs = [...new Set(localAgents.flatMap((a) => a.agent.skills ?? []))];
2512
- if (refs.length > 0) {
2513
- try {
2514
- const skills = await api.lookupAgentSkills(refs);
2515
- for (const s of skills) {
2516
- if (isAvailableToCustomAgents(s)) {
2517
- refToId.set(s.slug, s.id);
2518
- refToId.set(s.id, s.id);
2519
- } else {
2520
- blockedRefs.add(s.slug);
2521
- blockedRefs.add(s.id);
2522
- }
2523
- }
2524
- } catch (e) {
2525
- console.log(pc2.yellow(` \u26A0 Could not resolve skills: ${e}`));
2526
- }
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}`));
2527
2894
  }
2895
+ const { refToId, blockedRefs, outsideProjectRefs } = resolution;
2528
2896
  for (const { agent, systemPrompt, policyScript } of localAgents) {
2529
2897
  const remote = remoteByExternalId.get(agent.external_id);
2530
2898
  const skillIds = [];
2899
+ const outsideProjectForAgent = [];
2531
2900
  const blockedForAgent = [];
2532
2901
  const notFoundForAgent = [];
2533
2902
  if (agent.skills) {
@@ -2535,6 +2904,8 @@ async function applyAgents(api, localAgents, projectId) {
2535
2904
  const id = refToId.get(ref);
2536
2905
  if (id) {
2537
2906
  skillIds.push(id);
2907
+ } else if (outsideProjectRefs.has(ref)) {
2908
+ outsideProjectForAgent.push(ref);
2538
2909
  } else if (blockedRefs.has(ref)) {
2539
2910
  blockedForAgent.push(ref);
2540
2911
  } else {
@@ -2542,8 +2913,11 @@ async function applyAgents(api, localAgents, projectId) {
2542
2913
  }
2543
2914
  }
2544
2915
  }
2545
- if (blockedForAgent.length > 0 || notFoundForAgent.length > 0) {
2916
+ if (outsideProjectForAgent.length > 0 || blockedForAgent.length > 0 || notFoundForAgent.length > 0) {
2546
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
+ }
2547
2921
  if (blockedForAgent.length > 0) {
2548
2922
  console.log(pc2.red(` skills not available for custom agents: ${blockedForAgent.join(", ")}`));
2549
2923
  }
@@ -2583,7 +2957,7 @@ async function applyAgents(api, localAgents, projectId) {
2583
2957
  }
2584
2958
  return errors;
2585
2959
  }
2586
- async function pullAgents(api, platformDir, filterName, projectId) {
2960
+ async function pullAgents(api, platformDir, filterName, projectId, appName) {
2587
2961
  const agentsDir = join2(platformDir, "agents");
2588
2962
  mkdirSync(agentsDir, { recursive: true });
2589
2963
  const agents = await api.listAgents(projectId ? { project_id: projectId } : void 0);
@@ -2592,7 +2966,9 @@ async function pullAgents(api, platformDir, filterName, projectId) {
2592
2966
  if (pullRefs.length > 0) {
2593
2967
  try {
2594
2968
  const skills = await api.lookupAgentSkills(pullRefs);
2595
- 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
+ }
2596
2972
  } catch (e) {
2597
2973
  console.log(pc2.yellow(` \u26A0 Could not resolve skills for pull: ${e}`));
2598
2974
  console.log(pc2.yellow(` Pulled configs may omit skills \u2014 review before re-applying.`));
@@ -2747,6 +3123,28 @@ async function listResources(api, platformDir, filterType, appName, projectId) {
2747
3123
  }
2748
3124
  }
2749
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
+ }
2750
3148
  if (!filterType || filterType === "mailboxes") {
2751
3149
  const localMailboxes = loadLocalMailboxes(platformDir);
2752
3150
  const remoteMailboxes = await api.listMailboxes();
@@ -2894,6 +3292,20 @@ async function destroyResources(api, platformDir, resourceType, resourceName, sk
2894
3292
  } catch {
2895
3293
  }
2896
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
+ }
2897
3309
  if (toDelete.length === 0) {
2898
3310
  console.log(pc2.green(" \u2713 No resources found to delete"));
2899
3311
  return;
@@ -2917,6 +3329,7 @@ async function destroyResources(api, platformDir, resourceType, resourceName, sk
2917
3329
  }
2918
3330
  }
2919
3331
  const agents = toDelete.filter((r) => r.type === "agent");
3332
+ const skills = toDelete.filter((r) => r.type === "skill");
2920
3333
  const hooks = toDelete.filter((r) => r.type === "hook");
2921
3334
  const automations = toDelete.filter((r) => r.type === "automation");
2922
3335
  const collections = toDelete.filter((r) => r.type === "collection");
@@ -2930,6 +3343,15 @@ async function destroyResources(api, platformDir, resourceType, resourceName, sk
2930
3343
  errors++;
2931
3344
  }
2932
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
+ }
2933
3355
  for (const resource of hooks) {
2934
3356
  try {
2935
3357
  await api.deleteHook(resource.remoteId);
@@ -3186,6 +3608,31 @@ async function showResource(api, platformDir, resourceType, resourceName, appNam
3186
3608
  console.log(` Skills: ${skills.join(", ")}`);
3187
3609
  }
3188
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();
3189
3636
  } else if (resourceType === "mailboxes") {
3190
3637
  const localMailboxes = loadLocalMailboxes(platformDir, resourceName);
3191
3638
  const remoteMailboxes = await api.listMailboxes();
@@ -3302,10 +3749,15 @@ async function plan(args) {
3302
3749
  allChanges.push(...changes);
3303
3750
  }
3304
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
+ }
3305
3757
  if (!type || type === "agents") {
3306
3758
  const localAgents = loadLocalAgents(platformDir, name || void 0, appName);
3307
3759
  if (localAgents.length > 0) {
3308
- const changes = await planAgents(api, localAgents, projectId);
3760
+ const changes = await planAgents(api, localAgents, projectId, localSkillSlugs, appName);
3309
3761
  allChanges.push(...changes);
3310
3762
  }
3311
3763
  }
@@ -3428,15 +3880,18 @@ async function apply(args) {
3428
3880
  const localCollections = !type || type === "collections" ? loadLocalCollections(platformDir, name || void 0) : [];
3429
3881
  const localAutomations = !type || type === "automations" ? loadLocalAutomations(platformDir, name || void 0, appName) : [];
3430
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) : []);
3431
3885
  const localAgents = !type || type === "agents" ? loadLocalAgents(platformDir, name || void 0, appName) : [];
3432
3886
  const localMailboxes = !type || type === "mailboxes" ? loadLocalMailboxes(platformDir, name || void 0) : [];
3433
3887
  if (localCollections.length > 0) allChanges.push(...await planCollections(api, localCollections, appName, projectId));
3434
3888
  if (localAutomations.length > 0) allChanges.push(...await planAutomations(api, localAutomations, projectId));
3435
3889
  if (localHooks.length > 0) allChanges.push(...await planHooks(api, localHooks, collections, projectId));
3436
- 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));
3437
3892
  if (localMailboxes.length > 0) allChanges.push(...await planMailboxes(api, localMailboxes));
3438
3893
  if (name) {
3439
- 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;
3440
3895
  if (!hasLocal) {
3441
3896
  console.log();
3442
3897
  console.log(pc2.red(` Resource "${name}" not found locally`));
@@ -3523,9 +3978,14 @@ async function apply(args) {
3523
3978
  totalErrors += await applyHooks(api, localHooks, collections, projectId);
3524
3979
  console.log();
3525
3980
  }
3981
+ if (localSkills.length > 0) {
3982
+ console.log(pc2.bold(" Skills:"));
3983
+ totalErrors += await applySkills(api, localSkills, projectId, appName);
3984
+ console.log();
3985
+ }
3526
3986
  if (localAgents.length > 0) {
3527
3987
  console.log(pc2.bold(" Agents:"));
3528
- totalErrors += await applyAgents(api, localAgents, projectId);
3988
+ totalErrors += await applyAgents(api, localAgents, projectId, localSkillSlugs, appName);
3529
3989
  console.log();
3530
3990
  }
3531
3991
  if (localMailboxes.length > 0) {
@@ -3578,10 +4038,25 @@ async function pull(args) {
3578
4038
  } catch {
3579
4039
  collections = /* @__PURE__ */ new Map();
3580
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
+ }
3581
4050
  if (!type || type === "agents") {
3582
4051
  const localAgents = loadLocalAgents(platformDir, name || void 0, appName);
3583
4052
  if (localAgents.length > 0) {
3584
- 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
+ );
3585
4060
  for (const c of changes) {
3586
4061
  if (c.type === "update") conflicts.push(`agents/${c.name}`);
3587
4062
  }
@@ -3642,9 +4117,14 @@ async function pull(args) {
3642
4117
  await pullHooks(api, platformDir, name || void 0, appName, projectId);
3643
4118
  console.log();
3644
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
+ }
3645
4125
  if (!type || type === "agents") {
3646
4126
  console.log(pc2.bold(" Agents:"));
3647
- await pullAgents(api, platformDir, name || void 0, projectId);
4127
+ await pullAgents(api, platformDir, name || void 0, projectId, appName);
3648
4128
  console.log();
3649
4129
  }
3650
4130
  if (!type || type === "mailboxes") {
@@ -3796,6 +4276,7 @@ ${pc2.dim("Usage:")}
3796
4276
 
3797
4277
  ${pc2.dim("Resources:")}
3798
4278
  agents/<name> Diff agent (system_prompt, policy_script)
4279
+ skills/<slug> Diff project skill instructions
3799
4280
  automations/<name> Diff automation code
3800
4281
  hooks/<name> Diff hook script
3801
4282
 
@@ -3944,23 +4425,70 @@ async function diff(args) {
3944
4425
  } else if (scriptChanged) {
3945
4426
  renderFullDiff(local.fileName, remote.script || "", local.script);
3946
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
+ }
3947
4466
  } else {
3948
- 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`));
3949
4468
  process.exit(1);
3950
4469
  }
3951
4470
  console.log();
3952
4471
  }
3953
4472
  export {
3954
4473
  apply,
4474
+ applyAgents,
3955
4475
  applyCollections,
4476
+ applySkills,
3956
4477
  convertCollectionToApiFormat,
3957
4478
  destroy,
3958
4479
  diff,
3959
4480
  list,
3960
4481
  loadLocalCollections,
4482
+ loadLocalSkills,
4483
+ localProjectSkillSlug,
3961
4484
  plan,
3962
4485
  planCollections,
4486
+ planSkills,
4487
+ projectSkillRefCandidates,
3963
4488
  pull,
3964
4489
  pullCollections,
4490
+ pullSkills,
4491
+ resolveAgentSkillRefs,
4492
+ scopedProjectSkillSlug,
3965
4493
  show
3966
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.28.0",
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.31.0,<0.32.0",
7
+ "lumera[functions]>=0.33.0,<0.34.0",
8
8
  ]
9
9
 
10
10
  [project.optional-dependencies]