@lunnoa/client 1.4.0 → 2.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/README.md CHANGED
@@ -7,7 +7,7 @@ TypeScript SDK for building applications **on top of** [Lunnoa Automate](https:/
7
7
 
8
8
  ## What is this?
9
9
 
10
- `@lunnoa/client` is the official consumption SDK for the Lunnoa Automate **Public API**: a typed client for agents and chat (with SSE streaming), workflows and executions, entities (Objects), knowledge, queues, variables, connections, and projects — plus a codegen command that types the SDK against **your** deployment's entity types, workflows, and agents.
10
+ `@lunnoa/client` is the official consumption SDK for the Lunnoa Automate **Public API**: a typed client for agents and chat (with SSE streaming), workflows and executions, entities (Objects), knowledge, queues, variables, and connections — plus a codegen command that types the SDK against **your** deployment's entity types, workflows, and agents.
11
11
 
12
12
  **The boundary with the other Lunnoa SDK:** [`@lunnoa/toolkit`](https://github.com/lunnoa-automate/lunnoa_toolkit) is for building integration apps that run *inside* the platform (`createAction` / `createApp`); **`@lunnoa/client` is for building applications *on top of* the platform** — including `define*` factories, `runs.start`, and CLI deploy of workflows/agents.
13
13
 
@@ -21,6 +21,14 @@ npm install @lunnoa/client
21
21
 
22
22
  **Requirements:** Node.js ≥ 20 (or any runtime with `fetch` and web streams).
23
23
 
24
+ ### Start a new app
25
+
26
+ ```bash
27
+ npx @lunnoa/client init my-app --url https://lunnoa.your-company.example
28
+ ```
29
+
30
+ Scaffolds the [lunnoa-starter](https://github.com/lunnoa-automate/lunnoa-starter) Next.js template (thin, Docker-deployable, both auth patterns behind `LUNNOA_AUTH_MODE`) and writes `.env`, prompting for an API key when run interactively. `--template <owner/repo[#ref]|./local-folder>` overrides the source. Then `npm install` and open the folder in Claude Code / Cursor / Codex.
31
+
24
32
  ## Quickstart
25
33
 
26
34
  ```typescript
@@ -98,18 +106,18 @@ const { user, status, login, logout } = useLunnoaAuth();
98
106
  const lunnoa = useLunnoaClient();
99
107
  const { progress, status: progressStatus } = useExecutionProgress(executionId);
100
108
  const { waiting, pendingInput, submitInput } = useNeedsInput(progress);
101
- const { messages, send, stop, taskId } = useAgentChat(agentId); // needs @ai-sdk/react
109
+ const { messages, send, stop, taskId } = useAgentChat(agentId);
102
110
  const { items, decide } = useApprovalsInbox({ pollIntervalMs: 15_000 });
103
111
  const { data, page, setPage } = useEntityList({ objectTypeSlug: 'invoice' });
104
112
  ```
105
113
 
106
- Peers: `react` ≥ 18 (required for `/react`); `@ai-sdk/react` only for `useAgentChat`. Core `@lunnoa/client` stays headless.
114
+ Peer: `react` ≥ 18 (required for `/react`). `@ai-sdk/react` (behind `useAgentChat`) ships as a dependency. Core `@lunnoa/client` stays headless.
107
115
 
108
116
  ## Resources
109
117
 
110
118
  Most namespaces map to the Public API surface (`openapi.public.json`):
111
119
 
112
- `auth` · `agents` · `tasks` · `agentChat` (SSE streaming) · `workflows` · `executions` · `actions` · `entities` · `entityTypes` · `knowledge` · `queues` · `queueItems` · `variables` · `connections` · `projects` · `workflowApps` · `discovery`
120
+ `auth` · `agents` · `tasks` · `agentChat` (SSE streaming) · `workflows` · `executions` · `actions` · `entities` · `entityTypes` · `knowledge` · `queues` · `queueItems` · `variables` · `connections` · `workflowApps` · `discovery`
113
121
 
114
122
  `approvals` uses the authenticated **workspace** API (`/api/approvals/...`), not Public API v1. The inbox is filtered to requests where the current user is an eligible approver.
115
123
 
@@ -268,7 +276,7 @@ Re-run codegen whenever entity types, workflows, or agents change on the deploym
268
276
 
269
277
  ## Define, run, and CLI deploy
270
278
 
271
- Author local definitions with `defineWorkflow` / `defineAgent` / `defineEntityType`, run linear chains with `lunnoa.runs.start`, and upsert with the CLI (prefer CLI over calling upsert from app code). An entity type slug is unique on the deployment. Deploy grants that type to the API key's workspace when the slug is new, and will not rewrite a type already granted to other workspaces (no `--project-id`). Instance writes reject unknown attribute keys unless `allowUnknownAttributes` is true — use the reserved `extensions` object for flexible bags:
279
+ Author local definitions with `defineWorkflow` / `defineAgent` / `defineEntityType`, run linear chains with `lunnoa.runs.start`, and upsert with the CLI (prefer CLI over calling upsert from app code). Agent and workflow upsert is by slug inside the workspace on the API key. An entity type slug is unique on the deployment. Deploy grants that type to the API key's workspace when the slug is new, and will not rewrite a type already granted to other workspaces. Instance writes reject unknown attribute keys unless `allowUnknownAttributes` is true — use the reserved `extensions` object for flexible bags:
272
280
 
273
281
  ```bash
274
282
  npx @lunnoa/client entity-types deploy ./human.ts --url https://lunnoa.example --api-key lna_...
@@ -278,9 +286,9 @@ Workflows and agents:
278
286
 
279
287
  ```bash
280
288
  npx @lunnoa/client workflows deploy ./workflows/sync-orders.ts \
281
- --url https://lunnoa.example --api-key lna_... --project-id <uuid>
289
+ --url https://lunnoa.example --api-key lna_...
282
290
  npx @lunnoa/client agents deploy ./agents/triage.ts \
283
- --url https://lunnoa.example --api-key lna_... --project-id <uuid>
291
+ --url https://lunnoa.example --api-key lna_...
284
292
  ```
285
293
 
286
294
  `defineAction` binds catalogue actions only; toolkit `createAction` is for in-platform apps. See Lunnoa Docs under `guides/develop/client/define`, `runs`, and `cli-deploy`.
@@ -299,11 +307,10 @@ npx @lunnoa/client connections list --app gmail --json
299
307
  npx @lunnoa/client agents list --json
300
308
  npx @lunnoa/client workflows list --json
301
309
  npx @lunnoa/client entity-types list --json
302
- npx @lunnoa/client projects list --json
303
310
  npx @lunnoa/client features --json
304
311
  ```
305
312
 
306
- `tools list` is compact (`appId`, `actionId`, name, flags). `tools get` prints `inputConfig` and a `defineAction({ id, appId, input })` sketch. Auth flags and `LUNNOA_URL` / `LUNNOA_API_KEY` / `lunnoa.config` work the same as deploy.
313
+ `tools list` is compact (`appId`, `actionId`, name, flags). `tools get` prints `inputConfig` and a `defineAction({ id, appId, input })` sketch. Auth flags and `LUNNOA_URL` / `LUNNOA_API_KEY` / `lunnoa.config` work the same as deploy. The CLI also reads `./.env` (real environment variables win).
307
314
 
308
315
  ## Regenerating the API types
309
316
 
@@ -330,7 +337,7 @@ Partner paste block:
330
337
 
331
338
  ```text
332
339
  Use @lunnoa/client skills: lunnoa-solution-design then lunnoa-client.
333
- LUNNOA_URL=… LUNNOA_API_KEY=lna_… (server only). Project ID=…
340
+ LUNNOA_URL=… LUNNOA_API_KEY=lna_… (server only).
334
341
  Start by interviewing me, then workflowApps.list() / codegen.
335
342
  ```
336
343
 
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, writeFile } from "fs/promises";
5
- import { resolve as resolve3 } from "path";
4
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
5
+ import { resolve as resolve4 } from "path";
6
6
 
7
7
  // src/core/auth.ts
8
8
  var API_KEY_PREFIX = "lna_";
@@ -661,12 +661,12 @@ var AgentsResource = class {
661
661
  });
662
662
  }
