@topolo/cli 0.1.26 → 0.1.28

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 +307 -60
  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
@@ -2046,15 +2046,88 @@ function statusRank(status) {
2046
2046
  return 2;
2047
2047
  }
2048
2048
  var DEFAULT_SERVICE_URLS = SERVICES;
2049
+ function resolveCatalogServiceUrl(entry, options = {}) {
2050
+ const keys = catalogServiceKeyCandidates(entry, options.serviceKey);
2051
+ const override = resolveServiceUrlOverride(keys, options.overrides);
2052
+ if (override) return override;
2053
+ const apiBaseUrl = firstString(entry.apiBaseUrl, entry.api_base_url);
2054
+ if (apiBaseUrl) return apiBaseUrl;
2055
+ const bundledUrl = firstBundledServiceUrl(keys);
2056
+ const liveUrl = firstString(entry.baseUrl);
2057
+ if (bundledUrl && (!liveUrl || sameUrlEnvironment(bundledUrl, liveUrl))) return bundledUrl;
2058
+ return liveUrl ?? bundledUrl ?? null;
2059
+ }
2049
2060
  function resolveServiceUrl(service, overrides) {
2050
- const override = overrides?.[service];
2061
+ const override = resolveServiceUrlOverride([service], overrides);
2051
2062
  if (override) return override;
2052
- const envKey = `TOPOLO_SERVICE_URL_${service.replace(/\./g, "_").toUpperCase()}`;
2053
- const envValue = typeof process !== "undefined" ? process.env?.[envKey] : void 0;
2054
- if (envValue) return envValue;
2055
2063
  return DEFAULT_SERVICE_URLS[service];
2056
2064
  }
2065
+ function catalogServiceKeyCandidates(entry, preferred) {
2066
+ const values = [preferred, entry.slug, entry.name, entry.serviceId].filter(
2067
+ (value) => typeof value === "string" && value.trim().length > 0
2068
+ );
2069
+ const keys = /* @__PURE__ */ new Set();
2070
+ for (const value of values) {
2071
+ for (const candidate of serviceKeyCandidates(value)) keys.add(candidate);
2072
+ }
2073
+ return [...keys];
2074
+ }
2075
+ function serviceKeyCandidates(value) {
2076
+ const normalized = value.trim().toLowerCase().replace(/\s+/g, "-");
2077
+ const withoutTopolo = normalized.startsWith("topolo-") ? normalized.slice("topolo-".length) : normalized;
2078
+ return [.../* @__PURE__ */ new Set([
2079
+ normalized,
2080
+ normalized.replace(/-/g, "_"),
2081
+ withoutTopolo,
2082
+ withoutTopolo.replace(/-/g, "_")
2083
+ ])].filter(Boolean);
2084
+ }
2085
+ function resolveServiceUrlOverride(keys, overrides) {
2086
+ for (const key of keys) {
2087
+ const direct = overrides?.[key];
2088
+ if (direct) return direct;
2089
+ const envStyleKey = key.replace(/\./g, "_");
2090
+ const envStyle = overrides?.[envStyleKey];
2091
+ if (envStyle) return envStyle;
2092
+ }
2093
+ for (const key of keys) {
2094
+ const envKey = `TOPOLO_SERVICE_URL_${key.replace(/\./g, "_").toUpperCase()}`;
2095
+ const envValue = typeof process !== "undefined" ? process.env?.[envKey] : void 0;
2096
+ if (envValue) return envValue;
2097
+ }
2098
+ return null;
2099
+ }
2100
+ function firstBundledServiceUrl(keys) {
2101
+ for (const key of keys) {
2102
+ const url = DEFAULT_SERVICE_URLS[key];
2103
+ if (url) return url;
2104
+ }
2105
+ return null;
2106
+ }
2107
+ function firstString(...values) {
2108
+ for (const value of values) {
2109
+ if (typeof value === "string" && value.trim()) return value.trim();
2110
+ }
2111
+ return null;
2112
+ }
2113
+ function sameUrlEnvironment(left, right) {
2114
+ return urlEnvironment(left) === urlEnvironment(right);
2115
+ }
2116
+ function urlEnvironment(value) {
2117
+ try {
2118
+ const url = new URL(value);
2119
+ const host = url.hostname.toLowerCase();
2120
+ if (host === "localhost" || host === "127.0.0.1") return "local";
2121
+ if (host.endsWith(".stg.topolo.us") || host.includes("topolo-staging")) return "staging";
2122
+ if (host.endsWith(".topolo.app") || host.endsWith(".topolo.us")) return "production";
2123
+ return "external";
2124
+ } catch {
2125
+ return "external";
2126
+ }
2127
+ }
2057
2128
  var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
