@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.
- package/README.md +26 -17
- package/dist/index.js +307 -60
- 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`,
|
|
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.
|
|
79
|
+
request. Dynamic action calls and the generic `topolo api` passthrough enforce
|
|
80
|
+
this too:
|
|
80
81
|
|
|
81
82
|
```bash
|
|
82
|
-
topolo
|
|
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
|
|
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 =
|
|
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
|
|
2225
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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/
|
|
3202
|
-
function
|
|
3203
|
-
const
|
|
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
|
|
3213
|
-
|
|
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
|
-
|
|
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
|
|
3231
|
-
|
|
3232
|
-
|
|
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
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
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
|
|
3248
|
-
if (rows.length === 0) return "No
|
|
3249
|
-
const
|
|
3250
|
-
|
|
3251
|
-
|
|
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
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
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
|
|
3263
|
-
const name = [c.firstName, c.lastName].filter(Boolean).join(" ") || "(no name)";
|
|
3508
|
+
function formatActionDetail(action) {
|
|
3264
3509
|
return [
|
|
3265
|
-
`
|
|
3266
|
-
`
|
|
3267
|
-
`
|
|
3268
|
-
`
|
|
3269
|
-
`
|
|
3270
|
-
`
|
|
3271
|
-
`
|
|
3272
|
-
`
|
|
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
|
|
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,
|
|
8641
|
-
return
|
|
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
|
-
|
|
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.
|
|
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": {
|