@topolo/cli 0.1.26 → 0.1.27

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.
Files changed (3) hide show
  1. package/README.md +26 -17
  2. package/dist/index.js +208 -46
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -10,7 +10,7 @@ via `stdin`/`stdout`.
10
10
  - Access-token authentication via OAuth device flow when a CLI OAuth client id is configured.
11
11
  - Stored access-token refresh when the config includes a refresh token and OAuth client id.
12
12
  - First-class `production` and `staging` CLI environments with separate stored credentials, OAuth client ids, and service URL overrides.
13
- - Commands: `auth login/logout/status`, `whoami`, `apps`, `apps scaffold`, `oauth clients`, `integrations prompt/check`, `services`, `crm contacts:list`, `crm contacts:get`, and generic `api` passthrough for SDK-registered services.
13
+ - Commands: `auth login/logout/status`, `whoami`, `apps`, `apps scaffold`, `oauth clients`, `integrations prompt/check`, `services`, credential-scoped `actions`, and generic `api` passthrough for SDK-registered services.
14
14
 
15
15
  ## Install
16
16
 
@@ -76,10 +76,12 @@ The platform logs these for traceability.
76
76
  ## Write-action gating
77
77
 
78
78
  The SDK defaults to requiring an explicit `--confirm` flag on any mutating
79
- request. The generic `topolo api` passthrough enforces this too:
79
+ request. Dynamic action calls and the generic `topolo api` passthrough enforce
80
+ this too:
80
81
 
81
82
  ```bash
82
- topolo api crm POST /api/contacts --data '{"email":"a@b.co"}' --confirm
83
+ topolo actions call topolo_mail_messages_send --data '{"mailboxId":"primary","subject":"Hello"}' --confirm
84
+ topolo api mail POST /api/mail/mailboxes/primary/messages --data '{"subject":"Hello"}' --confirm
83
85
  ```
84
86
 
85
87
  ## Commands
@@ -93,19 +95,6 @@ topolo whoami # pretty
93
95
  topolo whoami --json # machine-readable
94
96
  ```
95
97
 
96
- ### `topolo crm contacts:list`
97
-
98
- ```bash
99
- topolo crm contacts:list
100
- topolo crm contacts:list -q "acme" -p 2 -s 50 --json
101
- ```
102
-
103
- ### `topolo crm contacts:get <id>`
104
-
105
- ```bash
106
- topolo crm contacts:get contact_abc123
107
- ```
108
-
109
98
  ### `topolo services`
110
99
 
111
100
  Lists the services the current credential can use in its organization, with the
@@ -123,6 +112,26 @@ topolo services --env staging --json
123
112
  catalog. New services become usable without a CLI release once Auth grants the
124
113
  organization and credential the relevant permissions.
125
114
 
115
+ ### `topolo actions`
116
+
117
+ Lists the callable actions available to the current credential. Applications
118
+ publish actions through Topolo Developers, Auth filters them by the same
119
+ service permissions already used by API keys, launchers, and backend services,
120
+ and the CLI calls them through the shared SDK.
121
+
122
+ ```bash
123
+ topolo actions --json
124
+ topolo actions --service mail --json
125
+ topolo actions get topolo_mail_messages_send --json
126
+ topolo actions call topolo_mail_messages_send \
127
+ --data '{"mailboxId":"primary","subject":"Hello","text":"..."}' \
128
+ --confirm --json
129
+ ```
130
+
131
+ Use `topolo actions` before `topolo api` when an app has published a machine
132
+ contract for the operation. `topolo api` remains the escape hatch for endpoints
133
+ that are not yet in the action catalog.
134
+
126
135
  ### `topolo apps`
127
136
 
128
137
  Lists launchable Topolo applications available to the current credential. This
@@ -206,7 +215,7 @@ Escape hatch for endpoints without typed modules yet.
206
215
 
207
216
  ```bash
208
217
  topolo api auth GET /api/me