2129
+ var TOPOLO_ACTION_ID_HEADER = "X-Topolo-Action-Id";
2130
+ var TOPOLO_ACTION_NAME_HEADER = "X-Topolo-Action-Name";
2058
2131
  var POSITIVE_CACHE_TTL_MS = 5 * 60 * 1e3;
2059
2132
  var NEGATIVE_CACHE_TTL_MS = 60 * 1e3;
2060
2133
  var CATALOG_CACHE_TTL_MS = 60 * 1e3;
@@ -2068,6 +2141,7 @@ var TopoloClient = class _TopoloClient {
2068
2141
  fetchImpl;
2069
2142
  debug;
2070
2143
  serviceCatalogCache = null;
2144
+ actionCatalogCache = null;
2071
2145
  /**
2072
2146
  * Process-wide cache for slug → service-id lookups. Keyed by
2073
2147
  * `${authBaseUrl}|${slug}` so multiple `TopoloClient` instances pointing at
@@ -2221,8 +2295,12 @@ var TopoloClient = class _TopoloClient {
2221
2295
  async resolveLiveCallableService(serviceKey) {
2222
2296
  const catalog = await this.listServices();
2223
2297
  const match = catalog.find((entry) => catalogEntryMatches(entry, serviceKey));
2224
- if (!match?.baseUrl || !isCallableCatalogEntry(match)) return null;
2225
- return { serviceKey, baseUrl: match.baseUrl };
2298
+ if (!match || !isCallableCatalogEntry(match)) return null;
2299
+ const baseUrl = resolveCatalogServiceUrl(match, {
2300
+ serviceKey,
2301
+ overrides: this.serviceUrls
2302
+ });
2303
+ return baseUrl ? { serviceKey, baseUrl } : null;
2226
2304
  }
2227
2305
  /**
2228
2306
  * Resolve a service-slug → opaque service-id via caller config or TopoloAuth.
@@ -2316,6 +2394,8 @@ var TopoloClient = class _TopoloClient {
2316
2394
  name: typeof s.name === "string" ? s.name : null,
2317
2395
  description: typeof s.description === "string" ? s.description : null,
2318
2396
  baseUrl: typeof s.base_url === "string" ? s.base_url : null,
2397
+ apiBaseUrl: typeof s.api_base_url === "string" ? s.api_base_url : typeof s.apiBaseUrl === "string" ? s.apiBaseUrl : null,
2398
+ launchUrl: typeof s.launch_url === "string" ? s.launch_url : typeof s.launchUrl === "string" ? s.launchUrl : null,
2319
2399
  status: typeof s.status === "string" ? s.status : null,
2320
2400
  surfaceKind: typeof s.surface_kind === "string" ? s.surface_kind : null,
2321
2401
  launchable: s.launchable === true,
@@ -2328,6 +2408,67 @@ var TopoloClient = class _TopoloClient {
2328
2408
  };
2329
2409
  return entries;
2330
2410
  }
2411
+ /**
2412
+ * List callable actions from the live Auth catalog. The response is scoped to
2413
+ * the caller's organization and effective permissions; no bundled/static app
2414
+ * action list is used as an authorization source.
2415
+ */
2416
+ async listActions(options = {}) {
2417
+ const now = Date.now();
2418
+ if (!options.service && this.actionCatalogCache && this.actionCatalogCache.expiresAt > now) {
2419
+ return this.actionCatalogCache.entries;
2420
+ }
2421
+ const res = await this.request({
2422
+ service: "auth",
2423
+ method: "GET",
2424
+ path: "/api/actions/catalog",
2425
+ ...options.service ? { query: { service: options.service } } : {}
2426
+ });
2427
+ const raw = res.data?.actions ?? res.actions;
2428
+ if (!Array.isArray(raw)) {
2429
+ throw new TopoloSdkError(
2430
+ "action_catalog_unavailable",
2431
+ "TopoloAuth did not return an action catalog for the current credential."
2432
+ );
2433
+ }
2434
+ const entries = raw.map((item) => normalizeActionCatalogEntry(item)).filter((entry) => Boolean(entry));
2435
+ if (!options.service) {
2436
+ this.actionCatalogCache = {
2437
+ entries,
2438
+ expiresAt: now + CATALOG_CACHE_TTL_MS
2439
+ };
2440
+ }
2441
+ return entries;
2442
+ }
2443
+ async getAction(action) {
2444
+ const needle = normalizeActionLookupKey(action);
2445
+ const actions = await this.listActions();
2446
+ const match = actions.find((entry) => actionEntryMatches(entry, needle));
2447
+ if (!match) {
2448
+ throw new TopoloSdkError(
2449
+ "unknown_action",
2450
+ `Unknown Topolo action "${action}". Run action discovery first and pass an action id, name, or tool name.`
2451
+ );
2452
+ }
2453
+ return match;
2454
+ }
2455
+ async callAction(action, input2 = {}, options = {}) {
2456
+ const entry = await this.getAction(action);
2457
+ const mapped = mapActionInput(entry, input2);
2458
+ const service = entry.serviceSlug || entry.serviceId;
2459
+ return this.request({
2460
+ service,
2461
+ method: entry.method,
2462
+ path: mapped.path,
2463
+ ...mapped.query ? { query: mapped.query } : {},
2464
+ ...mapped.body !== void 0 ? { body: mapped.body } : {},
2465
+ headers: {
2466
+ [TOPOLO_ACTION_ID_HEADER]: entry.actionId,
2467
+ [TOPOLO_ACTION_NAME_HEADER]: entry.name
2468
+ },
2469
+ confirm: options.confirm === true
2470
+ });
2471
+ }
2331
2472
  /**
2332
2473
  * Bundled generated registry for tooling that audits platform packaging, not
2333
2474
  * for deciding what the current credential may use.
@@ -2340,6 +2481,8 @@ var TopoloClient = class _TopoloClient {
2340
2481
  name: app.name ?? app.id,
2341
2482
  description: null,
2342
2483
  baseUrl: app.productionUrl ?? null,
2484
+ apiBaseUrl: null,
2485
+ launchUrl: app.productionUrl ?? null,
2343
2486
  status: "bundled",
2344
2487
  surfaceKind: "application",
2345
2488
  launchable: Boolean(app.productionUrl),
@@ -2383,7 +2526,7 @@ var TopoloClient = class _TopoloClient {
2383
2526
  }
2384
2527
  };
2385
2528
  function resolveConfiguredServiceId(slug, serviceIds) {
2386
- for (const serviceKey of serviceKeyCandidates(slug)) {
2529
+ for (const serviceKey of serviceKeyCandidates2(slug)) {
2387
2530
  const configured = serviceIds?.[serviceKey];
2388
2531
  if (typeof configured === "string" && configured.trim()) return configured.trim();
2389
2532
  const envKey = `TOPOLO_SERVICE_ID_${serviceKey.replace(/\./g, "_").toUpperCase()}`;
@@ -2393,7 +2536,7 @@ function resolveConfiguredServiceId(slug, serviceIds) {
2393
2536
  return null;
2394
2537
  }
2395
2538
  function resolveConfiguredServiceUrl(slug, serviceUrls) {
2396
- for (const serviceKey of serviceKeyCandidates(slug)) {
2539
+ for (const serviceKey of serviceKeyCandidates2(slug)) {
2397
2540
  const configured = serviceUrls?.[serviceKey];
2398
2541
  if (typeof configured === "string" && configured.trim()) return configured.trim();
2399
2542
  const envKey = `TOPOLO_SERVICE_URL_${serviceKey.replace(/\./g, "_").toUpperCase()}`;
@@ -2402,7 +2545,7 @@ function resolveConfiguredServiceUrl(slug, serviceUrls) {
2402
2545
  }
2403
2546
  return null;
2404
2547
  }
2405
- function serviceKeyCandidates(slug) {
2548
+ function serviceKeyCandidates2(slug) {
2406
2549
  const normalized = normalizeLookupKey(slug);
2407
2550
  const withoutTopolo = normalized.startsWith("topolo-") ? normalized.slice("topolo-".length) : normalized;
2408
2551
  return [.../* @__PURE__ */ new Set([
@@ -2424,7 +2567,7 @@ function catalogEntryLookupKeys(entry) {
2424
2567
  for (const value of values) {
2425
2568
  const normalized = normalizeLookupKey(value);
2426
2569
  if (!normalized) continue;
2427
- for (const candidate of serviceKeyCandidates(normalized)) {
2570
+ for (const candidate of serviceKeyCandidates2(normalized)) {
2428
2571
  keys.add(candidate);
2429
2572
  }
2430
2573
  }
@@ -2434,9 +2577,101 @@ function isCallableCatalogEntry(entry) {
2434
2577
  const kind = normalizeLookupKey(entry.surfaceKind || "");
2435
2578
  if (kind === "api" || kind === "technical_service" || kind === "service") return true;
2436
2579
  return Boolean(
2437
- entry.baseUrl && catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key))
2580
+ (entry.baseUrl || entry.apiBaseUrl) && catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key))
2438
2581
  );
2439
2582
  }
2583
+ function normalizeActionCatalogEntry(item) {
2584
+ if (!item || typeof item !== "object" || Array.isArray(item)) return null;
2585
+ const a = item;
2586
+ const actionId = pickActionString(a.action_id) ?? pickActionString(a.actionId);
2587
+ const name = pickActionString(a.name);
2588
+ const serviceId = pickActionString(a.service_id) ?? pickActionString(a.serviceId);
2589
+ const method = pickActionString(a.method)?.toUpperCase();
2590
+ const path4 = pickActionString(a.path);
2591
+ if (!actionId || !name || !serviceId || !method || !path4 || !isHttpMethod(method)) {
2592
+ return null;
2593
+ }
2594
+ return {
2595
+ actionId,
2596
+ name,
2597
+ toolName: pickActionString(a.tool_name) ?? pickActionString(a.toolName) ?? null,
2598
+ title: pickActionString(a.title) ?? name,
2599
+ description: pickActionString(a.description) ?? "",
2600
+ serviceId,
2601
+ serviceSlug: pickActionString(a.service_slug) ?? pickActionString(a.serviceSlug) ?? null,
2602
+ serviceName: pickActionString(a.service_name) ?? pickActionString(a.serviceName) ?? null,
2603
+ method,
2604
+ path: path4,
2605
+ permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
2606
+ requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? "",
2607
+ inputSchema: plainObject(a.input_schema ?? a.inputSchema),
2608
+ outputSchema: plainObject(a.output_schema ?? a.outputSchema),
2609
+ readOnly: Boolean(a.read_only ?? a.readOnly),
2610
+ destructive: Boolean(a.destructive),
2611
+ requiresConfirmation: Boolean(a.requires_confirmation ?? a.requiresConfirmation),
2612
+ status: pickActionString(a.status),
2613
+ sourceRevision: pickActionString(a.source_revision) ?? pickActionString(a.sourceRevision)
2614
+ };
2615
+ }
2616
+ function isHttpMethod(value) {
2617
+ return value === "GET" || value === "POST" || value === "PUT" || value === "PATCH" || value === "DELETE";
2618
+ }
2619
+ function pickActionString(value) {
2620
+ return typeof value === "string" && value.trim() ? value.trim() : null;
2621
+ }
2622
+ function plainObject(value) {
2623
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
2624
+ }
2625
+ function normalizeActionLookupKey(value) {
2626
+ return String(value || "").trim().toLowerCase();
2627
+ }
2628
+ function actionEntryMatches(entry, needle) {
2629
+ if (!needle) return false;
2630
+ return [entry.actionId, entry.name, entry.toolName].filter((value) => typeof value === "string" && value.length > 0).some((value) => value.toLowerCase() === needle);
2631
+ }
2632
+ function mapActionInput(action, input2) {
2633
+ const remaining = { ...input2 || {} };
2634
+ const path4 = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
2635
+ const value = remaining[key];
2636
+ if (value === void 0 || value === null || value === "") {
2637
+ throw new TopoloSdkError(
2638
+ "invalid_action_input",
2639
+ `Action "${action.name}" requires path parameter "${key}".`
2640
+ );
2641
+ }
2642
+ delete remaining[key];
2643
+ return encodeURIComponent(String(value));
2644
+ });
2645
+ const explicitQuery = plainObject(remaining.query);
2646
+ delete remaining.query;
2647
+ const explicitBody = Object.prototype.hasOwnProperty.call(remaining, "body") ? remaining.body : void 0;
2648
+ delete remaining.body;
2649
+ if (action.method === "GET") {
2650
+ return {
2651
+ path: path4,
2652
+ query: objectToQuery({ ...remaining, ...explicitQuery })
2653
+ };
2654
+ }
2655
+ const query = objectToQuery(explicitQuery);
2656
+ const body = explicitBody !== void 0 ? explicitBody : remaining;
2657
+ return {
2658
+ path: path4,
2659
+ ...query ? { query } : {},
2660
+ body
2661
+ };
2662
+ }
2663
+ function objectToQuery(value) {
2664
+ const query = {};
2665
+ for (const [key, raw] of Object.entries(value)) {
2666
+ if (raw === void 0 || raw === null) continue;
2667
+ if (typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean") {
2668
+ query[key] = raw;
2669
+ } else {
2670
+ query[key] = JSON.stringify(raw);
2671
+ }
2672
+ }
2673
+ return Object.keys(query).length > 0 ? query : void 0;
2674
+ }
2440
2675
  function describeError(body, fallback) {
2441
2676
  if (body && typeof body === "object") {
2442
2677
  const maybe = body;
@@ -3198,78 +3433,90 @@ function formatPretty(info) {
3198
3433
  return lines.join("\n");
3199
3434
  }
3200
3435
 
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) => {
3436
+ // src/commands/actions.ts
3437
+ function registerActionsCommand(program2) {
3438
+ 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
3439
  const mode = resolveOutputMode(opts.json);
3206
3440
  try {
3207
- await assertCrmRoutable();
3208
3441
  const topolo = await buildClient();
3209
3442
  if (!topolo) {
3210
3443
  throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
3211
3444
  }
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));
3445
+ const rows = await topolo.client.listActions({ service: opts.service });
3446
+ emit({ actions: rows }, mode, () => formatActionRows(rows));
3218
3447
  } catch (err) {
3219
3448
  fail(err, mode);
3220
3449
  }
3221
3450
  });
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) => {
3451
+ 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) => {
3223
3452
  const mode = resolveOutputMode(opts.json);
3224
3453
  try {
3225
- await assertCrmRoutable();
3226
3454
  const topolo = await buildClient();
3227
3455
  if (!topolo) {
3228
3456
  throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
3229
3457
  }
3230
- const contact = await topolo.crm.getContact(id);
3231
- if (!contact) throw new Error(`Contact not found: ${id}`);
3232
- emit(contact, mode, () => formatContact(contact));
3458
+ const entry = await topolo.client.getAction(action);
3459
+ emit(entry, mode, () => formatActionDetail(entry));
3460
+ } catch (err) {
3461
+ fail(err, mode);
3462
+ }
3463
+ });
3464
+ 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) => {
3465
+ const mode = resolveOutputMode(opts.json);
3466
+ try {
3467
+ const topolo = await buildClient();
3468
+ if (!topolo) {
3469
+ throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
3470
+ }
3471
+ const input2 = parseJsonObject(opts.data || "{}");
3472
+ const result = await topolo.client.callAction(action, input2, { confirm: opts.confirm === true });
3473
+ emit(result, mode, () => JSON.stringify(result, null, 2));
3233
3474
  } catch (err) {
3234
3475
  fail(err, mode);
3235
3476
  }
3236
3477
  });
