@lunnoa/client 1.4.0 → 1.5.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
@@ -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,12 +106,12 @@ 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
 
@@ -303,7 +311,7 @@ npx @lunnoa/client projects list --json
303
311
  npx @lunnoa/client features --json
304
312
  ```
305
313
 
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.
314
+ `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
315
 
308
316
  ## Regenerating the API types
309
317
 
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_";
@@ -1083,6 +1083,35 @@ 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(options = {}) {
1094
+ return this.http.get("/api/tables", {
1095
+ query: { projectId: options.projectId }
1096
+ });
1097
+ }
1098
+ /** Returns a single table by ID, including columns and rows. */
1099
+ get(id) {
1100
+ return this.http.get(`/api/tables/${id}`);
1101
+ }
1102
+ /** Returns a single table by its workspace slug, including columns and rows. */
1103
+ getBySlug(slug) {
1104
+ return this.http.get(`/api/tables/slug/${slug}`);
1105
+ }
1106
+ /**
1107
+ * Creates or updates a table by workspace slug. Omitted columns or rows
1108
+ * keep their current value. Marks `managedByCode` by default.
1109
+ */
1110
+ upsertBySlug(input) {
1111
+ return this.http.post("/api/tables/upsert", { body: input });
1112
+ }
1113
+ };
1114
+
1086
1115
  // src/resources/approvals.ts
