@lunnoa/client 2.4.0 → 3.0.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.
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/index.ts
4
- import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
5
- import { resolve as resolve4 } from "path";
4
+ import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
5
+ import { resolve as resolve6 } from "path";
6
6
 
7
7
  // src/core/auth.ts
8
8
  var API_KEY_PREFIX = "lna_";
@@ -32,16 +32,21 @@ var LunnoaApiError = class extends Error {
32
32
  method;
33
33
  /** Path of the failed request (without the base URL). */
34
34
  path;
35
+ /** Present on definition-sync 409s when the server sends the current revision. */
36
+ currentRevision;
35
37
  constructor(args) {
36
38
  const detail = extractMessage(args.body);
39
+ const currentRevision = extractCurrentRevision(args.body);
40
+ const shown = [detail, currentRevision ? `currentRevision: ${currentRevision}` : void 0].filter(Boolean).join(" ");
37
41
  super(
38
- `Lunnoa API request failed: ${args.method} ${args.path} \u2192 ${args.status}${detail ? ` (${detail})` : ""}`
42
+ `Lunnoa API request failed: ${args.method} ${args.path} \u2192 ${args.status}${shown ? ` (${shown})` : ""}`
39
43
  );
40
44
  this.name = "LunnoaApiError";
41
45
  this.status = args.status;
42
46
  this.body = args.body;
43
47
  this.method = args.method;
44
48
  this.path = args.path;
49
+ this.currentRevision = currentRevision;
45
50
  }
46
51
  /** True for 401 responses (missing, revoked, or expired credential). */
47
52
  get isUnauthorized() {
@@ -60,6 +65,15 @@ var LunnoaApiError = class extends Error {
60
65
  return this.status === 429;
61
66
  }
62
67
  };
68
+ function publicApiMessage(error) {
69
+ const message = extractMessage(error.body) ?? error.message;
70
+ return error.currentRevision ? `${message} (currentRevision: ${error.currentRevision})` : message;
71
+ }
72
+ function extractCurrentRevision(body) {
73
+ if (!body || typeof body !== "object") return void 0;
74
+ const revision = body.currentRevision;
75
+ return typeof revision === "string" ? revision : void 0;
76
+ }
63
77
  function extractMessage(body) {
64
78
  if (body && typeof body === "object") {
65
79
  const record = body;
@@ -536,6 +550,121 @@ var ActionsResource = class {
536
550
  }
537
551
  };
538
552
 
553
+ // src/define/files.ts
554
+ var STEP_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
555
+ function hasFileAgentSignals(record) {
556
+ if (record.instructions && typeof record.instructions === "object") return true;
557
+ if (record.settings && typeof record.settings === "object") return true;
558
+ if ("workflows" in record || "agents" in record || "knowledge" in record || "variables" in record || "objects" in record || "objectTypes" in record || "mcpServers" in record || "a2aAgents" in record) {
559
+ return true;
560
+ }
561
+ return Array.isArray(record.tools) && record.tools.some(
562
+ (tool) => tool && typeof tool === "object" && !("kind" in tool) && !("use" in tool)
563
+ );
564
+ }
565
+ function isAgentFileShape(config) {
566
+ if (!config || typeof config !== "object") return false;
567
+ return hasFileAgentSignals(config);
568
+ }
569
+ function acceptAgentFile(config) {
570
+ const slug = config.slug?.trim();
571
+ if (!slug) throw new Error("defineAgent requires slug");
572
+ const instructions = config.instructions;
573
+ const fileRef = instructions && typeof instructions === "object" && typeof instructions.file === "string" && instructions.file.trim().length > 0;
574
+ if (typeof instructions !== "string" && !fileRef) {
575
+ throw new Error("defineAgent requires instructions");
576
+ }
577
+ const copy = { ...config };
578
+ delete copy.$schema;
579
+ delete copy.kind;
580
+ return { ...copy, kind: "agent", slug, instructions };
581
+ }
582
+ function assertStepKeys(steps, seen) {
583
+ for (const step of steps ?? []) {
584
+ if (!step?.use?.id?.trim()) {
585
+ throw new Error("workflow step requires use.id");
586
+ }
587
+ if (step.key !== void 0) {
588
+ if (!STEP_KEY_PATTERN.test(step.key) || step.key === "trigger") {
589
+ throw new Error(
590
+ `Step key "${step.key}" must start with a letter, use letters, digits, - or _, and not be "trigger".`
591
+ );
592
+ }
593
+ if (seen.has(step.key)) {
594
+ throw new Error(`Step key "${step.key}" is used twice.`);
595
+ }
596
+ seen.add(step.key);
597
+ }
598
+ for (const path of step.paths ?? []) {
599
+ assertStepKeys(path.steps ?? [], seen);
600
+ }
601
+ }
602
+ }
603
+ function acceptWorkflowStepsFile(config) {
604
+ const slug = config.slug?.trim();
605
+ if (!slug) throw new Error("defineWorkflow requires slug");
606
+ if (!config.steps?.length) {
607
+ throw new Error("defineWorkflow requires at least one step");
608
+ }
609
+ if (config.trigger && !config.trigger.id?.trim()) {
610
+ throw new Error("defineWorkflow trigger requires id");
611
+ }
612
+ assertStepKeys(config.steps, /* @__PURE__ */ new Set());
613
+ const copy = { ...config };
614
+ delete copy.$schema;
615
+ delete copy.kind;
616
+ return {
617
+ ...copy,
618
+ kind: "workflow",
619
+ format: "steps",
620
+ slug
621
+ };
622
+ }
623
+ function acceptWorkflowGraphFile(config) {
624
+ const slug = config.slug?.trim();
625
+ if (!slug) throw new Error("defineWorkflow requires slug");
626
+ if (!Array.isArray(config.nodes) || config.nodes.length === 0) {
627
+ throw new Error("defineWorkflow graph requires nodes");
628
+ }
629
+ const keys = /* @__PURE__ */ new Set();
630
+ let triggers = 0;
631
+ for (const node of config.nodes) {
632
+ if (!node?.key || typeof node.key !== "string") {
633
+ throw new Error("graph node requires key");
634
+ }
635
+ if (!STEP_KEY_PATTERN.test(node.key)) {
636
+ throw new Error(
637
+ `Node key "${node.key}" must start with a letter, use letters, digits, - or _, and not be empty.`
638
+ );
639
+ }
640
+ if (keys.has(node.key)) {
641
+ throw new Error(`Node key "${node.key}" is used twice.`);
642
+ }
643
+ keys.add(node.key);
644
+ if (node.nodeType === "trigger") triggers += 1;
645
+ }
646
+ if (triggers !== 1) {
647
+ throw new Error("defineWorkflow graph requires exactly one trigger");
648
+ }
649
+ for (const edge of config.edges ?? []) {
650
+ if (!keys.has(edge?.source) || !keys.has(edge?.target)) {
651
+ throw new Error(
652
+ `Edge from "${edge?.source ?? ""}" to "${edge?.target ?? ""}" references an unknown key.`
653
+ );
654
+ }
655
+ }
656
+ const copy = { ...config };
657
+ delete copy.$schema;
658
+ delete copy.kind;
659
+ return {
660
+ ...copy,
661
+ kind: "workflow",
662
+ format: "graph",
663
+ slug,
664
+ edges: config.edges ?? []
665
+ };
666
+ }
667
+
539
668
  // src/define/index.ts
540
669
  var DEFINE_KINDS = {
541
670
  connection: "connection",
@@ -546,6 +675,33 @@ var DEFINE_KINDS = {
546
675
  agent: "agent",
547
676
  entityType: "entityType"
548
677
  };
678
+ var PATH_CONDITIONS = [
679
+ "contains",
680
+ "does_not_contain",
681
+ "equals",
682
+ "does_not_equal",
683
+ "exists",
684
+ "does_not_exist",
685
+ "starts_with",
686
+ "ends_with",
687
+ "is_empty",
688
+ "is_not_empty",
689
+ "comes_before",
690
+ "comes_after",
691
+ "is_in_the_past",
692
+ "is_in_the_future",
693
+ "is_true",
694
+ "is_false",
695
+ "is_truthy",
696
+ "is_falsey",
697
+ "is_greater_than",
698
+ "is_less_than",
699
+ "is_greater_than_or_equal_to",
700
+ "is_less_than_or_equal_to",
701
+ "matches_regex",
702
+ "does_not_match_regex"
703
+ ];
704
+ var PATH_CONDITION_SET = new Set(PATH_CONDITIONS);
549
705
  function resolveActionAppId(action) {
550
706
  if (action.appId?.trim()) {
551
707
  return action.appId.trim();
@@ -646,10 +802,24 @@ function unwrapTool(tool) {
646
802
  }
647
803
  return tool;
648
804
  }
805
+ function syncFields(options) {
806
+ return {
807
+ ...options?.baseRevision ? { baseRevision: options.baseRevision } : {},
808
+ ...options?.force ? { force: true } : {}
809
+ };
810
+ }
811
+ function idsOf(links) {
812
+ if (!links) return void 0;
813
+ return links.map((link) => link.id);
814
+ }
649
815
  function agentDefinitionToUpsert(def, options) {
816
+ if (isAgentFileShape(def)) {
817
+ return fileAgentToUpsert(def, options);
818
+ }
650
819
  const tools = [];
651
820
  const workflowIds = [...options?.workflowIds ?? []];
652
- for (const tool of def.tools ?? []) {
821
+ const authored = def;
822
+ for (const tool of authored.tools ?? []) {
653
823
  const unwrapped = unwrapTool(tool);
654
824
  if (unwrapped.kind === DEFINE_KINDS.workflow) {
655
825
  continue;
@@ -665,17 +835,65 @@ function agentDefinitionToUpsert(def, options) {
665
835
  });
666
836
  }
667
837
  }
838
+ return {
839
+ slug: authored.slug,
840
+ name: authored.name,
841
+ description: authored.description,
842
+ instructions: authored.instructions,
843
+ model: authored.model,
844
+ aiConnectionId: authored.aiConnection?.id,
845
+ connectionIds: authored.connections?.map((c) => c.id).filter((id) => Boolean(id)),
846
+ workflowIds: workflowIds.length ? workflowIds : void 0,
847
+ tools,
848
+ codeSource: options?.codeSource ?? "generated",
849
+ ...syncFields(options)
850
+ };
851
+ }
852
+ function fileAgentToUpsert(def, options) {
853
+ if (typeof def.instructions !== "string") {
854
+ throw new Error(
855
+ "Agent instructions are still a file reference. The CLI resolves these before push."
856
+ );
857
+ }
858
+ const tools = [];
859
+ for (const tool of def.tools ?? []) {
860
+ const action = {
861
+ kind: DEFINE_KINDS.action,
862
+ id: tool.id,
863
+ appId: tool.appId,
864
+ connection: tool.connection?.id ? { kind: DEFINE_KINDS.connection, id: tool.connection.id } : void 0
865
+ };
866
+ tools.push({
867
+ key: tool.key,
868
+ appId: resolveActionAppId(action),
869
+ actionId: resolveActionId(action),
870
+ connectionId: tool.connection?.id,
871
+ name: tool.name,
872
+ description: tool.description,
873
+ input: tool.input
874
+ });
875
+ }
668
876
  return {
669
877
  slug: def.slug,
670
878
  name: def.name,
671
- description: def.description,
879
+ // A file is the whole definition. Omitted description and model are empty.
880
+ description: def.description ?? null,
672
881
  instructions: def.instructions,
673
- model: def.model,
882
+ model: def.model ?? null,
674
883
  aiConnectionId: def.aiConnection?.id,
675
- connectionIds: def.connections?.map((c) => c.id).filter((id) => Boolean(id)),
676
- workflowIds: workflowIds.length ? workflowIds : void 0,
677
- tools,
678
- managedByCode: true
884
+ ...def.settings ? { settings: def.settings } : {},
885
+ ...def.tools ? { tools } : {},
886
+ ...def.workflows ? { workflows: def.workflows } : {},
887
+ ...def.agents ? { agents: def.agents } : {},
888
+ ...def.connections ? { connectionIds: idsOf(def.connections) } : {},
889
+ ...def.knowledge ? { knowledgeIds: idsOf(def.knowledge) } : {},
890
+ ...def.variables ? { variableIds: idsOf(def.variables) } : {},
891
+ ...def.objects ? { objectIds: idsOf(def.objects) } : {},
892
+ ...def.objectTypes ? { objectTypeIds: idsOf(def.objectTypes) } : {},
893
+ ...def.mcpServers ? { mcpServers: def.mcpServers } : {},
894
+ ...def.a2aAgents ? { a2aAgentIds: idsOf(def.a2aAgents) } : {},
895
+ codeSource: "synced",
896
+ ...syncFields(options)
679
897
  };
680
898
  }
681
899
  var AgentsResource = class {
@@ -695,15 +913,22 @@ var AgentsResource = class {
695
913
  query: serializeListQuery(options)
696
914
  });
697
915
  }
916
+ /** Exported definition for `lunnoa/agents/<slug>.json`. */
917
+ getDefinition(slug) {
918
+ return this.http.get(
919
+ `/api/agents/slug/${encodeURIComponent(slug)}/definition`
920
+ );
921
+ }
698
922
  /**
699
923
  * Upsert an agent by slug inside the workspace on the API key.
700
924
  * Prefer the CLI (`npx @lunnoa/client agents deploy`) for happy-path deploy.
701
925
  */
702
926
  upsertBySlug(body, options) {
703
927
  const payload = "kind" in body && body.kind === "agent" ? agentDefinitionToUpsert(body, options) : body;
704
- return this.http.post("/api/agents/upsert", {
705
- body: payload
706
- });
928
+ return this.http.post(
929
+ "/api/agents/upsert",
930
+ { body: payload }
931
+ );
707
932
  }
708
933
  };
709
934
 
@@ -1500,7 +1725,7 @@ function computeDurationMs(start, end) {
1500
1725
  return b - a;
1501
1726
  }
1502
1727
  function defaultSleep(ms) {
1503
- return new Promise((resolve5) => setTimeout(resolve5, ms));
1728
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
1504
1729
  }
1505
1730
 
1506
1731
  // src/resources/knowledge.ts
@@ -1805,6 +2030,11 @@ function stepFromDefinition(step) {
1805
2030
  ...step.name ? { name: step.name } : {}
1806
2031
  };
1807
2032
  }
2033
+ if (step.paths?.length) {
2034
+ throw new Error(
2035
+ "runs.start runs straight lines only. Deploy workflows with paths, then execute them."
2036
+ );
2037
+ }
1808
2038
  const use = step.use;
1809
2039
  const input = {
1810
2040
  ...use.input ?? {},
@@ -1822,6 +2052,11 @@ function stepFromDefinition(step) {
1822
2052
  }
1823
2053
  function normalizeStartRunInput(input) {
1824
2054
  if (isWorkflowDefinition(input)) {
2055
+ if ("format" in input && input.format === "graph") {
2056
+ throw new Error(
2057
+ "runs.start runs straight lines only. Push the graph workflow, then execute it."
2058
+ );
2059
+ }
1825
2060
  return {
1826
2061
  steps: input.steps.map(stepFromDefinition),
1827
2062
  name: input.name ?? input.slug
@@ -2013,6 +2248,18 @@ var WorkflowAppsResource = class {
2013
2248
  }
2014
2249
  };
2015
2250
 
2251
+ // src/resources/code-sync.ts
2252
+ var CodeSyncResource = class {
2253
+ constructor(http) {
2254
+ this.http = http;
2255
+ }
2256
+ http;
2257
+ /** Workspace id and whether synced resources are locked in the UI. */
2258
+ context() {
2259
+ return this.http.get("/api/code-sync/context");
2260
+ }
2261
+ };
2262
+
2016
2263
  // src/resources/workflows.ts
2017
2264
  function triggerToUpsertInput(trigger) {
2018
2265
  return {
@@ -2025,8 +2272,38 @@ function triggerToUpsertInput(trigger) {
2025
2272
  ...trigger.description ? { description: trigger.description } : {}
2026
2273
  };
2027
2274
  }
2275
+ function asAction(use) {
2276
+ if (use && typeof use === "object" && "kind" in use && use.kind === DEFINE_KINDS.action) {
2277
+ return use;
2278
+ }
2279
+ const fileUse = use;
2280
+ return {
2281
+ kind: DEFINE_KINDS.action,
2282
+ id: fileUse.id,
2283
+ appId: fileUse.appId,
2284
+ connection: fileUse.connection?.id ? { kind: DEFINE_KINDS.connection, id: fileUse.connection.id } : void 0
2285
+ };
2286
+ }
2287
+ function asTrigger(trigger) {
2288
+ if ("kind" in trigger && trigger.kind === DEFINE_KINDS.trigger) {
2289
+ return trigger;
2290
+ }
2291
+ const fileTrigger = trigger;
2292
+ return {
2293
+ kind: DEFINE_KINDS.trigger,
2294
+ id: fileTrigger.id,
2295
+ appId: fileTrigger.appId,
2296
+ connection: fileTrigger.connection?.id ? { kind: DEFINE_KINDS.connection, id: fileTrigger.connection.id } : void 0,
2297
+ input: fileTrigger.input,
2298
+ inputs: fileTrigger.inputs,
2299
+ name: fileTrigger.name,
2300
+ description: fileTrigger.description
2301
+ };
2302
+ }
2028
2303
  function stepToUpsertInput(step) {
2029
- const use = step.use;
2304
+ const use = asAction(step.use);
2305
+ const retry = "retrySettings" in step ? step.retrySettings : void 0;
2306
+ const continueOnFailure = "continueOnFailure" in step ? step.continueOnFailure : void 0;
2030
2307
  return {
2031
2308
  ...step.key ? { key: step.key } : {},
2032
2309
  appId: resolveActionAppId(use),
@@ -2037,18 +2314,60 @@ function stepToUpsertInput(step) {
2037
2314
  ...step.input ?? {}
2038
2315
  },
2039
2316
  name: step.name ?? use.name,
2040
- description: use.description
2317
+ description: step.description ?? use.description,
2318
+ ...retry ? { retrySettings: retry } : {},
2319
+ ...continueOnFailure ? { continueOnFailure } : {},
2320
+ ...step.paths?.length ? {
2321
+ paths: step.paths.map((path) => ({
2322
+ label: path.label,
2323
+ ...path.when ? { when: path.when } : {},
2324
+ steps: path.steps.map(
2325
+ (nested) => stepToUpsertInput(nested)
2326
+ )
2327
+ }))
2328
+ } : {}
2041
2329
  };
2042
2330
  }
2043
- function workflowDefinitionToUpsert(def) {
2331
+ function syncFields2(options) {
2044
2332
  return {
2045
- slug: def.slug,
2046
- name: def.name,
2047
- description: def.description,
2048
- isActive: def.isActive,
2049
- ...def.trigger ? { trigger: triggerToUpsertInput(def.trigger) } : {},
2050
- managedByCode: true,
2051
- steps: def.steps.map(stepToUpsertInput)
2333
+ ...options?.baseRevision ? { baseRevision: options.baseRevision } : {},
2334
+ ...options?.force ? { force: true } : {}
2335
+ };
2336
+ }
2337
+ function workflowDefinitionToUpsert(def, options) {
2338
+ if ("format" in def && def.format === "graph") {
2339
+ return {
2340
+ slug: def.slug,
2341
+ name: def.name,
2342
+ // A file is the whole definition: an omitted description is empty, not "leave it".
2343
+ description: def.description ?? null,
2344
+ codeSource: "synced",
2345
+ graph: { nodes: def.nodes, edges: def.edges ?? [] },
2346
+ ...syncFields2(options)
2347
+ };
2348
+ }
2349
+ if ("format" in def && def.format === "steps") {
2350
+ const file = def;
2351
+ return {
2352
+ slug: file.slug,
2353
+ name: file.name,
2354
+ description: file.description ?? null,
2355
+ ...file.trigger ? { trigger: triggerToUpsertInput(asTrigger(file.trigger)) } : {},
2356
+ codeSource: "synced",
2357
+ steps: file.steps.map((step) => stepToUpsertInput(step)),
2358
+ ...syncFields2(options)
2359
+ };
2360
+ }
2361
+ const authored = def;
2362
+ return {
2363
+ slug: authored.slug,
2364
+ name: authored.name,
2365
+ description: authored.description,
2366
+ isActive: authored.isActive,
2367
+ ...authored.trigger ? { trigger: triggerToUpsertInput(authored.trigger) } : {},
2368
+ codeSource: options?.codeSource ?? "generated",
2369
+ ...syncFields2(options),
2370
+ steps: authored.steps.map((step) => stepToUpsertInput(step))
2052
2371
  };
2053
2372
  }
2054
2373
  var WorkflowsResource = class {
@@ -2073,15 +2392,22 @@ var WorkflowsResource = class {
2073
2392
  query: serializeListQuery(options)
2074
2393
  });
2075
2394
  }
2395
+ /** Exported definition for `lunnoa/workflows/<slug>.json`. */
2396
+ getDefinition(slug) {
2397
+ return this.http.get(
2398
+ `/api/workflows/slug/${encodeURIComponent(slug)}/definition`
2399
+ );
2400
+ }
2076
2401
  /**
2077
2402
  * Upsert a workflow by slug inside the workspace on the API key.
2078
2403
  * Prefer the CLI (`npx @lunnoa/client workflows deploy`) for happy-path deploy.
2079
2404
  */
2080
- upsertBySlug(body) {
2081
- const payload = "kind" in body && body.kind === "workflow" ? workflowDefinitionToUpsert(body) : body;
2082
- return this.http.post("/api/workflows/upsert", {
2083
- body: payload
2084
- });
2405
+ upsertBySlug(body, options) {
2406
+ const payload = "kind" in body && body.kind === "workflow" ? workflowDefinitionToUpsert(body, options) : body;
2407
+ return this.http.post(
2408
+ "/api/workflows/upsert",
2409
+ { body: payload }
2410
+ );
2085
2411
  }
2086
2412
  /**
2087
2413
  * Starts a new execution of the workflow with the given trigger inputs.
@@ -2147,6 +2473,8 @@ var LunnoaClient = class {
2147
2473
  aiConnections;
2148
2474
  workflowApps;
2149
2475
  discovery;
2476
+ /** Workspace id for the definitions lock, and the code-sync lock flag. */
2477
+ codeSync;
2150
2478
  constructor(options) {
2151
2479
  this.http = new HttpClient(options);
2152
2480
  this.auth = new AuthResource(this.http, this.http.tokenStore);
@@ -2170,6 +2498,7 @@ var LunnoaClient = class {
2170
2498
  this.connections = new ConnectionsResource(this.http);
2171
2499
  this.aiConnections = new AiConnectionsResource(this.http);
2172
2500
  this.discovery = new DiscoveryResource(this.http);
2501
+ this.codeSync = new CodeSyncResource(this.http);
2173
2502
  }
2174
2503
  };
2175
2504
 
@@ -2191,7 +2520,8 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
2191
2520
  "--access-token",
2192
2521
  "--out",
2193
2522
  "--app",
2194
- "--template"
2523
+ "--template",
2524
+ "--dir"
2195
2525
  ]);
2196
2526
  function requireFlagValue(flag, value) {
2197
2527
  if (!value || value.startsWith("--")) {
@@ -2205,6 +2535,8 @@ function parseArgs(argv) {
2205
2535
  out: "./src/lunnoa",
2206
2536
  json: false,
2207
2537
  forAgent: false,
2538
+ all: false,
2539
+ force: false,
2208
2540
  help: false
2209
2541
  };
2210
2542
  let i = 1;
@@ -2233,6 +2565,14 @@ function parseArgs(argv) {
2233
2565
  args.forAgent = true;
2234
2566
  continue;
2235
2567
  }
2568
+ if (flag === "--all") {
2569
+ args.all = true;
2570
+ continue;
2571
+ }
2572
+ if (flag === "--force") {
2573
+ args.force = true;
2574
+ continue;
2575
+ }
2236
2576
  if (flag && VALUE_FLAGS.has(flag)) {
2237
2577
  const value = requireFlagValue(flag, argv[i + 1]);
2238
2578
  switch (flag) {
@@ -2254,6 +2594,9 @@ function parseArgs(argv) {
2254
2594
  case "--template":
2255
2595
  args.template = value;
2256
2596
  break;
2597
+ case "--dir":
2598
+ args.dir = value;
2599
+ break;
2257
2600
  default:
2258
2601
  break;
2259
2602
  }
@@ -2292,6 +2635,12 @@ var USAGE = `Usage:
2292
2635
  npx @lunnoa/client workflows deploy <path> --url <url> --api-key <key>
2293
2636
  npx @lunnoa/client agents deploy <path> --url <url> --api-key <key>
2294
2637
  npx @lunnoa/client entity-types deploy <path> --url <url> --api-key <key>
2638
+ .json is pushed (code synced). .ts / .js / .mjs is deployed from TypeScript (code generated).
2639
+
2640
+ Definitions folder (default ./lunnoa, or definitionsDir in lunnoa.config.*, or --dir):
2641
+ npx @lunnoa/client pull [--all | <slug> | agents/<slug> | workflows/<slug>] [--force]
2642
+ npx @lunnoa/client push [<path|slug>] [--force]
2643
+ npx @lunnoa/client status [--json]
2295
2644
 
2296
2645
  Agent skills (copies this version's skills into .claude/skills and .cursor/skills):
2297
2646
  npx @lunnoa/client skills install [projectDir]
@@ -2305,6 +2654,9 @@ Options:
2305
2654
  --for-agent Only catalogue entries with availableForAgent
2306
2655
  --template init starter: GitHub owner/repo[#ref] or local folder (default: lunnoa-automate/lunnoa-starter)
2307
2656
  --json Machine-readable JSON (preferred for coding agents)
2657
+ --dir Definitions folder (default: ./lunnoa or definitionsDir in lunnoa.config)
2658
+ --all pull every agent and workflow in the workspace
2659
+ --force pull: overwrite a local edit; push: skip the revision check
2308
2660
  --help Show this help
2309
2661
 
2310
2662
  Environment: LUNNOA_URL, LUNNOA_API_KEY (also read from ./.env)
@@ -2321,6 +2673,682 @@ A slug already granted to other workspaces is not rewritten. Unknown instance at
2321
2673
  allowUnknownAttributes is true; use extensions.
2322
2674
  `;
2323
2675
 
2676
+ // src/cli/print.ts
2677
+ function printJson(value) {
2678
+ console.log(JSON.stringify(value, null, 2));
2679
+ }
2680
+ function formatTable(rows, columns) {
2681
+ if (rows.length === 0) {
2682
+ return "No results.";
2683
+ }
2684
+ const widths = columns.map((col) => {
2685
+ let width = col.header.length;
2686
+ for (const row of rows) {
2687
+ width = Math.max(width, stringifyCell(row[col.key]).length);
2688
+ }
2689
+ return width;
2690
+ });
2691
+ const header = columns.map((col, i) => pad(col.header, widths[i])).join(" ");
2692
+ const divider = widths.map((w) => "-".repeat(w)).join(" ");
2693
+ const body = rows.map(
2694
+ (row) => columns.map((col, i) => pad(stringifyCell(row[col.key]), widths[i])).join(" ")
2695
+ );
2696
+ return [header, divider, ...body].join("\n");
2697
+ }
2698
+ function printTable(rows, columns) {
2699
+ console.log(formatTable(rows, columns));
2700
+ }
2701
+ function stringifyCell(value) {
2702
+ if (value === null || value === void 0) {
2703
+ return "";
2704
+ }
2705
+ if (typeof value === "boolean") {
2706
+ return value ? "yes" : "no";
2707
+ }
2708
+ return String(value);
2709
+ }
2710
+ function pad(value, width) {
2711
+ return value.length >= width ? value : `${value}${" ".repeat(width - value.length)}`;
2712
+ }
2713
+
2714
+ // src/cli/sync/engine.ts
2715
+ import { resolve as resolve2 } from "path";
2716
+
2717
+ // src/cli/sync/diff.ts
2718
+ var SECRET_NAME = /authorization|token|secret|password|passwd|api[-_]?key|bearer|cookie|private[-_]?key/i;
2719
+ function findSecretFields(definition) {
2720
+ if (!definition || typeof definition !== "object") return [];
2721
+ const record = definition;
2722
+ const hits = [];
2723
+ if (Array.isArray(record.tools)) {
2724
+ record.tools.forEach((tool, index) => {
2725
+ walk(tool?.input, `tools[${index}].input`, hits);
2726
+ });
2727
+ }
2728
+ if (record.trigger && typeof record.trigger === "object") {
2729
+ walk(record.trigger.input, "trigger.input", hits);
2730
+ }
2731
+ if (Array.isArray(record.steps)) {
2732
+ record.steps.forEach((step, index) => walkStep(step, `steps[${index}]`, hits));
2733
+ }
2734
+ if (Array.isArray(record.nodes)) {
2735
+ record.nodes.forEach((node, index) => {
2736
+ walk(node?.value, `nodes[${index}].value`, hits);
2737
+ });
2738
+ }
2739
+ return hits;
2740
+ }
2741
+ function walkStep(step, path, hits) {
2742
+ if (!step || typeof step !== "object") return;
2743
+ const record = step;
2744
+ walk(record.input, `${path}.input`, hits);
2745
+ record.paths?.forEach((pathDef, index) => {
2746
+ pathDef?.steps?.forEach((nested, stepIndex) => {
2747
+ walkStep(nested, `${path}.paths[${index}].steps[${stepIndex}]`, hits);
2748
+ });
2749
+ });
2750
+ }
2751
+ function walk(value, path, hits) {
2752
+ if (Array.isArray(value)) {
2753
+ value.forEach((item, index) => walk(item, `${path}[${index}]`, hits));
2754
+ return;
2755
+ }
2756
+ if (!value || typeof value !== "object") return;
2757
+ for (const [key, child] of Object.entries(value)) {
2758
+ const childPath = `${path}.${key}`;
2759
+ if (typeof child === "string" && child.trim() && SECRET_NAME.test(key) && !child.includes("={{")) {
2760
+ hits.push(childPath);
2761
+ } else {
2762
+ walk(child, childPath, hits);
2763
+ }
2764
+ }
2765
+ }
2766
+ function redactSecrets(text) {
2767
+ return text.replace(
2768
+ /("(?:\\.|[^"\\])*(?:authorization|token|secret|password|passwd|api[-_]?key|bearer|cookie|private[-_]?key)(?:\\.|[^"\\])*")(\s*:\s*)"((?:\\.|[^"\\])*)"/gi,
2769
+ (match, key, colon, value) => value.includes("={{") ? match : `${key}${colon}"***"`
2770
+ );
2771
+ }
2772
+ function lineDiff(local, server) {
2773
+ const a = local.split("\n");
2774
+ const b = server.split("\n");
2775
+ if (a.length * b.length > 1e6) {
2776
+ const lines = a.map((line) => `-${line}`);
2777
+ lines.push("---");
2778
+ lines.push(...b.map((line) => `+${line}`));
2779
+ return lines.join("\n");
2780
+ }
2781
+ const dp = Array.from(
2782
+ { length: a.length + 1 },
2783
+ () => new Array(b.length + 1).fill(0)
2784
+ );
2785
+ for (let i2 = a.length - 1; i2 >= 0; i2 -= 1) {
2786
+ for (let j2 = b.length - 1; j2 >= 0; j2 -= 1) {
2787
+ dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
2788
+ }
2789
+ }
2790
+ const out = [];
2791
+ let i = 0;
2792
+ let j = 0;
2793
+ while (i < a.length && j < b.length) {
2794
+ if (a[i] === b[j]) {
2795
+ out.push(` ${a[i]}`);
2796
+ i += 1;
2797
+ j += 1;
2798
+ } else if (dp[i + 1][j] >= dp[i][j + 1]) {
2799
+ out.push(`-${a[i]}`);
2800
+ i += 1;
2801
+ } else {
2802
+ out.push(`+${b[j]}`);
2803
+ j += 1;
2804
+ }
2805
+ }
2806
+ while (i < a.length) out.push(`-${a[i++]}`);
2807
+ while (j < b.length) out.push(`+${b[j++]}`);
2808
+ return out.join("\n");
2809
+ }
2810
+ function formatConflict(label, local, server) {
2811
+ return `${label}
2812
+ --- local
2813
+ +++ server
2814
+ ${lineDiff(redactSecrets(local), redactSecrets(server))}`;
2815
+ }
2816
+
2817
+ // src/cli/sync/store.ts
2818
+ import { createHash } from "crypto";
2819
+ import { lstat, mkdir, readFile, readdir, realpath, writeFile } from "fs/promises";
2820
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "path";
2821
+ var PathEscapeError = class extends Error {
2822
+ constructor(target) {
2823
+ super(`Refusing to write outside the definitions folder: ${target}`);
2824
+ this.name = "PathEscapeError";
2825
+ }
2826
+ };
2827
+ function isOutsideDefinitionsDir(root, target) {
2828
+ const rootAbs = resolve(root);
2829
+ const abs = resolve(rootAbs, target);
2830
+ const rel = relative(rootAbs, abs);
2831
+ return rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel);
2832
+ }
2833
+ async function assertInsideDefinitionsDir(root, target) {
2834
+ const rootAbs = resolve(root);
2835
+ const abs = resolve(rootAbs, target);
2836
+ if (isOutsideDefinitionsDir(rootAbs, abs)) {
2837
+ throw new PathEscapeError(target);
2838
+ }
2839
+ let rootReal = rootAbs;
2840
+ try {
2841
+ rootReal = await realpath(rootAbs);
2842
+ } catch (error) {
2843
+ if (error.code !== "ENOENT") throw error;
2844
+ }
2845
+ const parts = relative(rootAbs, abs).split(sep).filter(Boolean);
2846
+ let cursor = rootAbs;
2847
+ for (const part of parts) {
2848
+ if (part === ".." || part === ".") throw new PathEscapeError(target);
2849
+ const next = resolve(cursor, part);
2850
+ let stats;
2851
+ try {
2852
+ stats = await lstat(next);
2853
+ } catch (error) {
2854
+ if (error.code === "ENOENT") return abs;
2855
+ throw error;
2856
+ }
2857
+ if (stats.isSymbolicLink()) {
2858
+ const real = await realpath(next);
2859
+ if (isOutsideDefinitionsDir(rootReal, real)) throw new PathEscapeError(target);
2860
+ cursor = real;
2861
+ continue;
2862
+ }
2863
+ cursor = next;
2864
+ }
2865
+ if (isOutsideDefinitionsDir(rootReal, cursor)) throw new PathEscapeError(target);
2866
+ return cursor;
2867
+ }
2868
+ async function writeGuarded(root, target, contents) {
2869
+ const abs = await assertInsideDefinitionsDir(root, target);
2870
+ await mkdir(dirname(abs), { recursive: true });
2871
+ await writeFile(abs, contents);
2872
+ }
2873
+ function hashDefinition(json, instructions) {
2874
+ const hash = createHash("sha256");
2875
+ hash.update(json);
2876
+ if (instructions !== void 0) {
2877
+ hash.update("\0");
2878
+ hash.update(instructions);
2879
+ }
2880
+ return hash.digest("hex");
2881
+ }
2882
+ function deploymentKey(baseUrl, workspaceId) {
2883
+ return `${baseUrl.replace(/\/+$/, "")}#${workspaceId}`;
2884
+ }
2885
+ function emptyLock() {
2886
+ return { version: 1, deployments: {} };
2887
+ }
2888
+ async function readLock(root) {
2889
+ try {
2890
+ const text = await readFile(resolve(root, "lunnoa.lock.json"), "utf8");
2891
+ const parsed = JSON.parse(text);
2892
+ if (!parsed || parsed.version !== 1 || !parsed.deployments || typeof parsed.deployments !== "object") {
2893
+ throw new Error("lunnoa.lock.json is not a version 1 lock file.");
2894
+ }
2895
+ return parsed;
2896
+ } catch (error) {
2897
+ if (error.code === "ENOENT") return emptyLock();
2898
+ throw error;
2899
+ }
2900
+ }
2901
+ async function writeLock(root, lock) {
2902
+ await writeGuarded(root, "lunnoa.lock.json", `${JSON.stringify(lock, null, 2)}
2903
+ `);
2904
+ }
2905
+ function lockBucket(lock, key) {
2906
+ const existing = lock.deployments[key] ?? { agents: {}, workflows: {} };
2907
+ existing.agents ??= {};
2908
+ existing.workflows ??= {};
2909
+ lock.deployments[key] = existing;
2910
+ return existing;
2911
+ }
2912
+ function schemaRelativePath(jsonAbsolutePath, kind, projectRoot) {
2913
+ const target = resolve(
2914
+ projectRoot,
2915
+ "node_modules",
2916
+ "@lunnoa",
2917
+ "client",
2918
+ "schemas",
2919
+ `${kind}.schema.json`
2920
+ );
2921
+ let rel = relative(dirname(jsonAbsolutePath), target).split(sep).join("/");
2922
+ if (!rel.startsWith(".")) rel = `./${rel}`;
2923
+ return rel;
2924
+ }
2925
+ function serializeDefinition(definition, schema) {
2926
+ const body = {};
2927
+ for (const [key, value] of Object.entries(definition)) {
2928
+ if (key === "$schema") continue;
2929
+ body[key] = value;
2930
+ }
2931
+ return `${JSON.stringify({ $schema: schema, ...body }, null, 2)}
2932
+ `;
2933
+ }
2934
+ function renderAgentFiles(definition, schema) {
2935
+ if (typeof definition.instructions !== "string") {
2936
+ throw new Error(
2937
+ "Cannot write an agent definition whose instructions are still a file reference."
2938
+ );
2939
+ }
2940
+ const instructionsFile = `${definition.slug}.instructions.md`;
2941
+ const json = serializeDefinition(
2942
+ { ...definition, instructions: { file: instructionsFile } },
2943
+ schema
2944
+ );
2945
+ return { json, instructions: definition.instructions, instructionsFile };
2946
+ }
2947
+ function renderWorkflowFile(definition, schema) {
2948
+ return serializeDefinition({ ...definition }, schema);
2949
+ }
2950
+ async function writeAgentDefinition(root, projectRoot, definition) {
2951
+ const jsonRel = join("agents", `${definition.slug}.json`);
2952
+ const schema = schemaRelativePath(resolve(root, jsonRel), "agent", projectRoot);
2953
+ const rendered = renderAgentFiles(definition, schema);
2954
+ await writeGuarded(root, jsonRel, rendered.json);
2955
+ await writeGuarded(root, join("agents", rendered.instructionsFile), rendered.instructions);
2956
+ return hashDefinition(rendered.json, rendered.instructions);
2957
+ }
2958
+ async function writeWorkflowDefinition(root, projectRoot, definition) {
2959
+ const jsonRel = join("workflows", `${definition.slug}.json`);
2960
+ const schema = schemaRelativePath(resolve(root, jsonRel), "workflow", projectRoot);
2961
+ const json = renderWorkflowFile(definition, schema);
2962
+ await writeGuarded(root, jsonRel, json);
2963
+ return hashDefinition(json);
2964
+ }
2965
+ async function readInside(root, rel) {
2966
+ const lexical = resolve(root, rel);
2967
+ if (isOutsideDefinitionsDir(root, lexical)) throw new PathEscapeError(rel);
2968
+ try {
2969
+ const abs = await assertInsideDefinitionsDir(root, rel);
2970
+ return await readFile(abs, "utf8");
2971
+ } catch (error) {
2972
+ if (error instanceof PathEscapeError) throw error;
2973
+ if (error.code === "ENOENT") return void 0;
2974
+ throw error;
2975
+ }
2976
+ }
2977
+ async function hashOnDisk(root, kind, slug) {
2978
+ if (kind === "workflow") {
2979
+ const text = await readInside(root, join("workflows", `${slug}.json`));
2980
+ return text === void 0 ? void 0 : hashDefinition(text);
2981
+ }
2982
+ const json = await readInside(root, join("agents", `${slug}.json`));
2983
+ if (json === void 0) return void 0;
2984
+ const parsed = JSON.parse(json);
2985
+ if (parsed.instructions && typeof parsed.instructions === "object" && typeof parsed.instructions.file === "string") {
2986
+ const instructions = await readInside(root, join("agents", parsed.instructions.file));
2987
+ return hashDefinition(json, instructions ?? "");
2988
+ }
2989
+ return hashDefinition(json);
2990
+ }
2991
+ async function listDefinitionFiles(root) {
2992
+ const out = [];
2993
+ for (const kind of ["workflow", "agent"]) {
2994
+ const folder = kind === "agent" ? "agents" : "workflows";
2995
+ let names;
2996
+ try {
2997
+ names = await readdir(resolve(root, folder));
2998
+ } catch (error) {
2999
+ if (error.code === "ENOENT") continue;
3000
+ throw error;
3001
+ }
3002
+ for (const name of names.sort()) {
3003
+ if (!name.endsWith(".json")) continue;
3004
+ out.push({
3005
+ kind,
3006
+ slug: name.slice(0, -".json".length),
3007
+ path: resolve(root, folder, name)
3008
+ });
3009
+ }
3010
+ }
3011
+ return out;
3012
+ }
3013
+ async function readLocalText(root, kind, slug) {
3014
+ const folder = kind === "agent" ? "agents" : "workflows";
3015
+ const json = await readInside(root, join(folder, `${slug}.json`));
3016
+ if (json === void 0) return void 0;
3017
+ if (kind === "workflow") return json;
3018
+ const parsed = JSON.parse(json);
3019
+ if (parsed.instructions && typeof parsed.instructions === "object" && typeof parsed.instructions.file === "string") {
3020
+ const md = await readInside(root, join("agents", parsed.instructions.file));
3021
+ return `${json}--- instructions ---
3022
+ ${md ?? ""}`;
3023
+ }
3024
+ return json;
3025
+ }
3026
+ async function loadDefinitionForPush(root, filePath) {
3027
+ let text;
3028
+ try {
3029
+ text = await readFile(filePath, "utf8");
3030
+ } catch (error) {
3031
+ if (error.code === "ENOENT") {
3032
+ throw new Error(`Definition file not found: ${filePath}`);
3033
+ }
3034
+ throw error;
3035
+ }
3036
+ let parsed;
3037
+ try {
3038
+ parsed = JSON.parse(text);
3039
+ } catch {
3040
+ throw new Error(`Invalid JSON in ${filePath}`);
3041
+ }
3042
+ if (parsed.kind === "agent") {
3043
+ const agent = acceptAgentFile(parsed);
3044
+ if (typeof agent.instructions === "object") {
3045
+ const mdPath = resolve(dirname(resolve(filePath)), agent.instructions.file);
3046
+ const boundary = isOutsideDefinitionsDir(root, resolve(filePath)) ? dirname(resolve(filePath)) : root;
3047
+ await assertInsideDefinitionsDir(boundary, mdPath);
3048
+ const instructions = await readFile(mdPath, "utf8");
3049
+ return { kind: "agent", definition: { ...agent, instructions } };
3050
+ }
3051
+ return { kind: "agent", definition: agent };
3052
+ }
3053
+ if (parsed.kind === "workflow" || parsed.format === "steps" || parsed.format === "graph") {
3054
+ if (parsed.format === "graph" || Array.isArray(parsed.nodes) && !parsed.steps) {
3055
+ return {
3056
+ kind: "workflow",
3057
+ definition: acceptWorkflowGraphFile(parsed)
3058
+ };
3059
+ }
3060
+ return {
3061
+ kind: "workflow",
3062
+ definition: acceptWorkflowStepsFile(parsed)
3063
+ };
3064
+ }
3065
+ throw new Error(`Definition JSON needs kind "agent" or "workflow": ${filePath}`);
3066
+ }
3067
+
3068
+ // src/cli/sync/engine.ts
3069
+ var SyncConflictError = class extends Error {
3070
+ constructor(message) {
3071
+ super(message);
3072
+ this.name = "SyncConflictError";
3073
+ }
3074
+ };
3075
+ function folderOf(kind) {
3076
+ return kind === "agent" ? "agents" : "workflows";
3077
+ }
3078
+ function labelOf(kind, slug) {
3079
+ return `${folderOf(kind)}/${slug}.json`;
3080
+ }
3081
+ async function fetchDefinition(client, kind, slug) {
3082
+ try {
3083
+ const body = kind === "agent" ? await client.agents.getDefinition(slug) : await client.workflows.getDefinition(slug);
3084
+ return {
3085
+ kind,
3086
+ id: body.id,
3087
+ slug: body.definition?.slug || body.slug,
3088
+ revision: body.revision,
3089
+ definition: body.definition
3090
+ };
3091
+ } catch (error) {
3092
+ if (error instanceof LunnoaApiError && error.isNotFound) return null;
3093
+ throw error;
3094
+ }
3095
+ }
3096
+ function warnSecrets(kind, slug, definition) {
3097
+ for (const field of findSecretFields(definition)) {
3098
+ console.error(
3099
+ `Warning: ${labelOf(kind, slug)} field "${field}" looks like a secret. Definitions are committed; keep secrets in Lunnoa connections.`
3100
+ );
3101
+ }
3102
+ }
3103
+ function serverText(root, projectRoot, remote) {
3104
+ const rel = labelOf(remote.kind, remote.slug);
3105
+ if (remote.kind === "agent") {
3106
+ const schema2 = schemaRelativePath(resolve2(root, rel), "agent", projectRoot);
3107
+ const rendered = renderAgentFiles(remote.definition, schema2);
3108
+ return `${rendered.json}--- instructions ---
3109
+ ${rendered.instructions}`;
3110
+ }
3111
+ const schema = schemaRelativePath(resolve2(root, rel), "workflow", projectRoot);
3112
+ return renderWorkflowFile(remote.definition, schema);
3113
+ }
3114
+ async function resolvePullTargets(client, lock, key, selector) {
3115
+ if (selector.scope === "tracked") {
3116
+ const dep = lock.deployments[key];
3117
+ return [
3118
+ ...Object.keys(dep?.workflows ?? {}).sort().map((slug) => ({ kind: "workflow", slug })),
3119
+ ...Object.keys(dep?.agents ?? {}).sort().map((slug) => ({ kind: "agent", slug }))
3120
+ ];
3121
+ }
3122
+ if (selector.scope === "all") {
3123
+ const [workflows, agents] = await Promise.all([
3124
+ client.workflows.list({ expansion: ["slug"] }),
3125
+ client.agents.list({ expansion: ["slug"] })
3126
+ ]);
3127
+ return [
3128
+ ...workflows.filter((workflow2) => workflow2.slug).map((workflow2) => ({ kind: "workflow", slug: workflow2.slug })),
3129
+ ...agents.filter((agent2) => agent2.slug).map((agent2) => ({ kind: "agent", slug: agent2.slug }))
3130
+ ];
3131
+ }
3132
+ if (selector.kind) return [{ kind: selector.kind, slug: selector.slug }];
3133
+ const [agent, workflow] = await Promise.all([
3134
+ fetchDefinition(client, "agent", selector.slug),
3135
+ fetchDefinition(client, "workflow", selector.slug)
3136
+ ]);
3137
+ if (agent && workflow) {
3138
+ throw new Error(
3139
+ `Slug "${selector.slug}" matches an agent and a workflow. Use agents/${selector.slug} or workflows/${selector.slug}.`
3140
+ );
3141
+ }
3142
+ if (agent) return [{ kind: "agent", slug: selector.slug }];
3143
+ if (workflow) return [{ kind: "workflow", slug: selector.slug }];
3144
+ throw new Error(`No agent or workflow with slug "${selector.slug}".`);
3145
+ }
3146
+ async function pullResources(request, selector) {
3147
+ const context = await request.client.codeSync.context();
3148
+ const key = deploymentKey(request.client.http.baseUrl, context.workspaceId);
3149
+ const lock = await readLock(request.definitionsDir);
3150
+ const targets = await resolvePullTargets(request.client, lock, key, selector);
3151
+ const bucket = lockBucket(lock, key);
3152
+ const writes = [];
3153
+ const skipped = [];
3154
+ const conflicts = [];
3155
+ for (const target of targets) {
3156
+ const remote = await fetchDefinition(request.client, target.kind, target.slug);
3157
+ if (!remote) {
3158
+ throw new Error(
3159
+ `${target.kind === "agent" ? "Agent" : "Workflow"} "${target.slug}" is missing in Lunnoa.`
3160
+ );
3161
+ }
3162
+ const entry = bucket[folderOf(target.kind)][remote.slug];
3163
+ const hash = await hashOnDisk(request.definitionsDir, target.kind, remote.slug);
3164
+ if (entry && entry.revision === remote.revision) {
3165
+ skipped.push(labelOf(target.kind, remote.slug));
3166
+ continue;
3167
+ }
3168
+ const localChanged = hash !== void 0 && (!entry || hash !== entry.fileHash);
3169
+ const serverChanged = !entry || entry.revision !== remote.revision;
3170
+ if (localChanged && serverChanged && !request.force) {
3171
+ warnSecrets(target.kind, remote.slug, remote.definition);
3172
+ const local = await readLocalText(request.definitionsDir, target.kind, remote.slug) ?? "";
3173
+ conflicts.push(
3174
+ formatConflict(
3175
+ labelOf(target.kind, remote.slug),
3176
+ local,
3177
+ serverText(request.definitionsDir, request.projectRoot, remote)
3178
+ )
3179
+ );
3180
+ continue;
3181
+ }
3182
+ writes.push(remote);
3183
+ }
3184
+ if (conflicts.length) {
3185
+ throw new SyncConflictError(
3186
+ `Local and server both changed. Run pull --force to overwrite.
3187
+
3188
+ ${conflicts.join("\n\n")}`
3189
+ );
3190
+ }
3191
+ const written = [];
3192
+ for (const remote of writes) {
3193
+ warnSecrets(remote.kind, remote.slug, remote.definition);
3194
+ const fileHash = remote.kind === "agent" ? await writeAgentDefinition(
3195
+ request.definitionsDir,
3196
+ request.projectRoot,
3197
+ remote.definition
3198
+ ) : await writeWorkflowDefinition(
3199
+ request.definitionsDir,
3200
+ request.projectRoot,
3201
+ remote.definition
3202
+ );
3203
+ bucket[folderOf(remote.kind)][remote.slug] = {
3204
+ id: remote.id,
3205
+ revision: remote.revision,
3206
+ fileHash
3207
+ };
3208
+ written.push(labelOf(remote.kind, remote.slug));
3209
+ }
3210
+ if (written.length) await writeLock(request.definitionsDir, lock);
3211
+ return { written, skipped };
3212
+ }
3213
+ async function resolvePushFiles(root, selector) {
3214
+ if (selector.scope === "all") {
3215
+ return (await listDefinitionFiles(root)).map((file) => ({
3216
+ kind: file.kind,
3217
+ path: file.path
3218
+ }));
3219
+ }
3220
+ if (selector.file) return [{ kind: selector.kind, path: resolve2(selector.file) }];
3221
+ if (selector.kind && selector.slug) {
3222
+ return [
3223
+ {
3224
+ kind: selector.kind,
3225
+ path: resolve2(root, folderOf(selector.kind), `${selector.slug}.json`)
3226
+ }
3227
+ ];
3228
+ }
3229
+ if (!selector.slug) throw new Error("Missing definition to push.");
3230
+ const found = (await listDefinitionFiles(root)).filter((file) => file.slug === selector.slug);
3231
+ if (found.length === 0) throw new Error(`No definition file for "${selector.slug}".`);
3232
+ if (found.length > 1) {
3233
+ throw new Error(
3234
+ `Slug "${selector.slug}" matches an agent and a workflow. Use agents/${selector.slug} or workflows/${selector.slug}.`
3235
+ );
3236
+ }
3237
+ return [{ kind: found[0].kind, path: found[0].path }];
3238
+ }
3239
+ async function pushResources(request, selector) {
3240
+ const context = await request.client.codeSync.context();
3241
+ const key = deploymentKey(request.client.http.baseUrl, context.workspaceId);
3242
+ const lock = await readLock(request.definitionsDir);
3243
+ const bucket = lockBucket(lock, key);
3244
+ const files = await resolvePushFiles(request.definitionsDir, selector);
3245
+ const pushed = [];
3246
+ for (const file of files) {
3247
+ const loaded = await loadDefinitionForPush(request.definitionsDir, file.path);
3248
+ if (file.kind && loaded.kind !== file.kind) {
3249
+ throw new Error(`Expected a ${file.kind} definition, found ${loaded.kind}.`);
3250
+ }
3251
+ const slug = loaded.definition.slug;
3252
+ const entry = bucket[folderOf(loaded.kind)][slug];
3253
+ const options = {
3254
+ codeSource: "synced",
3255
+ ...entry ? { baseRevision: entry.revision } : {},
3256
+ ...request.force ? { force: true } : {}
3257
+ };
3258
+ let id = "";
3259
+ let revision = "";
3260
+ let definition;
3261
+ try {
3262
+ if (loaded.kind === "workflow") {
3263
+ const result = await request.client.workflows.upsertBySlug(
3264
+ loaded.definition,
3265
+ options
3266
+ );
3267
+ id = result.id;
3268
+ revision = result.revision;
3269
+ definition = result.definition;
3270
+ } else {
3271
+ const result = await request.client.agents.upsertBySlug(
3272
+ loaded.definition,
3273
+ options
3274
+ );
3275
+ id = result.id;
3276
+ revision = result.revision;
3277
+ definition = result.definition;
3278
+ }
3279
+ } catch (error) {
3280
+ if (error instanceof LunnoaApiError && error.status === 409) {
3281
+ throw new Error(publicApiMessage(error));
3282
+ }
3283
+ throw error;
3284
+ }
3285
+ const fileHash = loaded.kind === "agent" ? await writeAgentDefinition(
3286
+ request.definitionsDir,
3287
+ request.projectRoot,
3288
+ definition
3289
+ ) : await writeWorkflowDefinition(
3290
+ request.definitionsDir,
3291
+ request.projectRoot,
3292
+ definition
3293
+ );
3294
+ bucket[folderOf(loaded.kind)][definition.slug || slug] = { id, revision, fileHash };
3295
+ await writeLock(request.definitionsDir, lock);
3296
+ pushed.push(labelOf(loaded.kind, definition.slug || slug));
3297
+ }
3298
+ return { pushed };
3299
+ }
3300
+ function localState(exists, hash, entry) {
3301
+ if (!exists) return "missing";
3302
+ if (!entry) return "new";
3303
+ return hash === entry.fileHash ? "unchanged" : "modified";
3304
+ }
3305
+ function remoteState(remote, entry) {
3306
+ if (!remote) return "missing in Lunnoa";
3307
+ if (entry && entry.revision === remote.revision) return "unchanged";
3308
+ return "changed in UI";
3309
+ }
3310
+ function isConflict(local, remote) {
3311
+ if (local === "new" && remote === "missing in Lunnoa") return false;
3312
+ if (local === "missing" && remote === "missing in Lunnoa") return false;
3313
+ return local !== "unchanged" && remote !== "unchanged";
3314
+ }
3315
+ async function statusResources(request) {
3316
+ const context = await request.client.codeSync.context();
3317
+ const key = deploymentKey(request.client.http.baseUrl, context.workspaceId);
3318
+ const lock = await readLock(request.definitionsDir);
3319
+ const dep = lock.deployments[key];
3320
+ const items = /* @__PURE__ */ new Map();
3321
+ for (const slug of Object.keys(dep?.workflows ?? {})) {
3322
+ items.set(`workflow:${slug}`, { kind: "workflow", slug });
3323
+ }
3324
+ for (const slug of Object.keys(dep?.agents ?? {})) {
3325
+ items.set(`agent:${slug}`, { kind: "agent", slug });
3326
+ }
3327
+ for (const file of await listDefinitionFiles(request.definitionsDir)) {
3328
+ items.set(`${file.kind}:${file.slug}`, { kind: file.kind, slug: file.slug });
3329
+ }
3330
+ const ordered = [...items.values()].sort((a, b) => {
3331
+ if (a.kind !== b.kind) return a.kind === "workflow" ? -1 : 1;
3332
+ return a.slug.localeCompare(b.slug);
3333
+ });
3334
+ const rows = [];
3335
+ for (const item of ordered) {
3336
+ const remote = await fetchDefinition(request.client, item.kind, item.slug);
3337
+ const hash = await hashOnDisk(request.definitionsDir, item.kind, item.slug);
3338
+ const entry = dep?.[folderOf(item.kind)]?.[item.slug];
3339
+ const local = localState(hash !== void 0, hash, entry);
3340
+ const remoteLabel = remoteState(remote, entry);
3341
+ rows.push({
3342
+ kind: item.kind,
3343
+ slug: item.slug,
3344
+ local,
3345
+ remote: remoteLabel,
3346
+ conflict: isConflict(local, remoteLabel)
3347
+ });
3348
+ }
3349
+ return rows;
3350
+ }
3351
+
2324
3352
  // src/cli/catalog.ts
2325
3353
  function asRecord(value) {
2326
3354
  if (value && typeof value === "object" && !Array.isArray(value)) {
@@ -2567,44 +3595,6 @@ function actionDetail(action) {
2567
3595
  };
2568
3596
  }
2569
3597
 
2570
- // src/cli/print.ts
2571
- function printJson(value) {
2572
- console.log(JSON.stringify(value, null, 2));
2573
- }
2574
- function formatTable(rows, columns) {
2575
- if (rows.length === 0) {
2576
- return "No results.";
2577
- }
2578
- const widths = columns.map((col) => {
2579
- let width = col.header.length;
2580
- for (const row of rows) {
2581
- width = Math.max(width, stringifyCell(row[col.key]).length);
2582
- }
2583
- return width;
2584
- });
2585
- const header = columns.map((col, i) => pad(col.header, widths[i])).join(" ");
2586
- const divider = widths.map((w) => "-".repeat(w)).join(" ");
2587
- const body = rows.map(
2588
- (row) => columns.map((col, i) => pad(stringifyCell(row[col.key]), widths[i])).join(" ")
2589
- );
2590
- return [header, divider, ...body].join("\n");
2591
- }
2592
- function printTable(rows, columns) {
2593
- console.log(formatTable(rows, columns));
2594
- }
2595
- function stringifyCell(value) {
2596
- if (value === null || value === void 0) {
2597
- return "";
2598
- }
2599
- if (typeof value === "boolean") {
2600
- return value ? "yes" : "no";
2601
- }
2602
- return String(value);
2603
- }
2604
- function pad(value, width) {
2605
- return value.length >= width ? value : `${value}${" ".repeat(width - value.length)}`;
2606
- }
2607
-
2608
3598
  // src/cli/discover.ts
2609
3599
  function requireTarget(args, label) {
2610
3600
  const value = args.path?.trim();
@@ -2845,13 +3835,13 @@ async function runConnectionsGet(client, args) {
2845
3835
  }
2846
3836
  async function runAgentsList(client, args) {
2847
3837
  const agents = await client.agents.list({
2848
- expansion: ["description", "slug", "managedByCode"]
3838
+ expansion: ["description", "slug", "codeSource"]
2849
3839
  });
2850
3840
  const rows = agents.map((agent) => ({
2851
3841
  id: agent.id,
2852
3842
  name: agent.name,
2853
3843
  slug: agent.slug ?? "",
2854
- managedByCode: Boolean(agent.managedByCode),
3844
+ codeSource: agent.codeSource ?? "none",
2855
3845
  description: agent.description ?? ""
2856
3846
  }));
2857
3847
  if (args.json) {
@@ -2862,19 +3852,19 @@ async function runAgentsList(client, args) {
2862
3852
  { key: "id", header: "ID" },
2863
3853
  { key: "slug", header: "SLUG" },
2864
3854
  { key: "name", header: "NAME" },
2865
- { key: "managedByCode", header: "CODE" }
3855
+ { key: "codeSource", header: "SOURCE" }
2866
3856
  ]);
2867
3857
  }
2868
3858
  async function runWorkflowsList(client, args) {
2869
3859
  const workflows = await client.workflows.list({
2870
- expansion: ["description", "isActive", "slug", "managedByCode"]
3860
+ expansion: ["description", "isActive", "slug", "codeSource"]
2871
3861
  });
2872
3862
  const rows = workflows.map((workflow) => ({
2873
3863
  id: workflow.id,
2874
3864
  name: workflow.name,
2875
3865
  slug: workflow.slug ?? "",
2876
3866
  isActive: Boolean(workflow.isActive),
2877
- managedByCode: Boolean(workflow.managedByCode)
3867
+ codeSource: workflow.codeSource ?? "none"
2878
3868
  }));
2879
3869
  if (args.json) {
2880
3870
  printJson(rows);
@@ -2885,7 +3875,7 @@ async function runWorkflowsList(client, args) {
2885
3875
  { key: "slug", header: "SLUG" },
2886
3876
  { key: "name", header: "NAME" },
2887
3877
  { key: "isActive", header: "ACTIVE" },
2888
- { key: "managedByCode", header: "CODE" }
3878
+ { key: "codeSource", header: "SOURCE" }
2889
3879
  ]);
2890
3880
  }
2891
3881
  async function runEntityTypesList(client, args) {
@@ -3361,7 +4351,7 @@ function typedAgentAccessor(client: LunnoaClient, agentId: string) {
3361
4351
 
3362
4352
  // src/cli/load-definition.ts
3363
4353
  import { pathToFileURL } from "url";
3364
- import { resolve } from "path";
4354
+ import { resolve as resolve3 } from "path";
3365
4355
  import { access } from "fs/promises";
3366
4356
  import { constants as fsConstants } from "fs";
3367
4357
  async function fileExists(path) {
@@ -3373,7 +4363,7 @@ async function fileExists(path) {
3373
4363
  }
3374
4364
  }
3375
4365
  async function loadDefinitionModule(pathArg) {
3376
- const absolute = resolve(process.cwd(), pathArg);
4366
+ const absolute = resolve3(process.cwd(), pathArg);
3377
4367
  if (!await fileExists(absolute)) {
3378
4368
  throw new Error(`Definition file not found: ${absolute}`);
3379
4369
  }
@@ -3429,21 +4419,22 @@ async function loadOptionalConfig() {
3429
4419
  "lunnoa.config.json"
3430
4420
  ];
3431
4421
  for (const name of candidates) {
3432
- const absolute = resolve(process.cwd(), name);
4422
+ const absolute = resolve3(process.cwd(), name);
3433
4423
  if (!await fileExists(absolute)) {
3434
4424
  continue;
3435
4425
  }
3436
4426
  if (name.endsWith(".json")) {
3437
- const { readFile: readFile2 } = await import("fs/promises");
3438
- const raw = await readFile2(absolute, "utf8");
4427
+ const { readFile: readFile3 } = await import("fs/promises");
4428
+ const raw = await readFile3(absolute, "utf8");
3439
4429
  const cfg2 = JSON.parse(raw);
3440
- return { url: cfg2.url, apiKey: cfg2.apiKey };
4430
+ return { url: cfg2.url, apiKey: cfg2.apiKey, definitionsDir: cfg2.definitionsDir };
3441
4431
  }
3442
4432
  const mod = await import(pathToFileURL(absolute).href);
3443
4433
  const cfg = mod.default ?? mod;
3444
4434
  return {
3445
4435
  url: cfg.url,
3446
- apiKey: cfg.apiKey
4436
+ apiKey: cfg.apiKey,
4437
+ definitionsDir: cfg.definitionsDir
3447
4438
  };
3448
4439
  }
3449
4440
  return {};
@@ -3451,8 +4442,8 @@ async function loadOptionalConfig() {
3451
4442
 
3452
4443
  // src/cli/init.ts
3453
4444
  import { spawn } from "child_process";
3454
- import { cp, mkdir, readdir, readFile, stat, writeFile } from "fs/promises";
3455
- import { basename, join, resolve as resolve2 } from "path";
4445
+ import { cp, mkdir as mkdir2, readdir as readdir2, readFile as readFile2, stat, writeFile as writeFile2 } from "fs/promises";
4446
+ import { basename, join as join2, resolve as resolve4 } from "path";
3456
4447
  import { createInterface } from "readline/promises";
3457
4448
  var DEFAULT_TEMPLATE = "lunnoa-automate/lunnoa-starter";
3458
4449
  var SKIP_LOCAL = /* @__PURE__ */ new Set(["node_modules", ".next", ".git", ".env", ".claude", ".cursor"]);
@@ -3505,14 +4496,14 @@ async function runInit(args) {
3505
4496
  if (!args.path) {
3506
4497
  throw new Error("Missing project directory. Example: init my-app --url https://lunnoa.example");
3507
4498
  }
3508
- const dest = resolve2(process.cwd(), args.path);
3509
- if ((await readdir(dest).catch(() => [])).length > 0) {
4499
+ const dest = resolve4(process.cwd(), args.path);
4500
+ if ((await readdir2(dest).catch(() => [])).length > 0) {
3510
4501
  throw new Error(`${dest} already exists and is not empty.`);
3511
4502
  }
3512
- await mkdir(dest, { recursive: true });
4503
+ await mkdir2(dest, { recursive: true });
3513
4504
  const template = args.template ?? DEFAULT_TEMPLATE;
3514
- if (await isDirectory(resolve2(template))) {
3515
- await cp(resolve2(template), dest, {
4505
+ if (await isDirectory(resolve4(template))) {
4506
+ await cp(resolve4(template), dest, {
3516
4507
  recursive: true,
3517
4508
  filter: (src) => !SKIP_LOCAL.has(basename(src))
3518
4509
  });
@@ -3527,9 +4518,9 @@ async function runInit(args) {
3527
4518
  apiKey ||= (await rl.question("API key lna_... (CLI only: discovery, codegen, deploy. Enter to skip): ")).trim();
3528
4519
  rl.close();
3529
4520
  }
3530
- const example = await readFile(join(dest, ".env.example"), "utf8").catch(() => "");
3531
- await writeFile(
3532
- join(dest, ".env"),
4521
+ const example = await readFile2(join2(dest, ".env.example"), "utf8").catch(() => "");
4522
+ await writeFile2(
4523
+ join2(dest, ".env"),
3533
4524
  setEnvValues(dropRetiredProjectEnv(example), {
3534
4525
  LUNNOA_URL: url,
3535
4526
  LUNNOA_API_KEY: apiKey
@@ -3551,18 +4542,18 @@ Next steps:`);
3551
4542
 
3552
4543
  // src/cli/skills.ts
3553
4544
  import { cp as cp2 } from "fs/promises";
3554
- import { dirname, resolve as resolve3 } from "path";
4545
+ import { dirname as dirname2, resolve as resolve5 } from "path";
3555
4546
  import { fileURLToPath } from "url";
3556
4547
  var SKILL_TARGETS = [".claude/skills", ".cursor/skills"];
3557
- var BUNDLED_SKILLS_DIR = resolve3(
3558
- dirname(fileURLToPath(import.meta.url)),
4548
+ var BUNDLED_SKILLS_DIR = resolve5(
4549
+ dirname2(fileURLToPath(import.meta.url)),
3559
4550
  "..",
3560
4551
  "skills"
3561
4552
  );
3562
4553
  async function installSkills(projectDir, sourceDir = BUNDLED_SKILLS_DIR) {
3563
4554
  const written = [];
3564
4555
  for (const target of SKILL_TARGETS) {
3565
- const dest = resolve3(projectDir, target);
4556
+ const dest = resolve5(projectDir, target);
3566
4557
  await cp2(sourceDir, dest, { recursive: true, force: true });
3567
4558
  written.push(dest);
3568
4559
  }
@@ -3572,6 +4563,11 @@ async function installSkills(projectDir, sourceDir = BUNDLED_SKILLS_DIR) {
3572
4563
  // src/cli/index.ts
3573
4564
  async function bindClient(args) {
3574
4565
  const fileConfig = await loadOptionalConfig();
4566
+ const projectRoot = process.cwd();
4567
+ const definitionsDir = resolve6(
4568
+ projectRoot,
4569
+ args.dir ?? fileConfig.definitionsDir ?? "lunnoa"
4570
+ );
3575
4571
  const url = args.url ?? process.env.LUNNOA_URL ?? fileConfig.url;
3576
4572
  const apiKey = args.apiKey ?? process.env.LUNNOA_API_KEY ?? fileConfig.apiKey;
3577
4573
  const accessToken = args.accessToken;
@@ -3585,9 +4581,114 @@ async function bindClient(args) {
3585
4581
  }
3586
4582
  return {
3587
4583
  client: new LunnoaClient({ baseUrl: url, apiKey, accessToken }),
3588
- url
4584
+ url,
4585
+ definitionsDir,
4586
+ projectRoot
3589
4587
  };
3590
4588
  }
4589
+ function resourceSelector(path) {
4590
+ const normalized = path.replace(/\\/g, "/");
4591
+ const file = /^(?:.*\/)?(agents|workflows)\/([^/]+)\.json$/.exec(normalized);
4592
+ if (file) {
4593
+ return {
4594
+ kind: file[1] === "agents" ? "agent" : "workflow",
4595
+ slug: file[2],
4596
+ file: path
4597
+ };
4598
+ }
4599
+ const prefixed = /^(agents|workflows)\/([^/]+)$/.exec(normalized);
4600
+ if (prefixed) {
4601
+ return {
4602
+ kind: prefixed[1] === "agents" ? "agent" : "workflow",
4603
+ slug: prefixed[2]
4604
+ };
4605
+ }
4606
+ if (normalized.endsWith(".json") || normalized.includes("/")) {
4607
+ return { slug: "", file: path };
4608
+ }
4609
+ return { slug: normalized };
4610
+ }
4611
+ function pullSelector(args) {
4612
+ if (args.all && args.path) {
4613
+ throw new Error("pull --all does not take a slug.");
4614
+ }
4615
+ if (args.all || !args.path) {
4616
+ return args.all ? { scope: "all" } : { scope: "tracked" };
4617
+ }
4618
+ const parsed = resourceSelector(args.path);
4619
+ if (parsed.file && !parsed.kind) {
4620
+ throw new Error("Pass a slug, agents/<slug>, or workflows/<slug>.");
4621
+ }
4622
+ return { scope: "one", kind: parsed.kind, slug: parsed.slug };
4623
+ }
4624
+ function pushSelector(args, deploy) {
4625
+ if (deploy) return { scope: "one", kind: deploy.kind, file: deploy.file };
4626
+ if (args.all && args.path) {
4627
+ throw new Error("push --all does not take a path.");
4628
+ }
4629
+ if (!args.path) return { scope: "all" };
4630
+ const parsed = resourceSelector(args.path);
4631
+ if (parsed.file) return { scope: "one", kind: parsed.kind, file: parsed.file };
4632
+ return { scope: "one", kind: parsed.kind, slug: parsed.slug };
4633
+ }
4634
+ async function runPull(args) {
4635
+ const bound = await bindClient(args);
4636
+ const report = await pullResources(
4637
+ {
4638
+ client: bound.client,
4639
+ definitionsDir: bound.definitionsDir,
4640
+ projectRoot: bound.projectRoot,
4641
+ force: args.force
4642
+ },
4643
+ pullSelector(args)
4644
+ );
4645
+ if (report.written.length === 0) {
4646
+ console.log("Nothing to pull.");
4647
+ return;
4648
+ }
4649
+ for (const file of report.written) console.log(`Pulled ${file}`);
4650
+ }
4651
+ async function runPush(args, deploy) {
4652
+ const bound = await bindClient(args);
4653
+ const report = await pushResources(
4654
+ {
4655
+ client: bound.client,
4656
+ definitionsDir: bound.definitionsDir,
4657
+ projectRoot: bound.projectRoot,
4658
+ force: args.force
4659
+ },
4660
+ pushSelector(args, deploy)
4661
+ );
4662
+ for (const file of report.pushed) console.log(`Pushed ${file}`);
4663
+ }
4664
+ async function runStatus(args) {
4665
+ const bound = await bindClient(args);
4666
+ const rows = await statusResources({
4667
+ client: bound.client,
4668
+ definitionsDir: bound.definitionsDir,
4669
+ projectRoot: bound.projectRoot
4670
+ });
4671
+ if (args.json) {
4672
+ printJson(rows);
4673
+ return;
4674
+ }
4675
+ printTable(
4676
+ rows.map((row) => ({
4677
+ kind: row.kind,
4678
+ slug: row.slug,
4679
+ local: row.local,
4680
+ remote: row.remote,
4681
+ conflict: row.conflict
4682
+ })),
4683
+ [
4684
+ { key: "kind", header: "KIND" },
4685
+ { key: "slug", header: "SLUG" },
4686
+ { key: "local", header: "LOCAL" },
4687
+ { key: "remote", header: "REMOTE" },
4688
+ { key: "conflict", header: "CONFLICT" }
4689
+ ]
4690
+ );
4691
+ }
3591
4692
  async function fetchDefinitions(client, baseUrl) {
3592
4693
  const entityTypes = await client.entityTypes.list({
3593
4694
  expansion: ["attributeSchema", "stateSchema", "description"]
@@ -3622,16 +4723,20 @@ async function runCodegen(args) {
3622
4723
  `Found ${definitions.entityTypes.length} entity type(s), ${definitions.workflows.length} workflow(s), ${definitions.agents.length} agent(s).`
3623
4724
  );
3624
4725
  const moduleSource = generateDeploymentModule(definitions);
3625
- const outDir = resolve4(process.cwd(), args.out);
3626
- await mkdir2(outDir, { recursive: true });
3627
- const outFile = resolve4(outDir, "lunnoa.generated.ts");
3628
- await writeFile2(outFile, moduleSource, "utf8");
4726
+ const outDir = resolve6(process.cwd(), args.out);
4727
+ await mkdir3(outDir, { recursive: true });
4728
+ const outFile = resolve6(outDir, "lunnoa.generated.ts");
4729
+ await writeFile3(outFile, moduleSource, "utf8");
3629
4730
  console.log(`Wrote ${outFile}`);
3630
4731
  }
3631
4732
  async function deployWorkflow(args) {
3632
4733
  if (!args.path) {
3633
4734
  throw new Error("Missing definition path. Example: workflows deploy ./workflow.ts");
3634
4735
  }
4736
+ if (args.path.endsWith(".json")) {
4737
+ await runPush(args, { kind: "workflow", file: args.path });
4738
+ return;
4739
+ }
3635
4740
  const { client } = await bindClient(args);
3636
4741
  const mod = await loadDefinitionModule(args.path);
3637
4742
  const def = pickWorkflowDefinition(mod);
@@ -3644,6 +4749,10 @@ async function deployAgent(args) {
3644
4749
  if (!args.path) {
3645
4750
  throw new Error("Missing definition path. Example: agents deploy ./agent.ts");
3646
4751
  }
4752
+ if (args.path.endsWith(".json")) {
4753
+ await runPush(args, { kind: "agent", file: args.path });
4754
+ return;
4755
+ }
3647
4756
  const { client } = await bindClient(args);
3648
4757
  const mod = await loadDefinitionModule(args.path);
3649
4758
  const def = pickAgentDefinition(mod);
@@ -3785,8 +4894,12 @@ async function runDiscovery(args) {
3785
4894
  }
3786
4895
  return false;
3787
4896
  }
3788
- async function main() {
3789
- const args = parseArgs(process.argv.slice(2));
4897
+ function reportCliError(error) {
4898
+ console.error(error instanceof Error ? error.message : error);
4899
+ process.exitCode = 1;
4900
+ }
4901
+ async function run(argv) {
4902
+ const args = parseArgs(argv);
3790
4903
  try {
3791
4904
  process.loadEnvFile();
3792
4905
  } catch {
@@ -3800,7 +4913,7 @@ async function main() {
3800
4913
  return;
3801
4914
  }
3802
4915
  if (args.command === "skills" && args.subcommand === "install") {
3803
- const written = await installSkills(resolve4(process.cwd(), args.path ?? "."));
4916
+ const written = await installSkills(resolve6(process.cwd(), args.path ?? "."));
3804
4917
  for (const dest of written) {
3805
4918
  console.log(`Installed Lunnoa skills into ${dest}`);
3806
4919
  }
@@ -3810,6 +4923,18 @@ async function main() {
3810
4923
  await runCodegen(args);
3811
4924
  return;
3812
4925
  }
4926
+ if (args.command === "pull") {
4927
+ await runPull(args);
4928
+ return;
4929
+ }
4930
+ if (args.command === "push") {
4931
+ await runPush(args);
4932
+ return;
4933
+ }
4934
+ if (args.command === "status") {
4935
+ await runStatus(args);
4936
+ return;
4937
+ }
3813
4938
  if (await runDiscovery(args)) {
3814
4939
  return;
3815
4940
  }
@@ -3830,7 +4955,17 @@ async function main() {
3830
4955
  console.log(USAGE);
3831
4956
  process.exitCode = 1;
3832
4957
  }
3833
- main().catch((error) => {
3834
- console.error(error instanceof Error ? error.message : error);
3835
- process.exitCode = 1;
3836
- });
4958
+ async function main() {
4959
+ try {
4960
+ await run(process.argv.slice(2));
4961
+ } catch (error) {
4962
+ reportCliError(error);
4963
+ }
4964
+ }
4965
+ if (process.env.VITEST !== "true") {
4966
+ void main();
4967
+ }
4968
+ export {
4969
+ reportCliError,
4970
+ run
4971
+ };