3237
3478
  }
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
- );
3479
+ function parseJsonObject(value) {
3480
+ let parsed;
3481
+ try {
3482
+ parsed = JSON.parse(value);
3483
+ } catch {
3484
+ throw new Error("--data must be valid JSON");
3485
+ }
3486
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
3487
+ throw new Error("--data must be a JSON object");
3488
+ }
3489
+ return parsed;
3246
3490
  }
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("");
3491
+ function formatActionRows(rows) {
3492
+ if (rows.length === 0) return "No actions available for this credential.";
3493
+ const serviceWidth = Math.max(7, ...rows.map((row) => (row.serviceSlug || row.serviceId).length));
3494
+ const nameWidth = Math.max(6, ...rows.map((row) => row.name.length));
3495
+ const methodWidth = Math.max(6, ...rows.map((row) => row.method.length));
3496
+ const lines = [
3497
+ `${"SERVICE".padEnd(serviceWidth)} ${"ACTION".padEnd(nameWidth)} ${"METHOD".padEnd(methodWidth)} CONFIRM PERMISSION`,
3498
+ `${"-".repeat(serviceWidth)} ${"-".repeat(nameWidth)} ${"-".repeat(methodWidth)} ------- ----------`
3499
+ ];
3252
3500
  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 ")}`);
3501
+ const confirmation = row.requiresConfirmation ? "yes" : "no";
3502
+ lines.push(
3503
+ `${(row.serviceSlug || row.serviceId).padEnd(serviceWidth)} ${row.name.padEnd(nameWidth)} ${row.method.padEnd(methodWidth)} ${confirmation.padEnd(7)} ${row.requiredPermission}`
3504
+ );
3259
3505
  }
