@lunnoa/client 1.1.0 → 1.2.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
@@ -283,6 +283,26 @@ npx @lunnoa/client agents deploy ./agents/triage.ts \
283
283
 
284
284
  `defineAction` binds catalogue actions only; toolkit `createAction` is for in-platform apps. See Lunnoa Docs under `guides/develop/client/define`, `runs`, and `cli-deploy`.
285
285
 
286
+ ### CLI discovery
287
+
288
+ Coding agents (and humans) should list the catalogue for a given key instead of guessing action ids. `--json` is the machine-readable form.
289
+
290
+ ```bash
291
+ npx @lunnoa/client tools list --url … --api-key lna_… --for-agent --json
292
+ npx @lunnoa/client tools search email --for-agent --json
293
+ npx @lunnoa/client tools get http_action_send-request --json
294
+ npx @lunnoa/client apps list --json
295
+ npx @lunnoa/client apps get http --json
296
+ npx @lunnoa/client connections list --app gmail --json
297
+ npx @lunnoa/client agents list --json
298
+ npx @lunnoa/client workflows list --json
299
+ npx @lunnoa/client entity-types list --json
300
+ npx @lunnoa/client projects list --json
301
+ npx @lunnoa/client features --json
302
+ ```
303
+
304
+ `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.
305
+
286
306
  ## Regenerating the API types
287
307
 
288
308
  The generic types are generated from the deployment's public OpenAPI spec (`spec/openapi.public.json`, produced by the platform's `export-openapi --public`):
package/dist/cli.js CHANGED
@@ -2049,6 +2049,646 @@ var LunnoaClient = class {
2049
2049
  }
2050
2050
  };
2051
2051
 
2052
+ // src/cli/args.ts
2053
+ var GROUPS_WITH_SUBCOMMAND = /* @__PURE__ */ new Set([
2054
+ "workflows",
2055
+ "agents",
2056
+ "entity-types",
2057
+ "tools",
2058
+ "apps",
2059
+ "connections",
2060
+ "projects"
2061
+ ]);
2062
+ var VALUE_FLAGS = /* @__PURE__ */ new Set([
2063
+ "--url",
2064
+ "--api-key",
2065
+ "--access-token",
2066
+ "--project-id",
2067
+ "--out",
2068
+ "--app"
2069
+ ]);
2070
+ function requireFlagValue(flag, value) {
2071
+ if (!value || value.startsWith("--")) {
2072
+ throw new Error(`Missing value for ${flag}`);
2073
+ }
2074
+ return value;
2075
+ }
2076
+ function parseArgs(argv) {
2077
+ const args = {
2078
+ command: argv[0],
2079
+ out: "./src/lunnoa",
2080
+ json: false,
2081
+ forAgent: false,
2082
+ help: false
2083
+ };
2084
+ let i = 1;
2085
+ if (args.command && GROUPS_WITH_SUBCOMMAND.has(args.command)) {
2086
+ const next = argv[1];
2087
+ if (next && !next.startsWith("--")) {
2088
+ args.subcommand = next;
2089
+ i = 2;
2090
+ if (argv[2] && !argv[2].startsWith("--")) {
2091
+ args.path = argv[2];
2092
+ i = 3;
2093
+ }
2094
+ }
2095
+ }
2096
+ for (; i < argv.length; i += 1) {
2097
+ const flag = argv[i];
2098
+ if (flag === "--help" || flag === "-h") {
2099
+ args.help = true;
2100
+ continue;
2101
+ }
2102
+ if (flag === "--json") {
2103
+ args.json = true;
2104
+ continue;
2105
+ }
2106
+ if (flag === "--for-agent") {
2107
+ args.forAgent = true;
2108
+ continue;
2109
+ }
2110
+ if (flag && VALUE_FLAGS.has(flag)) {
2111
+ const value = requireFlagValue(flag, argv[i + 1]);
2112
+ switch (flag) {
2113
+ case "--url":
2114
+ args.url = value;
2115
+ break;
2116
+ case "--api-key":
2117
+ args.apiKey = value;
2118
+ break;
2119
+ case "--access-token":
2120
+ args.accessToken = value;
2121
+ break;
2122
+ case "--project-id":
2123
+ args.projectId = value;
2124
+ break;
2125
+ case "--out":
2126
+ args.out = value;
2127
+ break;
2128
+ case "--app":
2129
+ args.app = value;
2130
+ break;
2131
+ default:
2132
+ break;
2133
+ }
2134
+ i += 1;
2135
+ continue;
2136
+ }
2137
+ if (flag && !flag.startsWith("--") && !args.path) {
2138
+ args.path = flag;
2139
+ continue;
2140
+ }
2141
+ throw new Error(`Unknown option: ${flag}`);
2142
+ }
2143
+ return args;
2144
+ }
2145
+ var USAGE = `Usage:
2146
+ npx @lunnoa/client codegen --url <url> --api-key <lna_key> [--out <dir>]
2147
+
2148
+ Discover (do not invent catalogue action ids):
2149
+ npx @lunnoa/client tools list [--app <appId>] [--for-agent] [--json]
2150
+ npx @lunnoa/client tools get <actionId> [--app <appId>] [--json]
2151
+ npx @lunnoa/client tools search <query> [--app <appId>] [--for-agent] [--json]
2152
+ npx @lunnoa/client apps list [--for-agent] [--json]
2153
+ npx @lunnoa/client apps get <appId> [--json]
2154
+ npx @lunnoa/client connections list [--app <appId>] [--json]
2155
+ npx @lunnoa/client connections get <connectionId> [--json]
2156
+ npx @lunnoa/client agents list [--json]
2157
+ npx @lunnoa/client workflows list [--json]
2158
+ npx @lunnoa/client entity-types list [--json]
2159
+ npx @lunnoa/client projects list [--json]
2160
+ npx @lunnoa/client features [--json]
2161
+
2162
+ Deploy (upsert by slug):
2163
+ npx @lunnoa/client workflows deploy <path> --url <url> --api-key <key> --project-id <uuid>
2164
+ npx @lunnoa/client agents deploy <path> --url <url> --api-key <key> --project-id <uuid>
2165
+ npx @lunnoa/client entity-types deploy <path> --url <url> --api-key <key>
2166
+
2167
+ Options:
2168
+ --url Base URL of the Lunnoa Automate deployment
2169
+ --api-key Machine API key (lna_...)
2170
+ --access-token User JWT as an alternative to --api-key
2171
+ --project-id Project UUID (required for workflow/agent deploy)
2172
+ --out Output directory for lunnoa.generated.ts (codegen; default: ./src/lunnoa)
2173
+ --app Restrict tools/connections to one workflow app id
2174
+ --for-agent Only catalogue entries with availableForAgent
2175
+ --json Machine-readable JSON (preferred for coding agents)
2176
+ --help Show this help
2177
+
2178
+ Environment: LUNNOA_URL, LUNNOA_API_KEY, LUNNOA_PROJECT_ID
2179
+ Optional config file: lunnoa.config.ts | .mjs | .js | .json
2180
+
2181
+ tools get prints inputConfig and a defineAction() sketch. tools list is compact;
2182
+ run tools get for the schema. Entity-type deploy is workspace-scoped (no
2183
+ --project-id). Unknown instance attributes are rejected unless
2184
+ allowUnknownAttributes is true; use extensions.
2185
+ `;
2186
+
2187
+ // src/cli/catalog.ts
2188
+ function asRecord(value) {
2189
+ if (value && typeof value === "object" && !Array.isArray(value)) {
2190
+ return value;
2191
+ }
2192
+ return void 0;
2193
+ }
2194
+ function flattenActions(apps) {
2195
+ const out = [];
2196
+ for (const app of apps) {
2197
+ const appId = typeof app.id === "string" ? app.id : "";
2198
+ if (!appId) {
2199
+ continue;
2200
+ }
2201
+ const actions = Array.isArray(app.actions) ? app.actions : [];
2202
+ for (const raw of actions) {
2203
+ const action = asRecord(raw);
2204
+ if (!action || typeof action.id !== "string" || !action.id) {
2205
+ continue;
2206
+ }
2207
+ const actionId = action.id;
2208
+ out.push({
2209
+ appId,
2210
+ appName: typeof app.name === "string" ? app.name : appId,
2211
+ actionId,
2212
+ name: typeof action.name === "string" ? action.name : actionId,
2213
+ description: typeof action.description === "string" ? action.description : "",
2214
+ availableForAgent: typeof action.availableForAgent === "boolean" ? action.availableForAgent : Boolean(app.availableForAgent),
2215
+ needsConnection: typeof action.needsConnection === "boolean" ? action.needsConnection : Boolean(app.needsConnection),
2216
+ inputConfig: action.inputConfig
2217
+ });
2218
+ }
2219
+ }
2220
+ return out;
2221
+ }
2222
+ function filterActions(actions, filter = {}) {
2223
+ const appId = filter.appId?.trim();
2224
+ const query = filter.query?.trim().toLowerCase();
2225
+ return actions.filter((action) => {
2226
+ if (appId && action.appId !== appId) {
2227
+ return false;
2228
+ }
2229
+ if (filter.forAgent && !action.availableForAgent) {
2230
+ return false;
2231
+ }
2232
+ if (query) {
2233
+ const haystack = [
2234
+ action.appId,
2235
+ action.appName,
2236
+ action.actionId,
2237
+ action.name,
2238
+ action.description
2239
+ ].join(" ").toLowerCase();
2240
+ if (!haystack.includes(query)) {
2241
+ return false;
2242
+ }
2243
+ }
2244
+ return true;
2245
+ });
2246
+ }
2247
+ function parseActionRef(raw) {
2248
+ const trimmed = raw.trim();
2249
+ const dot = trimmed.indexOf(".");
2250
+ if (dot > 0) {
2251
+ const maybeApp = trimmed.slice(0, dot);
2252
+ const maybeAction = trimmed.slice(dot + 1);
2253
+ if (maybeApp && maybeAction && !maybeApp.includes("_action_")) {
2254
+ return { appId: maybeApp, actionId: maybeAction };
2255
+ }
2256
+ }
2257
+ return { actionId: trimmed };
2258
+ }
2259
+ function resolveAction(actions, rawRef, appId) {
2260
+ const parsed = parseActionRef(rawRef);
2261
+ const scope = appId?.trim() || parsed.appId;
2262
+ const matches = actions.filter((action) => {
2263
+ if (action.actionId !== parsed.actionId) {
2264
+ return false;
2265
+ }
2266
+ return scope ? action.appId === scope : true;
2267
+ });
2268
+ if (matches.length === 1) {
2269
+ return matches[0];
2270
+ }
2271
+ if (matches.length === 0) {
2272
+ const hint = scope ? `No action "${parsed.actionId}" on app "${scope}". Try: tools search ${parsed.actionId}` : `Unknown action "${parsed.actionId}". Try: tools search ${parsed.actionId}`;
2273
+ throw new Error(hint);
2274
+ }
2275
+ const apps = matches.map((m) => m.appId).join(", ");
2276
+ throw new Error(
2277
+ `Action "${parsed.actionId}" exists on multiple apps (${apps}). Pass --app <appId>.`
2278
+ );
2279
+ }
2280
+ function extractInputFields(inputConfig) {
2281
+ if (!inputConfig) {
2282
+ return [];
2283
+ }
2284
+ if (Array.isArray(inputConfig)) {
2285
+ return inputConfig.map((item) => fieldFromUnknown(item)).filter((field) => Boolean(field));
2286
+ }
2287
+ const record = asRecord(inputConfig);
2288
+ if (!record) {
2289
+ return [];
2290
+ }
2291
+ if (Array.isArray(record.fields)) {
2292
+ return extractInputFields(record.fields);
2293
+ }
2294
+ if (Array.isArray(record.sections)) {
2295
+ return record.sections.flatMap((section) => {
2296
+ const sectionRecord = asRecord(section);
2297
+ return extractInputFields(sectionRecord?.fields ?? section);
2298
+ });
2299
+ }
2300
+ return Object.entries(record).filter(([, value]) => value === null || typeof value !== "object" || !Array.isArray(value)).map(([id, value]) => {
2301
+ const field = asRecord(value);
2302
+ if (!field) {
2303
+ return { id };
2304
+ }
2305
+ return {
2306
+ id: typeof field.id === "string" ? field.id : id,
2307
+ label: typeof field.label === "string" ? field.label : void 0,
2308
+ type: typeof field.type === "string" ? field.type : typeof field.inputType === "string" ? field.inputType : void 0,
2309
+ required: typeof field.required === "boolean" ? field.required : void 0
2310
+ };
2311
+ });
2312
+ }
2313
+ function fieldFromUnknown(value) {
2314
+ const field = asRecord(value);
2315
+ if (!field || typeof field.id !== "string" || !field.id) {
2316
+ return void 0;
2317
+ }
2318
+ return {
2319
+ id: field.id,
2320
+ label: typeof field.label === "string" ? field.label : void 0,
2321
+ type: typeof field.type === "string" ? field.type : typeof field.inputType === "string" ? field.inputType : void 0,
2322
+ required: typeof field.required === "boolean" ? field.required : void 0
2323
+ };
2324
+ }
2325
+ function summarizeAction(action) {
2326
+ return {
2327
+ appId: action.appId,
2328
+ actionId: action.actionId,
2329
+ name: action.name,
2330
+ description: action.description,
2331
+ availableForAgent: action.availableForAgent,
2332
+ needsConnection: action.needsConnection
2333
+ };
2334
+ }
2335
+ function formatDefineActionSnippet(action) {
2336
+ const fields = extractInputFields(action.inputConfig);
2337
+ const inputBlock = fields.length === 0 ? " // no inputConfig fields advertised" : fields.map((field) => {
2338
+ const notes = [
2339
+ field.label && field.label !== field.id ? field.label : void 0,
2340
+ field.type,
2341
+ field.required ? "required" : void 0
2342
+ ].filter(Boolean).join(", ");
2343
+ return notes ? ` // ${field.id}${notes ? ` \u2014 ${notes}` : ""}` : ` // ${field.id}`;
2344
+ }).join("\n");
2345
+ return [
2346
+ "defineAction({",
2347
+ ` id: '${action.actionId}',`,
2348
+ ` appId: '${action.appId}',`,
2349
+ " input: {",
2350
+ inputBlock,
2351
+ " },",
2352
+ "})"
2353
+ ].join("\n");
2354
+ }
2355
+ function actionDetail(action) {
2356
+ return {
2357
+ ...summarizeAction(action),
2358
+ inputFields: extractInputFields(action.inputConfig),
2359
+ inputConfig: action.inputConfig ?? null,
2360
+ defineAction: formatDefineActionSnippet(action)
2361
+ };
2362
+ }
2363
+
2364
+ // src/cli/print.ts
2365
+ function printJson(value) {
2366
+ console.log(JSON.stringify(value, null, 2));
2367
+ }
2368
+ function formatTable(rows, columns) {
2369
+ if (rows.length === 0) {
2370
+ return "No results.";
2371
+ }
2372
+ const widths = columns.map((col) => {
2373
+ let width = col.header.length;
2374
+ for (const row of rows) {
2375
+ width = Math.max(width, stringifyCell(row[col.key]).length);
2376
+ }
2377
+ return width;
2378
+ });
2379
+ const header = columns.map((col, i) => pad(col.header, widths[i])).join(" ");
2380
+ const divider = widths.map((w) => "-".repeat(w)).join(" ");
2381
+ const body = rows.map(
2382
+ (row) => columns.map((col, i) => pad(stringifyCell(row[col.key]), widths[i])).join(" ")
2383
+ );
2384
+ return [header, divider, ...body].join("\n");
2385
+ }
2386
+ function printTable(rows, columns) {
2387
+ console.log(formatTable(rows, columns));
2388
+ }
2389
+ function stringifyCell(value) {
2390
+ if (value === null || value === void 0) {
2391
+ return "";
2392
+ }
2393
+ if (typeof value === "boolean") {
2394
+ return value ? "yes" : "no";
2395
+ }
2396
+ return String(value);
2397
+ }
2398
+ function pad(value, width) {
2399
+ return value.length >= width ? value : `${value}${" ".repeat(width - value.length)}`;
2400
+ }
2401
+
2402
+ // src/cli/discover.ts
2403
+ function requireTarget(args, label) {
2404
+ const value = args.path?.trim();
2405
+ if (!value) {
2406
+ throw new Error(`Missing ${label}.`);
2407
+ }
2408
+ return value;
2409
+ }
2410
+ async function loadCatalog(client) {
2411
+ return flattenActions(await client.workflowApps.list());
2412
+ }
2413
+ async function runToolsList(client, args) {
2414
+ const actions = filterActions(await loadCatalog(client), {
2415
+ appId: args.app,
2416
+ forAgent: args.forAgent
2417
+ });
2418
+ const rows = actions.map(summarizeAction);
2419
+ if (args.json) {
2420
+ printJson(rows);
2421
+ return;
2422
+ }
2423
+ printTable(rows, [
2424
+ { key: "appId", header: "APP" },
2425
+ { key: "actionId", header: "ACTION" },
2426
+ { key: "name", header: "NAME" },
2427
+ { key: "availableForAgent", header: "FOR_AGENT" },
2428
+ { key: "needsConnection", header: "NEEDS_CONN" }
2429
+ ]);
2430
+ }
2431
+ async function runToolsGet(client, args) {
2432
+ const ref = requireTarget(args, "action id (example: tools get http_action_send-request)");
2433
+ const action = resolveAction(await loadCatalog(client), ref, args.app);
2434
+ const detail = actionDetail(action);
2435
+ if (args.json) {
2436
+ printJson(detail);
2437
+ return;
2438
+ }
2439
+ console.log(`appId: ${detail.appId}`);
2440
+ console.log(`actionId: ${detail.actionId}`);
2441
+ console.log(`name: ${detail.name}`);
2442
+ if (detail.description) {
2443
+ console.log(`description: ${detail.description}`);
2444
+ }
2445
+ console.log(`availableForAgent: ${detail.availableForAgent ? "yes" : "no"}`);
2446
+ console.log(`needsConnection: ${detail.needsConnection ? "yes" : "no"}`);
2447
+ console.log("");
2448
+ console.log("inputFields:");
2449
+ if (detail.inputFields.length === 0) {
2450
+ console.log(" (none advertised)");
2451
+ } else {
2452
+ for (const field of detail.inputFields) {
2453
+ const bits = [
2454
+ field.label && field.label !== field.id ? field.label : void 0,
2455
+ field.type,
2456
+ field.required ? "required" : void 0
2457
+ ].filter(Boolean);
2458
+ console.log(` ${field.id}${bits.length ? ` (${bits.join(", ")})` : ""}`);
2459
+ }
2460
+ }
2461
+ console.log("");
2462
+ console.log("defineAction:");
2463
+ console.log(detail.defineAction);
2464
+ }
2465
+ async function runToolsSearch(client, args) {
2466
+ const query = requireTarget(args, "search query (example: tools search email)");
2467
+ const actions = filterActions(await loadCatalog(client), {
2468
+ appId: args.app,
2469
+ forAgent: args.forAgent,
2470
+ query
2471
+ });
2472
+ const rows = actions.map(summarizeAction);
2473
+ if (args.json) {
2474
+ printJson(rows);
2475
+ return;
2476
+ }
2477
+ printTable(rows, [
2478
+ { key: "appId", header: "APP" },
2479
+ { key: "actionId", header: "ACTION" },
2480
+ { key: "name", header: "NAME" },
2481
+ { key: "availableForAgent", header: "FOR_AGENT" },
2482
+ { key: "needsConnection", header: "NEEDS_CONN" }
2483
+ ]);
2484
+ }
2485
+ async function runAppsList(client, args) {
2486
+ const apps = await client.workflowApps.list();
2487
+ const rows = apps.filter((app) => args.forAgent ? Boolean(app.availableForAgent) : true).filter((app) => args.app ? app.id === args.app : true).map((app) => ({
2488
+ id: app.id ?? "",
2489
+ name: app.name ?? app.id ?? "",
2490
+ availableForAgent: Boolean(app.availableForAgent),
2491
+ needsConnection: Boolean(app.needsConnection),
2492
+ actions: Array.isArray(app.actions) ? app.actions.length : 0
2493
+ }));
2494
+ if (args.json) {
2495
+ printJson(rows);
2496
+ return;
2497
+ }
2498
+ printTable(rows, [
2499
+ { key: "id", header: "APP" },
2500
+ { key: "name", header: "NAME" },
2501
+ { key: "availableForAgent", header: "FOR_AGENT" },
2502
+ { key: "needsConnection", header: "NEEDS_CONN" },
2503
+ { key: "actions", header: "ACTIONS" }
2504
+ ]);
2505
+ }
2506
+ async function runAppsGet(client, args) {
2507
+ const appId = requireTarget(args, "app id (example: apps get http)");
2508
+ const apps = await client.workflowApps.list();
2509
+ const app = apps.find((item) => item.id === appId);
2510
+ if (!app) {
2511
+ throw new Error(`Unknown app "${appId}". Try: apps list`);
2512
+ }
2513
+ const actions = flattenActions([app]).map(summarizeAction);
2514
+ const payload = {
2515
+ id: app.id,
2516
+ name: app.name,
2517
+ description: app.description ?? "",
2518
+ availableForAgent: Boolean(app.availableForAgent),
2519
+ needsConnection: Boolean(app.needsConnection),
2520
+ isPublished: Boolean(app.isPublished),
2521
+ actions,
2522
+ connectionTypes: Array.isArray(app.connections) ? app.connections : []
2523
+ };
2524
+ if (args.json) {
2525
+ printJson(payload);
2526
+ return;
2527
+ }
2528
+ console.log(`id: ${payload.id}`);
2529
+ console.log(`name: ${payload.name}`);
2530
+ if (payload.description) {
2531
+ console.log(`description: ${payload.description}`);
2532
+ }
2533
+ console.log(`availableForAgent: ${payload.availableForAgent ? "yes" : "no"}`);
2534
+ console.log(`needsConnection: ${payload.needsConnection ? "yes" : "no"}`);
2535
+ console.log("");
2536
+ printTable(actions, [
2537
+ { key: "actionId", header: "ACTION" },
2538
+ { key: "name", header: "NAME" },
2539
+ { key: "availableForAgent", header: "FOR_AGENT" },
2540
+ { key: "needsConnection", header: "NEEDS_CONN" }
2541
+ ]);
2542
+ }
2543
+ async function runConnectionsList(client, args) {
2544
+ const connections = await client.connections.list({
2545
+ expansion: ["workflowAppId", "connectionId", "createdAt"]
2546
+ });
2547
+ const rows = connections.filter(
2548
+ (connection) => args.app ? connection.workflowAppId === args.app : true
2549
+ ).map((connection) => ({
2550
+ id: connection.id,
2551
+ name: connection.name,
2552
+ workflowAppId: connection.workflowAppId ?? "",
2553
+ connectionId: connection.connectionId ?? ""
2554
+ }));
2555
+ if (args.json) {
2556
+ printJson(rows);
2557
+ return;
2558
+ }
2559
+ printTable(rows, [
2560
+ { key: "id", header: "ID" },
2561
+ { key: "name", header: "NAME" },
2562
+ { key: "workflowAppId", header: "APP" },
2563
+ { key: "connectionId", header: "TYPE" }
2564
+ ]);
2565
+ }
2566
+ async function runConnectionsGet(client, args) {
2567
+ const id = requireTarget(args, "connection id (example: connections get <uuid>)");
2568
+ const connection = await client.connections.get(id, {
2569
+ expansion: ["workflowAppId", "connectionId", "createdAt", "updatedAt"]
2570
+ });
2571
+ const payload = {
2572
+ id: connection.id,
2573
+ name: connection.name,
2574
+ workflowAppId: connection.workflowAppId ?? null,
2575
+ connectionId: connection.connectionId ?? null,
2576
+ createdAt: connection.createdAt ?? null,
2577
+ updatedAt: connection.updatedAt ?? null
2578
+ };
2579
+ if (args.json) {
2580
+ printJson(payload);
2581
+ return;
2582
+ }
2583
+ console.log(`id: ${payload.id}`);
2584
+ console.log(`name: ${payload.name}`);
2585
+ console.log(`workflowAppId: ${payload.workflowAppId ?? ""}`);
2586
+ console.log(`connectionId: ${payload.connectionId ?? ""}`);
2587
+ }
2588
+ async function runAgentsList(client, args) {
2589
+ const agents = await client.agents.list({
2590
+ expansion: ["description", "slug", "managedByCode"]
2591
+ });
2592
+ const rows = agents.map((agent) => ({
2593
+ id: agent.id,
2594
+ name: agent.name,
2595
+ slug: agent.slug ?? "",
2596
+ managedByCode: Boolean(agent.managedByCode),
2597
+ description: agent.description ?? ""
2598
+ }));
2599
+ if (args.json) {
2600
+ printJson(rows);
2601
+ return;
2602
+ }
2603
+ printTable(rows, [
2604
+ { key: "id", header: "ID" },
2605
+ { key: "slug", header: "SLUG" },
2606
+ { key: "name", header: "NAME" },
2607
+ { key: "managedByCode", header: "CODE" }
2608
+ ]);
2609
+ }
2610
+ async function runWorkflowsList(client, args) {
2611
+ const workflows = await client.workflows.list({
2612
+ expansion: ["description", "isActive", "slug", "managedByCode"]
2613
+ });
2614
+ const rows = workflows.map((workflow) => ({
2615
+ id: workflow.id,
2616
+ name: workflow.name,
2617
+ slug: workflow.slug ?? "",
2618
+ isActive: Boolean(workflow.isActive),
2619
+ managedByCode: Boolean(workflow.managedByCode)
2620
+ }));
2621
+ if (args.json) {
2622
+ printJson(rows);
2623
+ return;
2624
+ }
2625
+ printTable(rows, [
2626
+ { key: "id", header: "ID" },
2627
+ { key: "slug", header: "SLUG" },
2628
+ { key: "name", header: "NAME" },
2629
+ { key: "isActive", header: "ACTIVE" },
2630
+ { key: "managedByCode", header: "CODE" }
2631
+ ]);
2632
+ }
2633
+ async function runEntityTypesList(client, args) {
2634
+ const types = await client.entityTypes.list({
2635
+ expansion: ["description"]
2636
+ });
2637
+ const rows = types.map((type) => ({
2638
+ id: type.id,
2639
+ slug: type.slug,
2640
+ name: type.name,
2641
+ managedByCode: Boolean(type.managedByCode)
2642
+ }));
2643
+ if (args.json) {
2644
+ printJson(rows);
2645
+ return;
2646
+ }
2647
+ printTable(rows, [
2648
+ { key: "id", header: "ID" },
2649
+ { key: "slug", header: "SLUG" },
2650
+ { key: "name", header: "NAME" },
2651
+ { key: "managedByCode", header: "CODE" }
2652
+ ]);
2653
+ }
2654
+ async function runProjectsList(client, args) {
2655
+ const projects = await client.projects.list({
2656
+ expansion: ["description", "countAgents", "countWorkflows"]
2657
+ });
2658
+ const rows = projects.map((project) => ({
2659
+ id: project.id,
2660
+ name: project.name,
2661
+ description: project.description ?? "",
2662
+ agents: project._count?.agents ?? "",
2663
+ workflows: project._count?.workflows ?? ""
2664
+ }));
2665
+ if (args.json) {
2666
+ printJson(rows);
2667
+ return;
2668
+ }
2669
+ printTable(rows, [
2670
+ { key: "id", header: "ID" },
2671
+ { key: "name", header: "NAME" },
2672
+ { key: "agents", header: "AGENTS" },
2673
+ { key: "workflows", header: "WORKFLOWS" }
2674
+ ]);
2675
+ }
2676
+ async function runFeatures(client, args) {
2677
+ const features = await client.discovery.enabledFeatures();
2678
+ if (args.json) {
2679
+ printJson(features);
2680
+ return;
2681
+ }
2682
+ const rows = Object.entries(features).filter(([, value]) => typeof value === "boolean").map(([key, value]) => ({
2683
+ feature: key,
2684
+ enabled: value
2685
+ }));
2686
+ printTable(rows, [
2687
+ { key: "feature", header: "FEATURE" },
2688
+ { key: "enabled", header: "ENABLED" }
2689
+ ]);
2690
+ }
2691
+
2052
2692
  // src/cli/generate.ts