1087
1116
  var ApprovalsResource = class {
1088
1117
  constructor(http) {
@@ -1422,7 +1451,7 @@ function computeDurationMs(start, end) {
1422
1451
  return b - a;
1423
1452
  }
1424
1453
  function defaultSleep(ms) {
1425
- return new Promise((resolve4) => setTimeout(resolve4, ms));
1454
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
1426
1455
  }
1427
1456
 
1428
1457
  // src/resources/knowledge.ts
@@ -2067,6 +2096,8 @@ var LunnoaClient = class {
2067
2096
  approvals;
2068
2097
  entities;
2069
2098
  entityTypes;
2099
+ /** Small reference / lookup tables (rows stored inline; use entities for transactional data). */
2100
+ tables;
2070
2101
  knowledge;
2071
2102
  queues;
2072
2103
  queueItems;
@@ -2090,6 +2121,7 @@ var LunnoaClient = class {
2090
2121
  this.workflows = new WorkflowsResource(this.http, this.executions);
2091
2122
  this.entities = new EntitiesResource(this.http);
2092
2123
  this.entityTypes = new EntityTypesResource(this.http);
2124
+ this.tables = new TablesResource(this.http);
2093
2125
  this.knowledge = new KnowledgeResource(this.http);
2094
2126
  this.queues = new QueuesResource(this.http);
2095
2127
  this.queueItems = new QueueItemsResource(this.http);
@@ -2106,6 +2138,7 @@ var GROUPS_WITH_SUBCOMMAND = /* @__PURE__ */ new Set([
2106
2138
  "workflows",
2107
2139
  "agents",
2108
2140
  "entity-types",
2141
+ "tables",
2109
2142
  "tools",
2110
2143
  "apps",
2111
2144
  "connections",
@@ -2118,7 +2151,8 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
2118
2151
  "--access-token",
2119
2152
  "--project-id",
2120
2153
  "--out",
2121
- "--app"
2154
+ "--app",
2155
+ "--template"
2122
2156
  ]);
2123
2157
  function requireFlagValue(flag, value) {
2124
2158
  if (!value || value.startsWith("--")) {
@@ -2181,6 +2215,9 @@ function parseArgs(argv) {
2181
2215
  case "--app":
2182
2216
  args.app = value;
2183
2217
  break;
2218
+ case "--template":
2219
+ args.template = value;
2220
+ break;
2184
2221
  default:
2185
2222
  break;
2186
2223
  }
@@ -2196,6 +2233,7 @@ function parseArgs(argv) {
2196
2233
  return args;
2197
2234
  }
2198
2235
  var USAGE = `Usage:
2236
+ npx @lunnoa/client init <dir> [--url <url>] [--api-key <lna_key>] [--template <owner/repo|path>]
2199
2237
  npx @lunnoa/client codegen --url <url> --api-key <lna_key> [--out <dir>]
2200
2238
 
2201
2239
  Discover (do not invent catalogue action ids):
@@ -2209,6 +2247,8 @@ var USAGE = `Usage:
2209
2247
  npx @lunnoa/client agents list [--json]
2210
2248
  npx @lunnoa/client workflows list [--json]
2211
2249
  npx @lunnoa/client entity-types list [--json]
2250
+ npx @lunnoa/client tables list [--json]
2251
+ npx @lunnoa/client tables get <slugOrId> [--json]
2212
2252
  npx @lunnoa/client projects list [--json]
2213
2253
  npx @lunnoa/client features [--json]
2214
2254
 
@@ -2228,10 +2268,11 @@ Options:
2228
2268
  --out Output directory for lunnoa.generated.ts (codegen; default: ./src/lunnoa)
2229
2269
  --app Restrict tools/connections to one workflow app id
2230
2270
  --for-agent Only catalogue entries with availableForAgent
2271
+ --template init starter: GitHub owner/repo[#ref] or local folder (default: lunnoa-automate/lunnoa-starter)
2231
2272
  --json Machine-readable JSON (preferred for coding agents)
2232
2273
  --help Show this help
2233
2274
 
2234
- Environment: LUNNOA_URL, LUNNOA_API_KEY, LUNNOA_PROJECT_ID
2275
+ Environment: LUNNOA_URL, LUNNOA_API_KEY, LUNNOA_PROJECT_ID (also read from ./.env)
2235
2276
  Optional config file: lunnoa.config.ts | .mjs | .js | .json
2236
2277
 
2237
2278
  tools get prints inputConfig and a defineAction() sketch. tools list is compact;
@@ -2708,6 +2749,39 @@ async function runEntityTypesList(client, args) {
2708
2749
  { key: "managedByCode", header: "CODE" }
2709
2750
  ]);
2710
2751
  }
2752
+ async function runTablesList(client, args) {
2753
+ const tables = await client.tables.list({ projectId: args.projectId });
2754
+ const rows = tables.map((table) => ({
2755
+ id: table.id,
2756
+ slug: table.slug,
2757
+ name: table.name,
2758
+ description: table.description ?? "",
2759
+ managedByCode: Boolean(table.managedByCode)
2760
+ }));
2761
+ if (args.json) {
2762
+ printJson(rows);
2763
+ return;
2764
+ }
2765
+ printTable(rows, [
2766
+ { key: "id", header: "ID" },
2767
+ { key: "slug", header: "SLUG" },
2768
+ { key: "name", header: "NAME" },
2769
+ { key: "managedByCode", header: "CODE" }
2770
+ ]);
2771
+ }
2772
+ async function runTablesGet(client, args) {
2773
+ const ref = requireTarget(args, "table slug or id (example: tables get cost_centres)");
2774
+ const table = /^[0-9a-f]{8}-[0-9a-f]{4}-/i.test(ref) ? await client.tables.get(ref) : await client.tables.getBySlug(ref);
2775
+ if (args.json) {
2776
+ printJson(table);
2777
+ return;
2778
+ }
2779
+ console.log(`id: ${table.id}`);
2780
+ console.log(`slug: ${table.slug}`);
2781
+ console.log(`name: ${table.name}`);
2782
+ console.log(`columns: ${(table.columns ?? []).map((c) => c.key).join(", ")}`);
2783
+ console.log(`rows: ${table.rows?.length ?? 0}`);
2784
+ }
2711
2785
  async function runProjectsList(client, args) {
2712
2786
  const projects = await client.projects.list({
2713
2787
  expansion: ["description", "countAgents", "countWorkflows"]
@@ -3222,8 +3296,8 @@ async function loadOptionalConfig() {
3222
3296
  continue;
3223
3297
  }
3224
3298
  if (name.endsWith(".json")) {
3225
- const { readFile } = await import("fs/promises");
3226
- const raw = await readFile(absolute, "utf8");
3299
+ const { readFile: readFile2 } = await import("fs/promises");
3300
+ const raw = await readFile2(absolute, "utf8");
3227
3301
  return JSON.parse(raw);
3228
3302
  }
3229
3303
  const mod = await import(pathToFileURL(absolute).href);
@@ -3237,12 +3311,102 @@ async function loadOptionalConfig() {
3237
3311
  return {};
3238
3312
  }
3239
3313
 
3314
+ // src/cli/init.ts
3315
+ import { spawn } from "child_process";
3316
+ import { cp, mkdir, readdir, readFile, stat, writeFile } from "fs/promises";
3317
+ import { basename, join, resolve as resolve2 } from "path";
3318
+ import { createInterface } from "readline/promises";
3319
+ var DEFAULT_TEMPLATE = "lunnoa-automate/lunnoa-starter";
3320
+ var SKIP_LOCAL = /* @__PURE__ */ new Set(["node_modules", ".next", ".git", ".env", ".claude", ".cursor"]);
3321
+ function setEnvValues(source, values) {
3322
+ let out = source;
3323
+ for (const [key, value] of Object.entries(values)) {
3324
+ if (!value) continue;
3325
+ const line = `${key}=${value}`;
3326
+ const existing = new RegExp(`^${key}=.*$`, "m");
3327
+ out = existing.test(out) ? out.replace(existing, () => line) : `${out}${out === "" || out.endsWith("\n") ? "" : "\n"}${line}
3328
+ `;
3329
+ }
3330
+ return out;
3331
+ }
3332
+ async function isDirectory(path) {
3333
+ return (await stat(path).catch(() => void 0))?.isDirectory() ?? false;
3334
+ }
3335
+ async function downloadGithubTemplate(template, dest) {
3336
+ const [repo, ref = "HEAD"] = template.split("#");
3337
+ const url = `https://codeload.github.com/${repo}/tar.gz/${ref}`;
3338
+ const response = await fetch(url);
3339
+ if (!response.ok) {
3340
+ throw new Error(`Could not download template ${template} (${response.status} from ${url}).`);
3341
+ }
3342
+ const tarball = Buffer.from(await response.arrayBuffer());
3343
+ await new Promise((done, fail) => {
3344
+ const child = spawn("tar", ["-xzf", "-", "--strip-components=1"], {
3345
+ cwd: dest,
3346
+ stdio: ["pipe", "ignore", "inherit"]
3347
+ });
3348
+ child.on(
3349
+ "error",
3350
+ (error) => fail(new Error(`Could not run tar (${error.message}). Download ${url} manually.`))
3351
+ );
3352
+ child.on(
3353
+ "close",
3354
+ (code) => code === 0 ? done() : fail(new Error(`tar exited with code ${code}`))
3355
+ );
3356
+ child.stdin.end(tarball);
3357
+ });
3358
+ }
3359
+ async function runInit(args) {
3360
+ if (!args.path) {
3361
+ throw new Error("Missing project directory. Example: init my-app --url https://lunnoa.example");
3362
+ }
3363
+ const dest = resolve2(process.cwd(), args.path);
3364
+ if ((await readdir(dest).catch(() => [])).length > 0) {
3365
+ throw new Error(`${dest} already exists and is not empty.`);
3366
+ }
3367
+ await mkdir(dest, { recursive: true });
3368
+ const template = args.template ?? DEFAULT_TEMPLATE;
3369
+ if (await isDirectory(resolve2(template))) {
3370
+ await cp(resolve2(template), dest, {
3371
+ recursive: true,
3372
+ filter: (src) => !SKIP_LOCAL.has(basename(src))
3373
+ });
3374
+ } else {
3375
+ console.log(`Downloading ${template} ...`);
3376
+ await downloadGithubTemplate(template, dest);
3377
+ }
3378
+ let { url, apiKey } = args;
3379
+ if (process.stdin.isTTY && (!url || !apiKey)) {
3380
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
3381
+ url ||= (await rl.question("Lunnoa URL (e.g. https://lunnoa.your-company.example): ")).trim();
3382
+ apiKey ||= (await rl.question("API key lna_... (server-side only; Enter to skip and sign in as a user instead): ")).trim();
3383
+ rl.close();
3384
+ }
3385
+ const example = await readFile(join(dest, ".env.example"), "utf8").catch(() => "");
3386
+ await writeFile(
3387
+ join(dest, ".env"),
3388
+ setEnvValues(example, { LUNNOA_URL: url, LUNNOA_API_KEY: apiKey }),
3389
+ "utf8"
3390
+ );
3391
+ console.log(`
3392
+ Created ${dest}
3393
+
3394
+ Next steps:`);
3395
+ console.log(` cd ${args.path}`);
3396
+ console.log(" npm install");
3397
+ if (!apiKey) {
3398
+ console.log(" Add LUNNOA_API_KEY to .env, or set LUNNOA_AUTH_MODE=user to sign in with your Lunnoa account");
3399
+ }
3400
+ console.log(" npm run dev");
3401
+ console.log(" Open the folder in Claude Code / Cursor / Codex and describe your idea.");
3402
+ }
3403
+
3240
3404
  // src/cli/skills.ts
3241
- import { cp } from "fs/promises";
3242
- import { dirname, resolve as resolve2 } from "path";
3405
+ import { cp as cp2 } from "fs/promises";
3406
+ import { dirname, resolve as resolve3 } from "path";
3243
3407
  import { fileURLToPath } from "url";
3244
3408
  var SKILL_TARGETS = [".claude/skills", ".cursor/skills"];
3245
- var BUNDLED_SKILLS_DIR = resolve2(
3409
+ var BUNDLED_SKILLS_DIR = resolve3(
3246
3410
  dirname(fileURLToPath(import.meta.url)),
3247
3411
  "..",
3248
3412
  "skills"
@@ -3250,8 +3414,8 @@ var BUNDLED_SKILLS_DIR = resolve2(
3250
3414
  async function installSkills(projectDir, sourceDir = BUNDLED_SKILLS_DIR) {
3251
3415
  const written = [];
3252
3416
  for (const target of SKILL_TARGETS) {
3253
- const dest = resolve2(projectDir, target);
3254
- await cp(sourceDir, dest, { recursive: true, force: true });
3417
+ const dest = resolve3(projectDir, target);
3418
+ await cp2(sourceDir, dest, { recursive: true, force: true });
3255
3419
  written.push(dest);
3256
3420
  }
3257
3421
  return written;
@@ -3312,10 +3476,10 @@ async function runCodegen(args) {
3312
3476
  `Found ${definitions.entityTypes.length} entity type(s), ${definitions.workflows.length} workflow(s), ${definitions.agents.length} agent(s).`
3313
3477
  );
3314
3478
  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");
3479
+ const outDir = resolve4(process.cwd(), args.out);
3480
+ await mkdir2(outDir, { recursive: true });
3481
+ const outFile = resolve4(outDir, "lunnoa.generated.ts");
3482
+ await writeFile2(outFile, moduleSource, "utf8");
3319
3483
  console.log(`Wrote ${outFile}`);
3320
3484
  }
3321
3485
  async function deployWorkflow(args) {
@@ -3387,6 +3551,7 @@ var DISCOVERY_SUBCOMMANDS = {
3387
3551
  tools: ["list", "get", "search"],
3388
3552
  apps: ["list", "get"],
3389
3553
  connections: ["list", "get"],
3554
+ tables: ["list", "get"],
3390
3555
  features: void 0,
3391
3556
  projects: ["list"]
3392
3557
  };
@@ -3423,7 +3588,7 @@ ${USAGE}`
3423
3588
  }
3424
3589
  async function runDiscovery(args) {
3425
3590
  const { command, subcommand } = args;
3426
- if (command === "tools" || command === "apps" || command === "connections") {
3591
+ if (command === "tools" || command === "apps" || command === "connections" || command === "tables") {
3427
3592
  assertKnownDiscoveryShape(args);
3428
3593
  }
3429
3594
  if (!isDiscoveryCommand(args)) {
@@ -3470,6 +3635,14 @@ async function runDiscovery(args) {
3470
3635
  await runEntityTypesList(client, args);
3471
3636
  return true;
3472
3637
  }
3638
+ if (command === "tables" && subcommand === "list") {
3639
+ await runTablesList(client, args);
3640
+ return true;
3641
+ }
3642
+ if (command === "tables" && subcommand === "get") {
3643
+ await runTablesGet(client, args);
3644
+ return true;
3645
+ }
3473
3646
  if (command === "projects" && (subcommand === "list" || !subcommand)) {
3474
3647
  await runProjectsList(client, args);
3475
3648
  return true;
@@ -3482,12 +3655,20 @@ async function runDiscovery(args) {
3482
3655
  }
3483
3656
  async function main() {
3484
3657
  const args = parseArgs(process.argv.slice(2));
3658
+ try {
3659
+ process.loadEnvFile();
3660
+ } catch {
3661
+ }
3485
3662
  if (!args.command || args.command === "--help" || args.command === "help" || args.help) {
3486
3663
  console.log(USAGE);
3487
3664
  return;
3488
3665
  }
3666
+ if (args.command === "init") {
3667
+ await runInit(args);
3668
+ return;
3669
+ }
3489
3670
  if (args.command === "skills" && args.subcommand === "install") {
3490
- const written = await installSkills(resolve3(process.cwd(), args.path ?? "."));
3671
+ const written = await installSkills(resolve4(process.cwd(), args.path ?? "."));
3491
3672
  for (const dest of written) {
3492
3673
  console.log(`Installed Lunnoa skills into ${dest}`);
3493
3674
  }
package/dist/index.cjs CHANGED
@@ -1125,6 +1125,35 @@ var EntityTypesResource = class {
1125
1125
  }
1126
1126
  };
1127
1127
 
1128
+ // src/resources/tables.ts
1129
+ var TablesResource = class {
1130
+ constructor(http) {
1131
+ this.http = http;
1132
+ }
1133
+ http;
1134
+ /** Lists tables in the API key's workspace (without columns and rows). */
1135
+ list(options = {}) {
1136
+ return this.http.get("/api/tables", {
1137
+ query: { projectId: options.projectId }
1138
+ });
1139
+ }
1140
+ /** Returns a single table by ID, including columns and rows. */
1141
+ get(id) {
1142
+ return this.http.get(`/api/tables/${id}`);
1143
+ }
1144
+ /** Returns a single table by its workspace slug, including columns and rows. */
1145
+ getBySlug(slug) {
1146
+ return this.http.get(`/api/tables/slug/${slug}`);
1147
+ }
1148
+ /**
1149
+ * Creates or updates a table by workspace slug. Omitted columns or rows
1150
+ * keep their current value. Marks `managedByCode` by default.
1151
+ */
1152
+ upsertBySlug(input) {
1153
+ return this.http.post("/api/tables/upsert", { body: input });
1154
+ }
1155
+ };
1156
+
1128
1157
  // src/resources/approvals.ts
1129
1158
  var ApprovalsResource = class {
1130
1159
  constructor(http) {
@@ -2109,6 +2138,8 @@ var LunnoaClient = class {
2109
2138
  approvals;
2110
2139
  entities;
2111
2140
  entityTypes;
2141
+ /** Small reference / lookup tables (rows stored inline; use entities for transactional data). */
2142
+ tables;
2112
2143
  knowledge;
2113
2144
  queues;
2114
2145
  queueItems;
@@ -2132,6 +2163,7 @@ var LunnoaClient = class {
2132
2163
  this.workflows = new WorkflowsResource(this.http, this.executions);
2133
2164
  this.entities = new EntitiesResource(this.http);
2134
2165
  this.entityTypes = new EntityTypesResource(this.http);
2166
+ this.tables = new TablesResource(this.http);
2135
2167
  this.knowledge = new KnowledgeResource(this.http);
2136
2168
  this.queues = new QueuesResource(this.http);
2137
2169
  this.queueItems = new QueueItemsResource(this.http);
@@ -2240,6 +2272,7 @@ exports.QueuesResource = QueuesResource;
2240
2272
  exports.RunsResource = RunsResource;
2241
2273
  exports.ScorecardsResource = ScorecardsResource;
2242
2274
  exports.SsoAuthResource = SsoAuthResource;
2275
+ exports.TablesResource = TablesResource;
2243
2276
  exports.TasksResource = TasksResource;
2244
2277
  exports.VariablesResource = VariablesResource;
2245
2278
  exports.WorkflowAppsResource = WorkflowAppsResource;