3260
3506
  return lines.join("\n");
3261
3507
  }
3262
- function formatContact(c) {
3263
- const name = [c.firstName, c.lastName].filter(Boolean).join(" ") || "(no name)";
3508
+ function formatActionDetail(action) {
3264
3509
  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 ?? "-"}`
3510
+ `Action: ${action.name}`,
3511
+ `ID: ${action.actionId}`,
3512
+ `Tool: ${action.toolName || "-"}`,
3513
+ `Service: ${action.serviceSlug || action.serviceId}`,
3514
+ `Method: ${action.method}`,
3515
+ `Path: ${action.path}`,
3516
+ `Permission: ${action.requiredPermission}`,
3517
+ `Confirmation: ${action.requiresConfirmation ? "required" : "not required"}`,
3518
+ "",
3519
+ action.description
3273
3520
  ].join("\n");
3274
3521
  }
3275
3522
 
@@ -8592,7 +8839,7 @@ function buildRows(catalog, overrides, environment = selectedEnvironment()) {
8592
8839
  const resolvedOverrides = resolveServiceUrls(overrides, environment);
8593
8840
  return catalog.map((entry) => {
8594
8841
  const id = displayServiceId(entry);
8595
- const defaultUrl = defaultServiceUrl(id, entry.baseUrl);
8842
+ const defaultUrl = defaultServiceUrl(id, entry);
8596
8843
  const configuredUrl = configuredServiceUrl(id, resolvedOverrides);
8597
8844
  const effectiveUrl = configuredUrl || defaultUrl;
8598
8845
  return {
@@ -8637,8 +8884,8 @@ function displayServiceId(entry) {
8637
8884
  }
8638
8885
  return serviceIdCandidates(entry.slug || entry.serviceId)[0] || entry.serviceId;
8639
8886
  }
8640
- function defaultServiceUrl(id, fallback) {
8641
- return fallback ?? DEFAULT_SERVICE_URLS[id] ?? "";
8887
+ function defaultServiceUrl(id, entry) {
8888
+ return resolveCatalogServiceUrl(entry, { serviceKey: id }) ?? DEFAULT_SERVICE_URLS[id] ?? "";
8642
8889
  }
8643
8890
  function configuredServiceUrl(id, overrides) {
8644
8891
  if (!overrides) return null;
@@ -9190,7 +9437,7 @@ program.hook("preAction", (thisCommand) => {
9190
9437
  });
9191
9438
  registerAuthCommands(program);
9192
9439
  registerWhoamiCommand(program);
9193
- registerCrmCommands(program);
9440
+ registerActionsCommand(program);
9194
9441
  registerApiCommand(program);
9195
9442
  registerAppsCommand(program);
9196
9443
  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.28",
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": {