209
- topolo api crm POST /api/contacts --data '{"first":"Jane"}' --confirm
218
+ topolo api mail POST /api/mail/mailboxes/primary/messages --data '{"subject":"Hello"}' --confirm
210
219
  ```
211
220
 
212
221
  Use `topolo services --json` to discover service ids permitted for the current
package/dist/index.js CHANGED
@@ -2068,6 +2068,7 @@ var TopoloClient = class _TopoloClient {
2068
2068
  fetchImpl;
2069
2069
  debug;
2070
2070
  serviceCatalogCache = null;
2071
+ actionCatalogCache = null;
2071
2072
  /**
2072
2073
  * Process-wide cache for slug → service-id lookups. Keyed by
2073
2074
  * `${authBaseUrl}|${slug}` so multiple `TopoloClient` instances pointing at
@@ -2328,6 +2329,63 @@ var TopoloClient = class _TopoloClient {
2328
2329
  };
2329
2330
  return entries;
2330
2331
  }
2332
+ /**
2333
+ * List callable actions from the live Auth catalog. The response is scoped to
2334
+ * the caller's organization and effective permissions; no bundled/static app
2335
+ * action list is used as an authorization source.
2336
+ */
2337
+ async listActions(options = {}) {
2338
+ const now = Date.now();
2339
+ if (!options.service && this.actionCatalogCache && this.actionCatalogCache.expiresAt > now) {
2340
+ return this.actionCatalogCache.entries;
2341
+ }
2342
+ const res = await this.request({
2343
+ service: "auth",
2344
+ method: "GET",
2345
+ path: "/api/actions/catalog",
2346
+ ...options.service ? { query: { service: options.service } } : {}
2347
+ });
2348
+ const raw = res.data?.actions ?? res.actions;
2349
+ if (!Array.isArray(raw)) {
2350
+ throw new TopoloSdkError(
2351
+ "action_catalog_unavailable",
2352
+ "TopoloAuth did not return an action catalog for the current credential."
2353
+ );
2354
+ }
2355
+ const entries = raw.map((item) => normalizeActionCatalogEntry(item)).filter((entry) => Boolean(entry));
2356
+ if (!options.service) {
2357
+ this.actionCatalogCache = {
2358
+ entries,
2359
+ expiresAt: now + CATALOG_CACHE_TTL_MS
2360
+ };
2361
+ }
2362
+ return entries;
2363
+ }
2364
+ async getAction(action) {
2365
+ const needle = normalizeActionLookupKey(action);
2366
+ const actions = await this.listActions();
2367
+ const match = actions.find((entry) => actionEntryMatches(entry, needle));
2368
+ if (!match) {
2369
+ throw new TopoloSdkError(
2370
+ "unknown_action",
2371
+ `Unknown Topolo action "${action}". Run action discovery first and pass an action id, name, or tool name.`
2372
+ );
2373
+ }
2374
+ return match;
2375
+ }
2376
+ async callAction(action, input2 = {}, options = {}) {
2377
+ const entry = await this.getAction(action);
2378
+ const mapped = mapActionInput(entry, input2);
2379
+ const service = entry.serviceSlug || entry.serviceId;
2380
+ return this.request({
2381
+ service,
2382
+ method: entry.method,
2383
+ path: mapped.path,
2384
+ ...mapped.query ? { query: mapped.query } : {},
2385
+ ...mapped.body !== void 0 ? { body: mapped.body } : {},
2386
+ confirm: options.confirm === true
2387
+ });
2388
+ }
2331
2389
  /**
2332
2390
  * Bundled generated registry for tooling that audits platform packaging, not
2333
2391
  * for deciding what the current credential may use.
@@ -2437,6 +2495,98 @@ function isCallableCatalogEntry(entry) {
2437
2495
  entry.baseUrl && catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key))
2438
2496
  );
2439
2497
  }
2498
+ function normalizeActionCatalogEntry(item) {
2499
+ if (!item || typeof item !== "object" || Array.isArray(item)) return null;
2500
+ const a = item;
2501
+ const actionId = pickActionString(a.action_id) ?? pickActionString(a.actionId);
2502
+ const name = pickActionString(a.name);
2503
+ const serviceId = pickActionString(a.service_id) ?? pickActionString(a.serviceId);
2504
+ const method = pickActionString(a.method)?.toUpperCase();
2505
+ const path4 = pickActionString(a.path);
2506
+ if (!actionId || !name || !serviceId || !method || !path4 || !isHttpMethod(method)) {
2507
+ return null;
2508
+ }
2509
+ return {
2510
+ actionId,
2511
+ name,
2512
+ toolName: pickActionString(a.tool_name) ?? pickActionString(a.toolName) ?? null,
2513
+ title: pickActionString(a.title) ?? name,
2514
+ description: pickActionString(a.description) ?? "",
2515
+ serviceId,
2516
+ serviceSlug: pickActionString(a.service_slug) ?? pickActionString(a.serviceSlug) ?? null,
2517
+ serviceName: pickActionString(a.service_name) ?? pickActionString(a.serviceName) ?? null,
2518
+ method,
2519
+ path: path4,
2520
+ permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
2521
+ requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? "",
2522
+ inputSchema: plainObject(a.input_schema ?? a.inputSchema),
2523
+ outputSchema: plainObject(a.output_schema ?? a.outputSchema),
2524
+ readOnly: Boolean(a.read_only ?? a.readOnly),
2525
+ destructive: Boolean(a.destructive),
2526
+ requiresConfirmation: Boolean(a.requires_confirmation ?? a.requiresConfirmation),
2527
+ status: pickActionString(a.status),
2528
+ sourceRevision: pickActionString(a.source_revision) ?? pickActionString(a.sourceRevision)
2529
+ };
2530
+ }
2531
+ function isHttpMethod(value) {
2532
+ return value === "GET" || value === "POST" || value === "PUT" || value === "PATCH" || value === "DELETE";
2533
+ }
2534
+ function pickActionString(value) {
2535
+ return typeof value === "string" && value.trim() ? value.trim() : null;
2536
+ }
2537
+ function plainObject(value) {
2538
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
2539
+ }
2540
+ function normalizeActionLookupKey(value) {
2541
+ return String(value || "").trim().toLowerCase();
2542
+ }
2543
+ function actionEntryMatches(entry, needle) {
2544
+ if (!needle) return false;
2545
+ return [entry.actionId, entry.name, entry.toolName].filter((value) => typeof value === "string" && value.length > 0).some((value) => value.toLowerCase() === needle);
2546
+ }
2547
+ function mapActionInput(action, input2) {
2548
+ const remaining = { ...input2 || {} };
2549
+ const path4 = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
2550
+ const value = remaining[key];
2551
+ if (value === void 0 || value === null || value === "") {
2552
+ throw new TopoloSdkError(
2553
+ "invalid_action_input",
2554
+ `Action "${action.name}" requires path parameter "${key}".`
2555
+ );
2556
+ }
2557
+ delete remaining[key];
2558
+ return encodeURIComponent(String(value));
2559
+ });
2560
+ const explicitQuery = plainObject(remaining.query);
2561
+ delete remaining.query;
2562
+ const explicitBody = Object.prototype.hasOwnProperty.call(remaining, "body") ? remaining.body : void 0;
2563
+ delete remaining.body;
2564
+ if (action.method === "GET") {
2565
+ return {
2566
+ path: path4,
2567
+ query: objectToQuery({ ...remaining, ...explicitQuery })
2568
+ };
2569
+ }
2570
+ const query = objectToQuery(explicitQuery);
2571
+ const body = explicitBody !== void 0 ? explicitBody : remaining;
2572
+ return {
2573
+ path: path4,
2574
+ ...query ? { query } : {},
2575
+ body
2576
+ };
2577
+ }
2578
+ function objectToQuery(value) {
2579
+ const query = {};
2580
+ for (const [key, raw] of Object.entries(value)) {
2581
+ if (raw === void 0 || raw === null) continue;
2582
+ if (typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean") {
2583
+ query[key] = raw;
2584
+ } else {
2585
+ query[key] = JSON.stringify(raw);
2586
+ }
2587
+ }
2588
+ return Object.keys(query).length > 0 ? query : void 0;
2589
+ }
2440
2590
  function describeError(body, fallback) {
2441
2591
  if (body && typeof body === "object") {
2442
2592
  const maybe = body;
@@ -3198,78 +3348,90 @@ function formatPretty(info) {
3198
3348
  return lines.join("\n");
3199
3349
  }
3200
3350
 
3201
- // src/commands/crm.ts
3202
- function registerCrmCommands(program2) {
3203
- const crm = program2.command("crm").description("TopoloCRM operations");
3204
- crm.command("contacts:list").description("List contacts in the authenticated organization").option("-q, --query <text>", "Full-text search").option("-p, --page <n>", "Page number (1-indexed)", (v) => parseInt(v, 10), 1).option("-s, --page-size <n>", "Page size", (v) => parseInt(v, 10), 25).option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (opts) => {
3351
+ // src/commands/actions.ts
3352
+ function registerActionsCommand(program2) {
3353
+ const actions = program2.command("actions").description("List and call permission-scoped Topolo actions from the live catalog").option("-s, --service <service>", "Filter by service id, slug, or name").option("--json", "Output JSON", true).option("--no-json", "Output human-readable table").action(async (opts) => {
3205
3354
  const mode = resolveOutputMode(opts.json);
3206
3355
  try {
3207
- await assertCrmRoutable();
3208
3356
  const topolo = await buildClient();
3209
3357
  if (!topolo) {
3210
3358
  throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
3211
3359
  }
3212
- const result = await topolo.crm.listContacts({
3213
- q: opts.query,
3214
- page: opts.page,
3215
- pageSize: opts.pageSize
3216
- });
3217
- emit(result, mode, () => formatContactsTable(result.contacts, result.total, result.page));
3360
+ const rows = await topolo.client.listActions({ service: opts.service });
3361
+ emit({ actions: rows }, mode, () => formatActionRows(rows));
3362
+ } catch (err) {
3363
+ fail(err, mode);
3364
+ }
3365
+ });
3366
+ actions.command("get <action>").description("Describe one Topolo action by action id, name, or tool name").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (action, opts) => {
3367
+ const mode = resolveOutputMode(opts.json);
3368
+ try {
3369
+ const topolo = await buildClient();
3370
+ if (!topolo) {
3371
+ throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
3372
+ }
3373
+ const entry = await topolo.client.getAction(action);
3374
+ emit(entry, mode, () => formatActionDetail(entry));
3218
3375
  } catch (err) {
3219
3376
  fail(err, mode);
3220
3377
  }
3221
3378
  });
3222
- crm.command("contacts:get <id>").description("Fetch a single contact by id").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (id, opts) => {
3379
+ actions.command("call <action>").description("Call a permission-scoped Topolo action").option("-d, --data <json>", "JSON action input", "{}").option("--confirm", "Acknowledge a mutating action").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (action, opts) => {
3223
3380
  const mode = resolveOutputMode(opts.json);
3224
3381
  try {
3225
- await assertCrmRoutable();
3226
3382
  const topolo = await buildClient();
3227
3383
  if (!topolo) {
3228
3384
  throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
3229
3385
  }
3230
- const contact = await topolo.crm.getContact(id);
3231
- if (!contact) throw new Error(`Contact not found: ${id}`);
3232
- emit(contact, mode, () => formatContact(contact));
3386
+ const input2 = parseJsonObject(opts.data || "{}");
3387
+ const result = await topolo.client.callAction(action, input2, { confirm: opts.confirm === true });
3388
+ emit(result, mode, () => JSON.stringify(result, null, 2));
3233
3389
  } catch (err) {
3234
3390
  fail(err, mode);
3235
3391
  }
3236
3392
  });
3237
3393
  }
3238
- async function assertCrmRoutable() {
3239
- const environment = selectedEnvironment();
3240
- const profile = resolvedProfileConfig(await readConfig(), environment);
3241
- assertServiceUrlForEnvironment(
3242
- "crm",
3243
- resolveServiceUrl("crm", profile.serviceUrls),
3244
- environment
3245
- );
3394
+ function parseJsonObject(value) {
3395
+ let parsed;
3396
+ try {
3397
+ parsed = JSON.parse(value);
3398
+ } catch {
3399
+ throw new Error("--data must be valid JSON");
3400
+ }
3401
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
3402
+ throw new Error("--data must be a JSON object");
3403
+ }
3404
+ return parsed;
3246
3405
  }
3247
- function formatContactsTable(rows, total, page) {
3248
- if (rows.length === 0) return "No contacts found.";
3249
- const lines = [];
3250
- lines.push(`Showing ${rows.length} of ${total} contacts (page ${page})`);
3251
- lines.push("");
3406
+ function formatActionRows(rows) {
3407
+ if (rows.length === 0) return "No actions available for this credential.";
3408
+ const serviceWidth = Math.max(7, ...rows.map((row) => (row.serviceSlug || row.serviceId).length));
3409
+ const nameWidth = Math.max(6, ...rows.map((row) => row.name.length));
3410
+ const methodWidth = Math.max(6, ...rows.map((row) => row.method.length));
3411
+ const lines = [
3412
+ `${"SERVICE".padEnd(serviceWidth)} ${"ACTION".padEnd(nameWidth)} ${"METHOD".padEnd(methodWidth)} CONFIRM PERMISSION`,
3413
+ `${"-".repeat(serviceWidth)} ${"-".repeat(nameWidth)} ${"-".repeat(methodWidth)} ------- ----------`
3414
+ ];
3252
3415
  for (const row of rows) {
3253
- const name = [row.firstName, row.lastName].filter(Boolean).join(" ") || "(no name)";
3254
- const parts = [name];
3255
- if (row.email) parts.push(row.email);
3256
- if (row.company) parts.push(`@ ${row.company}`);
3257
- if (row.leadStatus) parts.push(`[${row.leadStatus}]`);
3258
- lines.push(` ${row.id} ${parts.join(" \xB7 ")}`);
3416
+ const confirmation = row.requiresConfirmation ? "yes" : "no";
3417
+ lines.push(
3418
+ `${(row.serviceSlug || row.serviceId).padEnd(serviceWidth)} ${row.name.padEnd(nameWidth)} ${row.method.padEnd(methodWidth)} ${confirmation.padEnd(7)} ${row.requiredPermission}`
3419
+ );
3259
3420
  }
3260
3421
  return lines.join("\n");
3261
3422
  }
3262
- function formatContact(c) {
3263
- const name = [c.firstName, c.lastName].filter(Boolean).join(" ") || "(no name)";
3423
+ function formatActionDetail(action) {
3264
3424
  return [
3265
- `ID: ${c.id}`,
3266
- `Name: ${name}`,
3267
- `Email: ${c.email ?? "-"}`,
3268
- `Phone: ${c.phone ?? "-"}`,
3269
- `Company: ${c.company ?? "-"}`,
3270
- `Lead status: ${c.leadStatus ?? "-"}`,
3271
- `Lifecycle: ${c.lifecycleStage ?? "-"}`,
3272
- `Updated at: ${c.updatedAt ?? "-"}`
3425
+ `Action: ${action.name}`,
3426
+ `ID: ${action.actionId}`,
3427
+ `Tool: ${action.toolName || "-"}`,
3428
+ `Service: ${action.serviceSlug || action.serviceId}`,
3429
+ `Method: ${action.method}`,
3430
+ `Path: ${action.path}`,
3431
+ `Permission: ${action.requiredPermission}`,
3432
+ `Confirmation: ${action.requiresConfirmation ? "required" : "not required"}`,
3433
+ "",
3434
+ action.description
3273
3435
  ].join("\n");
3274
3436
  }
3275
3437
 
@@ -9190,7 +9352,7 @@ program.hook("preAction", (thisCommand) => {
9190
9352
  });
9191
9353
  registerAuthCommands(program);
9192
9354
  registerWhoamiCommand(program);
9193
- registerCrmCommands(program);
9355
+ registerActionsCommand(program);
9194
9356
  registerApiCommand(program);
9195
9357
  registerAppsCommand(program);
9196
9358
  registerOAuthCommands(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topolo/cli",
3
- "version": "0.1.26",
3
+ "version": "0.1.27",
4
4
  "description": "Command-line interface for Topolo developers, operators, and agents, with internal first-party scaffolding available under the topolo platform namespace.",
5
5
  "type": "module",
6
6
  "bin": {