2053
2693
  function computeDeploymentSchemaFingerprint(entityTypes) {
2054
2694
  const parts = [...entityTypes].map((type) => {
@@ -2537,70 +3177,6 @@ async function loadOptionalConfig() {
2537
3177
  }
2538
3178
 
2539
3179
  // src/cli/index.ts
2540
- function parseArgs(argv) {
2541
- const args = { command: argv[0], out: "./src/lunnoa" };
2542
- let i = 1;
2543
- if (args.command === "workflows" || args.command === "agents" || args.command === "entity-types") {
2544
- args.subcommand = argv[1];
2545
- i = 2;
2546
- if (argv[2] && !argv[2].startsWith("--")) {
2547
- args.path = argv[2];
2548
- i = 3;
2549
- }
2550
- }
2551
- for (; i < argv.length; i += 1) {
2552
- const flag = argv[i];
2553
- const value = argv[i + 1];
2554
- switch (flag) {
2555
- case "--url":
2556
- args.url = value;
2557
- i += 1;
2558
- break;
2559
- case "--api-key":
2560
- args.apiKey = value;
2561
- i += 1;
2562
- break;
2563
- case "--access-token":
2564
- args.accessToken = value;
2565
- i += 1;
2566
- break;
2567
- case "--project-id":
2568
- args.projectId = value;
2569
- i += 1;
2570
- break;
2571
- case "--out":
2572
- args.out = value;
2573
- i += 1;
2574
- break;
2575
- default:
2576
- if (!args.path && !flag.startsWith("--")) {
2577
- args.path = flag;
2578
- break;
2579
- }
2580
- throw new Error(`Unknown option: ${flag}`);
2581
- }
2582
- }
2583
- return args;
2584
- }
2585
- var USAGE = `Usage:
2586
- npx @lunnoa/client codegen --url <deployment-url> --api-key <lna_key> [--out <dir>]
2587
- npx @lunnoa/client workflows deploy <path> --url <url> --api-key <key> --project-id <uuid>
2588
- npx @lunnoa/client agents deploy <path> --url <url> --api-key <key> --project-id <uuid>
2589
- npx @lunnoa/client entity-types deploy <path> --url <url> --api-key <key>
2590
-
2591
- Options:
2592
- --url Base URL of the Lunnoa Automate deployment
2593
- --api-key Machine API key (lna_...)
2594
- --access-token User JWT as an alternative to --api-key
2595
- --project-id Project UUID (required for workflow/agent deploy)
2596
- --out Output directory for lunnoa.generated.ts (codegen; default: ./src/lunnoa)
2597
-
2598
- Environment: LUNNOA_URL, LUNNOA_API_KEY, LUNNOA_PROJECT_ID
2599
- Optional config file: lunnoa.config.ts | .mjs | .js | .json
2600
-
2601
- Entity-type deploy is workspace-scoped (no --project-id). Unknown instance
2602
- attributes are rejected unless allowUnknownAttributes is true; use extensions.
2603
- `;
2604
3180
  async function bindClient(args) {
2605
3181
  const fileConfig = await loadOptionalConfig();
2606
3182
  const url = args.url ?? process.env.LUNNOA_URL ?? fileConfig.url;
@@ -2726,9 +3302,106 @@ async function deployEntityType(args) {
2726
3302
  `Upserted entity type ${result.id} (${payload.slug})` + (typeof result.schemaRevision === "number" ? ` schemaRevision=${result.schemaRevision}` : "")
2727
3303
  );
2728
3304
  }
3305
+ var DISCOVERY_SUBCOMMANDS = {
3306
+ tools: ["list", "get", "search"],
3307
+ apps: ["list", "get"],
3308
+ connections: ["list", "get"],
3309
+ features: void 0,
3310
+ projects: ["list"]
3311
+ };
3312
+ function isDiscoveryCommand(args) {
3313
+ const { command, subcommand } = args;
3314
+ if (!command) {
3315
+ return false;
3316
+ }
3317
+ if (command === "features") {
3318
+ return true;
3319
+ }
3320
+ if (command === "projects" && (subcommand === "list" || !subcommand)) {
3321
+ return true;
3322
+ }
3323
+ if ((command === "agents" || command === "workflows" || command === "entity-types") && subcommand === "list") {
3324
+ return true;
3325
+ }
3326
+ const allowed = DISCOVERY_SUBCOMMANDS[command];
3327
+ return Array.isArray(allowed) && Boolean(subcommand && allowed.includes(subcommand));
3328
+ }
3329
+ function assertKnownDiscoveryShape(args) {
3330
+ const { command, subcommand } = args;
3331
+ const allowed = command ? DISCOVERY_SUBCOMMANDS[command] : void 0;
3332
+ if (!command || allowed === void 0) {
3333
+ return;
3334
+ }
3335
+ if (!subcommand || !allowed.includes(subcommand)) {
3336
+ throw new Error(
3337
+ `Unknown command: ${command} ${subcommand ?? ""}
3338
+
3339
+ ${USAGE}`
3340
+ );
3341
+ }
3342
+ }
3343
+ async function runDiscovery(args) {
3344
+ const { command, subcommand } = args;
3345
+ if (command === "tools" || command === "apps" || command === "connections") {
3346
+ assertKnownDiscoveryShape(args);
3347
+ }
3348
+ if (!isDiscoveryCommand(args)) {
3349
+ return false;
3350
+ }
3351
+ const { client } = await bindClient(args);
3352
+ if (command === "tools" && subcommand === "list") {
3353
+ await runToolsList(client, args);
3354
+ return true;
3355
+ }
3356
+ if (command === "tools" && subcommand === "get") {
3357
+ await runToolsGet(client, args);
3358
+ return true;
3359
+ }
3360
+ if (command === "tools" && subcommand === "search") {
3361
+ await runToolsSearch(client, args);
3362
+ return true;
3363
+ }
3364
+ if (command === "apps" && subcommand === "list") {
3365
+ await runAppsList(client, args);
3366
+ return true;
3367
+ }
3368
+ if (command === "apps" && subcommand === "get") {
3369
+ await runAppsGet(client, args);
3370
+ return true;
3371
+ }
3372
+ if (command === "connections" && subcommand === "list") {
3373
+ await runConnectionsList(client, args);
3374
+ return true;
3375
+ }
3376
+ if (command === "connections" && subcommand === "get") {
3377
+ await runConnectionsGet(client, args);
3378
+ return true;
3379
+ }
3380
+ if (command === "agents" && subcommand === "list") {
3381
+ await runAgentsList(client, args);
3382
+ return true;
3383
+ }
3384
+ if (command === "workflows" && subcommand === "list") {
3385
+ await runWorkflowsList(client, args);
3386
+ return true;
3387
+ }
3388
+ if (command === "entity-types" && subcommand === "list") {
3389
+ await runEntityTypesList(client, args);
3390
+ return true;
3391
+ }
3392
+ if (command === "projects" && (subcommand === "list" || !subcommand)) {
3393
+ await runProjectsList(client, args);
3394
+ return true;
3395
+ }
3396
+ if (command === "features") {
3397
+ await runFeatures(client, args);
3398
+ return true;
3399
+ }
3400
+ return false;
3401
+ }
2729
3402
  async function main() {
2730
3403
  const args = parseArgs(process.argv.slice(2));
2731
- if (!args.command || args.command === "--help" || args.command === "help") {
3404
+ if (!args.command || args.command === "--help" || args.command === "help" || args.help) {
2732
3405
  console.log(USAGE);
2733
3406
  return;
2734
3407
  }
@@ -2736,6 +3409,9 @@ async function main() {
2736
3409
  await runCodegen(args);
2737
3410
  return;
2738
3411
  }
3412
+ if (await runDiscovery(args)) {
3413
+ return;
3414
+ }
2739
3415
  if (args.command === "workflows" && args.subcommand === "deploy") {
2740
3416
  await deployWorkflow(args);
2741
3417
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunnoa/client",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "TypeScript SDK for building applications on top of Lunnoa Automate — typed client for the Public API (agents, workflows, executions, entities, knowledge, queues), SSE agent chat streaming, and deployment-specific codegen",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -54,8 +54,12 @@ Run these before inventing resources or shipping UI code:
54
54
  browser (SDK throws when `window` exists).
55
55
  2. **Codegen** — When building typed UIs, run
56
56
  `npx @lunnoa/client codegen` against the deployment first.
57
- 3. **Discover before inventing** — `workflowApps.list()`, `agents.list`,
58
- `workflows.list`, `entityTypes.list` (and `discovery.enabledFeatures()`).
57
+ 3. **Discover before inventing** — prefer the CLI (JSON for agents), then
58
+ the SDK: `npx @lunnoa/client tools list --json --for-agent`,
59
+ `tools get <actionId> --json`, `apps list`, `connections list`,
60
+ `agents|workflows|entity-types|projects list`, `features`.
61
+ SDK equivalents: `workflowApps.list()`, `agents.list`, `workflows.list`,
62
+ `entityTypes.list`, `discovery.enabledFeatures()`.
59
63
  Do not invent catalogue action ids.
60
64
  4. **Run vs deploy** — Ad-hoc: `actions.run` / `runs.start` /
61
65
  `workflows.execute`. Durable shared automation: CLI
@@ -361,6 +365,18 @@ const result = await lunnoa.runs.start(ping);
361
365
  // or lunnoa.runs.startAndWait(ping)
362
366
  ```
363
367
 
368
+ Discover the catalogue **before** writing `defineAction` (do not guess ids):
369
+
370
+ ```bash
371
+ npx @lunnoa/client tools list --json --for-agent
372
+ npx @lunnoa/client tools search email --json --for-agent
373
+ npx @lunnoa/client tools get http_action_send-request --json
374
+ npx @lunnoa/client connections list --app gmail --json
375
+ ```
376
+
377
+ `tools get` returns `inputConfig` / `inputFields` and a `defineAction` sketch.
378
+ `tools list` is compact; use `get` for the schema. `--json` is for coding agents.
379
+
364
380
  Deploy to Automate UI (upsert by slug; prefer CLI):
365
381
 
366
382
  ```bash
@@ -510,7 +526,7 @@ emails (capped; not Azure object IDs).
510
526
  3. Call `discovery.enabledFeatures()` and hide unsupported areas.
511
527
  4. Build forms/tables from `attributeSchema` / `customInputConfig` (or the generated types).
512
528
  5. Render workflow progress from `executionPath`; handle `NEEDS_INPUT` with `submitInput`.
513
- 6. For single-action calls, use `actions.run`; for linear multi-step, use `runs.start` / `defineWorkflow`. Deploy with `npx @lunnoa/client workflows|agents deploy`. Discover catalogue via `workflowApps.list()`.
529
+ 6. For single-action calls, use `actions.run`; for linear multi-step, use `runs.start` / `defineWorkflow`. Deploy with `npx @lunnoa/client workflows|agents deploy`. Discover catalogue via `npx @lunnoa/client tools list|get|search` (or `workflowApps.list()`).
514
530
  7. For Request Approval nodes, use `approvals.inbox` / `approvals.decide` (or `executions.submitApproval`).
515
531
  8. Chat: stream via SSE, handle resume (reconnects) and stop, load history from the task.
516
532
  9. Handle `LunnoaApiError`: 401 → re-auth; 403 → hide the capability; 429 → back off.
@@ -63,8 +63,9 @@ interrogation.
63
63
  ### Integrations
64
64
 
65
65
  - Which external systems? Prefer existing catalogue apps (Slack, HTTP, CRM…).
66
- - Do not invent action ids. Implementation must call `workflowApps.list()`
67
- (or codegen) to see what this API key can use.
66
+ - Do not invent action ids. Implementation must discover the catalogue for
67
+ this API key (`npx @lunnoa/client tools list --json --for-agent`, then
68
+ `tools get <actionId> --json`, or `workflowApps.list()` / codegen).
68
69
 
69
70
  ### Auth for any custom UI
70
71
 
@@ -96,8 +97,9 @@ interrogation.
96
97
  2. **Connections stay on Lunnoa.** Definitions only reference connection ids
97
98
  (or omit for sole/default resolution). Never ask the partner to put OAuth
98
99
  secrets in their app.
99
- 3. **Do not invent catalogue actions.** Discover with `workflowApps.list()` /
100
- codegen. Client `defineAction` binds existing catalogue actions; toolkit
100
+ 3. **Do not invent catalogue actions.** Discover with
101
+ `npx @lunnoa/client tools list|get|search --json` (or `workflowApps.list()`
102
+ / codegen). Client `defineAction` binds existing catalogue actions; toolkit
101
103
  `createAction` is a different product (in-platform apps).
102
104
  4. **Durable vs ephemeral:** experiments → `runs.start`; shared production
103
105
  automation → CLI `workflows deploy` / `agents deploy` by slug.
@@ -131,8 +133,8 @@ can execute. Use this shape:
131
133
  - Notes: <CORS, BFF, project id, env vars>
132
134
 
133
135
  ### Discovery (run first)
134
- - [ ] workflowApps.list() — catalogue actions for tools/steps
135
- - [ ] agents.list / workflows.list / entityTypes.list — reuse before create
136
+ - [ ] `npx @lunnoa/client tools list --json --for-agent` then `tools get <id>`
137
+ - [ ] agents / workflows / entity-types / projects list — reuse before create
136
138
  - [ ] codegen — if building typed entity/workflow accessors
137
139
 
138
140
  ### Resources
@@ -175,7 +177,7 @@ continue in a coding turn with that skill loaded.
175
177
  action)? Need approvals? Existing HTTP/CRM connections?
176
178
 
177
179
  **You conclude (example):** Pattern A BFF; Agent `order-status` with tools
178
- `defineAction` for ERP lookup (after `workflowApps.list`) + optional
180
+ `defineAction` for ERP lookup (after `tools list` / `tools get`) + optional
179
181
  workflow for staff notifications; UI = chat only; no agent schedule.
180
182
 
181
183
  **You produce** the build brief, then hand off.