663
663
  /**
664
- * Upsert an agent by project-scoped slug.
664
+ * Upsert an agent by slug inside the workspace on the API key.
665
665
  * Prefer the CLI (`npx @lunnoa/client agents deploy`) for happy-path deploy.
666
666
  */
667
- upsertBySlug(projectId, body, options) {
667
+ upsertBySlug(body, options) {
668
668
  const payload = "kind" in body && body.kind === "agent" ? agentDefinitionToUpsert(body, options) : body;
669
- return this.http.post(`/api/projects/${projectId}/agents/upsert`, {
669
+ return this.http.post("/api/agents/upsert", {
670
670
  body: payload
671
671
  });
672
672
  }
@@ -1083,6 +1083,33 @@ var EntityTypesResource = class {
1083
1083
  }
1084
1084
  };
1085
1085
 
1086
+ // src/resources/tables.ts
1087
+ var TablesResource = class {
1088
+ constructor(http) {
1089
+ this.http = http;
1090
+ }
1091
+ http;
1092
+ /** Lists tables in the API key's workspace (without columns and rows). */
1093
+ list() {
1094
+ return this.http.get("/api/tables");
1095
+ }
1096
+ /** Returns a single table by ID, including columns and rows. */
1097
+ get(id) {
1098
+ return this.http.get(`/api/tables/${id}`);
1099
+ }
1100
+ /** Returns a single table by its workspace slug, including columns and rows. */
1101
+ getBySlug(slug) {
1102
+ return this.http.get(`/api/tables/slug/${slug}`);
1103
+ }
1104
+ /**
1105
+ * Creates or updates a table by workspace slug. Omitted columns or rows
1106
+ * keep their current value. Marks `managedByCode` by default.
1107
+ */
1108
+ upsertBySlug(input) {
1109
+ return this.http.post("/api/tables/upsert", { body: input });
1110
+ }
1111
+ };
1112
+
1086
1113
  // src/resources/approvals.ts
1087
1114
  var ApprovalsResource = class {
1088
1115
  constructor(http) {
@@ -1422,7 +1449,7 @@ function computeDurationMs(start, end) {
1422
1449
  return b - a;
1423
1450
  }
1424
1451
  function defaultSleep(ms) {
1425
- return new Promise((resolve4) => setTimeout(resolve4, ms));
1452
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
1426
1453
  }
1427
1454
 
1428
1455
  // src/resources/knowledge.ts
@@ -1484,30 +1511,6 @@ var KnowledgeResource = class {
1484
1511
  }
1485
1512
  };
1486
1513
 
1487
- // src/resources/projects.ts
1488
- var ProjectsResource = class {
1489
- constructor(http) {
1490
- this.http = http;
1491
- }
1492
- http;
1493
- /** Lists projects in the workspace, most recently updated first. */
1494
- list(options) {
1495
- return this.http.get("/api/projects", {
1496
- query: serializeListQuery(options)
1497
- });
1498
- }
1499
- /** Returns a single project. */
1500
- get(projectId, options) {
1501
- return this.http.get(`/api/projects/${projectId}`, {
1502
- query: serializeListQuery(options)
1503
- });
1504
- }
1505
- /** Creates a project. */
1506
- create(data) {
1507
- return this.http.post("/api/projects", { body: data });
1508
- }
1509
- };
1510
-
1511
1514
  // src/resources/queue-items.ts
1512
1515
  var QueueItemsResource = class {
1513
1516
  constructor(http) {
@@ -1787,7 +1790,7 @@ var RunsResource = class {
1787
1790
  * Start a linear multi-step ad-hoc Execution.
1788
1791
  *
1789
1792
  * Accepts a `defineWorkflow` result, a step array, or an explicit
1790
- * `{ steps, projectId?, name? }` payload.
1793
+ * `{ steps, name? }` payload.
1791
1794
  */
1792
1795
  start(input) {
1793
1796
  const options = normalizeStartRunInput(input);
@@ -2005,15 +2008,14 @@ var WorkflowsResource = class {
2005
2008
  });
2006
2009
  }
2007
2010
  /**
2008
- * Upsert a workflow by project-scoped slug from a linear step list.
2011
+ * Upsert a workflow by slug inside the workspace on the API key.
2009
2012
  * Prefer the CLI (`npx @lunnoa/client workflows deploy`) for happy-path deploy.
2010
2013
  */
2011
- upsertBySlug(projectId, body) {
2014
+ upsertBySlug(body) {
2012
2015
  const payload = "kind" in body && body.kind === "workflow" ? workflowDefinitionToUpsert(body) : body;
2013
- return this.http.post(
2014
- `/api/projects/${projectId}/workflows/upsert`,
2015
- { body: payload }
2016
- );
2016
+ return this.http.post("/api/workflows/upsert", {
2017
+ body: payload
2018
+ });
2017
2019
  }
2018
2020
  /**
2019
2021
  * Starts a new execution of the workflow with the given trigger inputs.
@@ -2067,13 +2069,14 @@ var LunnoaClient = class {
2067
2069
  approvals;
2068
2070
  entities;
2069
2071
  entityTypes;
2072
+ /** Small reference / lookup tables (rows stored inline; use entities for transactional data). */
2073
+ tables;
2070
2074
  knowledge;
2071
2075
  queues;
2072
2076
  queueItems;
2073
2077
  variables;
2074
2078
  scorecards;
2075
2079
  connections;
2076
- projects;
2077
2080
  workflowApps;
2078
2081
  discovery;
2079
2082
  constructor(options) {
@@ -2090,13 +2093,13 @@ var LunnoaClient = class {
2090
2093
  this.workflows = new WorkflowsResource(this.http, this.executions);
2091
2094
  this.entities = new EntitiesResource(this.http);
2092
2095
  this.entityTypes = new EntityTypesResource(this.http);
2096
+ this.tables = new TablesResource(this.http);
2093
2097
  this.knowledge = new KnowledgeResource(this.http);
2094
2098
  this.queues = new QueuesResource(this.http);
2095
2099
  this.queueItems = new QueueItemsResource(this.http);
2096
2100
  this.variables = new VariablesResource(this.http);
2097
2101
  this.scorecards = new ScorecardsResource(this.http);
2098
2102
  this.connections = new ConnectionsResource(this.http);
2099
- this.projects = new ProjectsResource(this.http);
2100
2103
  this.discovery = new DiscoveryResource(this.http);
2101
2104
  }
2102
2105
  };
@@ -2106,19 +2109,19 @@ var GROUPS_WITH_SUBCOMMAND = /* @__PURE__ */ new Set([
2106
2109
  "workflows",
2107
2110
  "agents",
2108
2111
  "entity-types",
2112
+ "tables",
2109
2113
  "tools",
2110
2114
  "apps",
2111
2115
  "connections",
2112
- "projects",
2113
2116
  "skills"
2114
2117
  ]);
2115
2118
  var VALUE_FLAGS = /* @__PURE__ */ new Set([
2116
2119
  "--url",
2117
2120
  "--api-key",
2118
2121
  "--access-token",
2119
- "--project-id",
2120
2122
  "--out",
2121
- "--app"
2123
+ "--app",
2124
+ "--template"
2122
2125
  ]);
2123
2126
  function requireFlagValue(flag, value) {
2124
2127
  if (!value || value.startsWith("--")) {
@@ -2172,15 +2175,15 @@ function parseArgs(argv) {
2172
2175
  case "--access-token":
2173
2176
  args.accessToken = value;
2174
2177
  break;
2175
- case "--project-id":
2176
- args.projectId = value;
2177
- break;
2178
2178
  case "--out":
2179
2179
  args.out = value;
2180
2180
  break;
2181
2181
  case "--app":
2182
2182
  args.app = value;
2183
2183
  break;
2184
+ case "--template":
2185
+ args.template = value;
2186
+ break;
2184
2187
  default:
2185
2188
  break;
2186
2189
  }
@@ -2196,6 +2199,7 @@ function parseArgs(argv) {
2196
2199
  return args;
2197
2200
  }
2198
2201
  var USAGE = `Usage:
2202
+ npx @lunnoa/client init <dir> [--url <url>] [--api-key <lna_key>] [--template <owner/repo|path>]
2199
2203
  npx @lunnoa/client codegen --url <url> --api-key <lna_key> [--out <dir>]
2200
2204
 
2201
2205
  Discover (do not invent catalogue action ids):
@@ -2209,12 +2213,13 @@ var USAGE = `Usage:
2209
2213
  npx @lunnoa/client agents list [--json]
2210
2214
  npx @lunnoa/client workflows list [--json]
2211
2215
  npx @lunnoa/client entity-types list [--json]
2212
- npx @lunnoa/client projects list [--json]
2216
+ npx @lunnoa/client tables list [--json]
2217
+ npx @lunnoa/client tables get <slugOrId> [--json]
2213
2218
  npx @lunnoa/client features [--json]
2214
2219
 
2215
- Deploy (upsert by slug):
2216
- npx @lunnoa/client workflows deploy <path> --url <url> --api-key <key> --project-id <uuid>
2217
- npx @lunnoa/client agents deploy <path> --url <url> --api-key <key> --project-id <uuid>
2220
+ Deploy (upsert by slug inside the workspace on the API key):
2221
+ npx @lunnoa/client workflows deploy <path> --url <url> --api-key <key>
2222
+ npx @lunnoa/client agents deploy <path> --url <url> --api-key <key>
2218
2223
  npx @lunnoa/client entity-types deploy <path> --url <url> --api-key <key>
2219
2224
 
2220
2225
  Agent skills (copies this version's skills into .claude/skills and .cursor/skills):
@@ -2224,19 +2229,19 @@ Options:
2224
2229
  --url Base URL of the Lunnoa Automate deployment
2225
2230
  --api-key Machine API key (lna_...)
2226
2231
  --access-token User JWT as an alternative to --api-key
2227
- --project-id Project UUID (required for workflow/agent deploy)
2228
2232
  --out Output directory for lunnoa.generated.ts (codegen; default: ./src/lunnoa)
2229
2233
  --app Restrict tools/connections to one workflow app id
2230
2234
  --for-agent Only catalogue entries with availableForAgent
2235
+ --template init starter: GitHub owner/repo[#ref] or local folder (default: lunnoa-automate/lunnoa-starter)
2231
2236
  --json Machine-readable JSON (preferred for coding agents)
2232
2237
  --help Show this help
2233
2238
 
2234
- Environment: LUNNOA_URL, LUNNOA_API_KEY, LUNNOA_PROJECT_ID
2239
+ Environment: LUNNOA_URL, LUNNOA_API_KEY (also read from ./.env)
2235
2240
  Optional config file: lunnoa.config.ts | .mjs | .js | .json
2236
2241
 
2237
2242
  tools get prints inputConfig and a defineAction() sketch. tools list is compact;
2238
- run tools get for the schema. Entity-type deploy uses a deployment-wide slug
2239
- and grants the API key's workspace when the slug is new (no --project-id).
2243
+ run tools get for the schema. An entity-type slug is unique on the deployment
2244
+ and grants the API key's workspace when the slug is new.
2240
2245
  A slug already granted to other workspaces is not rewritten. Unknown instance attributes are rejected unless
2241
2246
  allowUnknownAttributes is true; use extensions.
2242
2247
  `;
@@ -2708,16 +2713,14 @@ async function runEntityTypesList(client, args) {
2708
2713
  { key: "managedByCode", header: "CODE" }
2709
2714
  ]);
2710
2715
  }
2711
- async function runProjectsList(client, args) {
2712
- const projects = await client.projects.list({
2713
- expansion: ["description", "countAgents", "countWorkflows"]
2714
- });
2715
- const rows = projects.map((project) => ({
2716
- id: project.id,
2717
- name: project.name,
2718
- description: project.description ?? "",
2719
- agents: project._count?.agents ?? "",
2720
- workflows: project._count?.workflows ?? ""
2716
+ async function runTablesList(client, args) {
2717
+ const tables = await client.tables.list();
2718
+ const rows = tables.map((table) => ({
2719
+ id: table.id,
2720
+ slug: table.slug,
2721
+ name: table.name,
2722
+ description: table.description ?? "",
2723
+ managedByCode: Boolean(table.managedByCode)
2721
2724
  }));
2722
2725
  if (args.json) {
2723
2726
  printJson(rows);
@@ -2725,11 +2728,24 @@ async function runProjectsList(client, args) {
2725
2728
  }
2726
2729
  printTable(rows, [
2727
2730
  { key: "id", header: "ID" },
2731
+ { key: "slug", header: "SLUG" },
2728
2732
  { key: "name", header: "NAME" },
2729
- { key: "agents", header: "AGENTS" },
2730
- { key: "workflows", header: "WORKFLOWS" }
2733
+ { key: "managedByCode", header: "CODE" }
2731
2734
  ]);
2732
2735
  }
2736
+ async function runTablesGet(client, args) {
2737
+ const ref = requireTarget(args, "table slug or id (example: tables get cost_centres)");
2738
+ const table = /^[0-9a-f]{8}-[0-9a-f]{4}-/i.test(ref) ? await client.tables.get(ref) : await client.tables.getBySlug(ref);
2739
+ if (args.json) {
2740
+ printJson(table);
2741
+ return;
2742
+ }
2743
+ console.log(`id: ${table.id}`);
2744
+ console.log(`slug: ${table.slug}`);
2745
+ console.log(`name: ${table.name}`);
2746
+ console.log(`columns: ${(table.columns ?? []).map((c) => c.key).join(", ")}`);
2747
+ console.log(`rows: ${table.rows?.length ?? 0}`);
2748
+ }
2733
2749
  async function runFeatures(client, args) {
2734
2750
  const features = await client.discovery.enabledFeatures();
2735
2751
  if (args.json) {
@@ -3222,27 +3238,127 @@ async function loadOptionalConfig() {
3222
3238
  continue;
3223
3239
  }
3224
3240
  if (name.endsWith(".json")) {
3225
- const { readFile } = await import("fs/promises");
3226
- const raw = await readFile(absolute, "utf8");
3227
- return JSON.parse(raw);
3241
+ const { readFile: readFile2 } = await import("fs/promises");
3242
+ const raw = await readFile2(absolute, "utf8");
3243
+ const cfg2 = JSON.parse(raw);
3244
+ return { url: cfg2.url, apiKey: cfg2.apiKey };
3228
3245
  }
3229
3246
  const mod = await import(pathToFileURL(absolute).href);
3230
3247
  const cfg = mod.default ?? mod;
3231
3248
  return {
3232
3249
  url: cfg.url,
3233
- apiKey: cfg.apiKey,
3234
- projectId: cfg.projectId
3250
+ apiKey: cfg.apiKey
3235
3251
  };
3236
3252
  }
3237
3253
  return {};
3238
3254
  }
3239
3255
 
3256
+ // src/cli/init.ts
3257
+ import { spawn } from "child_process";
3258
+ import { cp, mkdir, readdir, readFile, stat, writeFile } from "fs/promises";
3259
+ import { basename, join, resolve as resolve2 } from "path";
3260
+ import { createInterface } from "readline/promises";
3261
+ var DEFAULT_TEMPLATE = "lunnoa-automate/lunnoa-starter";
3262
+ var SKIP_LOCAL = /* @__PURE__ */ new Set(["node_modules", ".next", ".git", ".env", ".claude", ".cursor"]);
3263
+ function setEnvValues(source, values) {
3264
+ let out = source;
3265
+ for (const [key, value] of Object.entries(values)) {
3266
+ if (!value) continue;
3267
+ const line = `${key}=${value}`;
3268
+ const existing = new RegExp(`^${key}=.*$`, "m");
3269
+ out = existing.test(out) ? out.replace(existing, () => line) : `${out}${out === "" || out.endsWith("\n") ? "" : "\n"}${line}
3270
+ `;
3271
+ }
3272
+ return out;
3273
+ }
3274
+ function dropRetiredProjectEnv(source) {
3275
+ const body = source.split("\n").filter(
3276
+ (line) => !line.includes("LUNNOA_PROJECT_ID") && !line.includes("projects list") && !/Project UUID/i.test(line)
3277
+ ).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "");
3278
+ return body.length ? `${body}
3279
+ ` : "";
3280
+ }
3281
+ async function isDirectory(path) {
3282
+ return (await stat(path).catch(() => void 0))?.isDirectory() ?? false;
3283
+ }
3284
+ async function downloadGithubTemplate(template, dest) {
3285
+ const [repo, ref = "HEAD"] = template.split("#");
3286
+ const url = `https://codeload.github.com/${repo}/tar.gz/${ref}`;
3287
+ const response = await fetch(url);
3288
+ if (!response.ok) {
3289
+ throw new Error(`Could not download template ${template} (${response.status} from ${url}).`);
3290
+ }
3291
+ const tarball = Buffer.from(await response.arrayBuffer());
3292
+ await new Promise((done, fail) => {
3293
+ const child = spawn("tar", ["-xzf", "-", "--strip-components=1"], {
3294
+ cwd: dest,
3295
+ stdio: ["pipe", "ignore", "inherit"]
3296
+ });
3297
+ child.on(
3298
+ "error",
3299
+ (error) => fail(new Error(`Could not run tar (${error.message}). Download ${url} manually.`))
3300
+ );
3301
+ child.on(
3302
+ "close",
3303
+ (code) => code === 0 ? done() : fail(new Error(`tar exited with code ${code}`))
3304
+ );
3305
+ child.stdin.end(tarball);
3306
+ });
3307
+ }
3308
+ async function runInit(args) {
3309
+ if (!args.path) {
3310
+ throw new Error("Missing project directory. Example: init my-app --url https://lunnoa.example");
3311
+ }
3312
+ const dest = resolve2(process.cwd(), args.path);
3313
+ if ((await readdir(dest).catch(() => [])).length > 0) {
3314
+ throw new Error(`${dest} already exists and is not empty.`);
3315
+ }
3316
+ await mkdir(dest, { recursive: true });
3317
+ const template = args.template ?? DEFAULT_TEMPLATE;
3318
+ if (await isDirectory(resolve2(template))) {
3319
+ await cp(resolve2(template), dest, {
3320
+ recursive: true,
3321
+ filter: (src) => !SKIP_LOCAL.has(basename(src))
3322
+ });
3323
+ } else {
3324
+ console.log(`Downloading ${template} ...`);
3325
+ await downloadGithubTemplate(template, dest);
3326
+ }
3327
+ let { url, apiKey } = args;
3328
+ if (process.stdin.isTTY && (!url || !apiKey)) {
3329
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
3330
+ url ||= (await rl.question("Lunnoa URL (e.g. https://lunnoa.your-company.example): ")).trim();
3331
+ apiKey ||= (await rl.question("API key lna_... (server-side only; Enter to skip and sign in as a user instead): ")).trim();
3332
+ rl.close();
3333
+ }
3334
+ const example = await readFile(join(dest, ".env.example"), "utf8").catch(() => "");
3335
+ await writeFile(
3336
+ join(dest, ".env"),
3337
+ setEnvValues(dropRetiredProjectEnv(example), {
3338
+ LUNNOA_URL: url,
3339
+ LUNNOA_API_KEY: apiKey
3340
+ }),
3341
+ "utf8"
3342
+ );
3343
+ console.log(`
3344
+ Created ${dest}
3345
+
3346
+ Next steps:`);
3347
+ console.log(` cd ${args.path}`);
3348
+ console.log(" npm install");
3349
+ if (!apiKey) {
3350
+ console.log(" Add LUNNOA_API_KEY to .env, or set LUNNOA_AUTH_MODE=user to sign in with your Lunnoa account");
3351
+ }
3352
+ console.log(" npm run dev");
3353
+ console.log(" Open the folder in Claude Code / Cursor / Codex and describe your idea.");
3354
+ }
3355
+
3240
3356
  // src/cli/skills.ts
3241
- import { cp } from "fs/promises";
3242
- import { dirname, resolve as resolve2 } from "path";
3357
+ import { cp as cp2 } from "fs/promises";
3358
+ import { dirname, resolve as resolve3 } from "path";
3243
3359
  import { fileURLToPath } from "url";
3244
3360
  var SKILL_TARGETS = [".claude/skills", ".cursor/skills"];
3245
- var BUNDLED_SKILLS_DIR = resolve2(
3361
+ var BUNDLED_SKILLS_DIR = resolve3(
3246
3362
  dirname(fileURLToPath(import.meta.url)),
3247
3363
  "..",
3248
3364
  "skills"
@@ -3250,8 +3366,8 @@ var BUNDLED_SKILLS_DIR = resolve2(
3250
3366
  async function installSkills(projectDir, sourceDir = BUNDLED_SKILLS_DIR) {
3251
3367
  const written = [];
3252
3368
  for (const target of SKILL_TARGETS) {
3253
- const dest = resolve2(projectDir, target);
3254
- await cp(sourceDir, dest, { recursive: true, force: true });
3369
+ const dest = resolve3(projectDir, target);
3370
+ await cp2(sourceDir, dest, { recursive: true, force: true });
3255
3371
  written.push(dest);
3256
3372
  }
3257
3373
  return written;
@@ -3263,7 +3379,6 @@ async function bindClient(args) {
3263
3379
  const url = args.url ?? process.env.LUNNOA_URL ?? fileConfig.url;
3264
3380
  const apiKey = args.apiKey ?? process.env.LUNNOA_API_KEY ?? fileConfig.apiKey;
3265
3381
  const accessToken = args.accessToken;
3266
- const projectId = args.projectId ?? process.env.LUNNOA_PROJECT_ID ?? fileConfig.projectId;
3267
3382
  if (!url) {
3268
3383
  throw new Error("Missing --url (or LUNNOA_URL / lunnoa.config).");
3269
3384
  }
@@ -3274,7 +3389,6 @@ async function bindClient(args) {
3274
3389
  }
3275
3390
  return {
3276
3391
  client: new LunnoaClient({ baseUrl: url, apiKey, accessToken }),
3277
- projectId,
3278
3392
  url
3279
3393
  };
3280
3394
  }
@@ -3312,39 +3426,29 @@ async function runCodegen(args) {
3312
3426
  `Found ${definitions.entityTypes.length} entity type(s), ${definitions.workflows.length} workflow(s), ${definitions.agents.length} agent(s).`
3313
3427
  );
3314
3428
  const moduleSource = generateDeploymentModule(definitions);
3315
- const outDir = resolve3(process.cwd(), args.out);
3316
- await mkdir(outDir, { recursive: true });
3317
- const outFile = resolve3(outDir, "lunnoa.generated.ts");
3318
- await writeFile(outFile, moduleSource, "utf8");
3429
+ const outDir = resolve4(process.cwd(), args.out);
3430
+ await mkdir2(outDir, { recursive: true });
3431
+ const outFile = resolve4(outDir, "lunnoa.generated.ts");
3432
+ await writeFile2(outFile, moduleSource, "utf8");
3319
3433
  console.log(`Wrote ${outFile}`);
3320
3434
  }
3321
3435
  async function deployWorkflow(args) {
3322
3436
  if (!args.path) {
3323
3437
  throw new Error("Missing definition path. Example: workflows deploy ./workflow.ts");
3324
3438
  }
3325
- const { client, projectId } = await bindClient(args);
3326
- if (!projectId) {
3327
- throw new Error(
3328
- "Missing --project-id (or LUNNOA_PROJECT_ID / lunnoa.config)."
3329
- );
3330
- }
3439
+ const { client } = await bindClient(args);
3331
3440
  const mod = await loadDefinitionModule(args.path);
3332
3441
  const def = pickWorkflowDefinition(mod);
3333
3442
  const payload = workflowDefinitionToUpsert(def);
3334
- console.log(`Deploying workflow slug="${payload.slug}" to project ${projectId} ...`);
3335
- const result = await client.workflows.upsertBySlug(projectId, payload);
3443
+ console.log(`Deploying workflow slug="${payload.slug}" ...`);
3444
+ const result = await client.workflows.upsertBySlug(payload);
3336
3445
  console.log(`Upserted workflow ${result.id} (${payload.slug})`);
3337
3446
  }
3338
3447
  async function deployAgent(args) {
3339
3448
  if (!args.path) {
3340
3449
  throw new Error("Missing definition path. Example: agents deploy ./agent.ts");
3341
3450
  }
3342
- const { client, projectId } = await bindClient(args);
3343
- if (!projectId) {
3344
- throw new Error(
3345
- "Missing --project-id (or LUNNOA_PROJECT_ID / lunnoa.config)."
3346
- );
3347
- }
3451
+ const { client } = await bindClient(args);
3348
3452
  const mod = await loadDefinitionModule(args.path);
3349
3453
  const def = pickAgentDefinition(mod);
3350
3454
  const workflowIds = [];
@@ -3353,7 +3457,6 @@ async function deployAgent(args) {
3353
3457
  if (unwrapped && typeof unwrapped === "object" && unwrapped.kind === DEFINE_KINDS.workflow) {
3354
3458
  const wf = unwrapped;
3355
3459
  const upserted = await client.workflows.upsertBySlug(
3356
- projectId,
3357
3460
  workflowDefinitionToUpsert(wf)
3358
3461
  );
3359
3462
  if (upserted.id) {
@@ -3363,8 +3466,8 @@ async function deployAgent(args) {
3363
3466
  }
3364
3467
  }
3365
3468
  const payload = agentDefinitionToUpsert(def, { workflowIds });
3366
- console.log(`Deploying agent slug="${payload.slug}" to project ${projectId} ...`);
3367
- const result = await client.agents.upsertBySlug(projectId, payload);
3469
+ console.log(`Deploying agent slug="${payload.slug}" ...`);
3470
+ const result = await client.agents.upsertBySlug(payload);
3368
3471
  console.log(`Upserted agent ${result.id} (${payload.slug})`);
3369
3472
  }
3370
3473
  async function deployEntityType(args) {
@@ -3387,8 +3490,8 @@ var DISCOVERY_SUBCOMMANDS = {
3387
3490
  tools: ["list", "get", "search"],
3388
3491
  apps: ["list", "get"],
3389
3492
  connections: ["list", "get"],
3390
- features: void 0,
3391
- projects: ["list"]
3493
+ tables: ["list", "get"],
3494
+ features: void 0
3392
3495
  };
3393
3496
  function isDiscoveryCommand(args) {
3394
3497
  const { command, subcommand } = args;
@@ -3398,9 +3501,6 @@ function isDiscoveryCommand(args) {
3398
3501
  if (command === "features") {
3399
3502
  return true;
3400
3503
  }
3401
- if (command === "projects" && (subcommand === "list" || !subcommand)) {
3402
- return true;
3403
- }
3404
3504
  if ((command === "agents" || command === "workflows" || command === "entity-types") && subcommand === "list") {
3405
3505
  return true;
3406
3506
  }
@@ -3423,7 +3523,7 @@ ${USAGE}`
3423
3523
  }
3424
3524
  async function runDiscovery(args) {
3425
3525
  const { command, subcommand } = args;
3426
- if (command === "tools" || command === "apps" || command === "connections") {
3526
+ if (command === "tools" || command === "apps" || command === "connections" || command === "tables") {
3427
3527
  assertKnownDiscoveryShape(args);
3428
3528
  }
3429
3529
  if (!isDiscoveryCommand(args)) {
@@ -3470,8 +3570,12 @@ async function runDiscovery(args) {
3470
3570
  await runEntityTypesList(client, args);
3471
3571
  return true;
3472
3572
  }
3473
- if (command === "projects" && (subcommand === "list" || !subcommand)) {
3474
- await runProjectsList(client, args);
3573
+ if (command === "tables" && subcommand === "list") {
3574
+ await runTablesList(client, args);
3575
+ return true;
3576
+ }
3577
+ if (command === "tables" && subcommand === "get") {
3578
+ await runTablesGet(client, args);
3475
3579
  return true;
3476
3580
  }
3477
3581
  if (command === "features") {
@@ -3482,12 +3586,20 @@ async function runDiscovery(args) {
3482
3586
  }
3483
3587
  async function main() {
3484
3588
  const args = parseArgs(process.argv.slice(2));
3589
+ try {
3590
+ process.loadEnvFile();
3591
+ } catch {
3592
+ }
3485
3593
  if (!args.command || args.command === "--help" || args.command === "help" || args.help) {
3486
3594
  console.log(USAGE);
3487
3595
  return;
3488
3596
  }
3597
+ if (args.command === "init") {
3598
+ await runInit(args);
3599
+ return;
3600
+ }
3489
3601
  if (args.command === "skills" && args.subcommand === "install") {
3490
- const written = await installSkills(resolve3(process.cwd(), args.path ?? "."));
3602
+ const written = await installSkills(resolve4(process.cwd(), args.path ?? "."));
3491
3603
  for (const dest of written) {
3492
3604
  console.log(`Installed Lunnoa skills into ${dest}`);
3493
3605
  }