@topolo/cli 0.1.34 → 0.2.1

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/dist/index.js CHANGED
@@ -50,6 +50,12 @@ var TopoloPermissionError = class extends TopoloSdkError {
50
50
  this.required = required;
51
51
  }
52
52
  };
53
+ var TopoloConfirmationRequiredError = class extends TopoloSdkError {
54
+ constructor(message) {
55
+ super("confirmation_required", message);
56
+ this.name = "TopoloConfirmationRequiredError";
57
+ }
58
+ };
53
59
  var TopoloHttpError = class extends TopoloSdkError {
54
60
  status;
55
61
  body;
@@ -1112,6 +1118,7 @@ var APPLICATIONS = {
1112
1118
  "topolo.io"
1113
1119
  ],
1114
1120
  "routes": [
1121
+ "one.topolo.io",
1115
1122
  "topolo.io",
1116
1123
  "www.topolo.io"
1117
1124
  ],
@@ -2065,7 +2072,6 @@ var TOPOLO_RESOURCE_TYPE_HEADER = "X-Topolo-Resource-Type";
2065
2072
  var TOPOLO_RESOURCE_ID_HEADER = "X-Topolo-Resource-ID";
2066
2073
  var POSITIVE_CACHE_TTL_MS = 5 * 60 * 1e3;
2067
2074
  var NEGATIVE_CACHE_TTL_MS = 60 * 1e3;
2068
- var CATALOG_CACHE_TTL_MS = 60 * 1e3;
2069
2075
  var TopoloClient = class _TopoloClient {
2070
2076
  credential;
2071
2077
  agent;
@@ -2074,9 +2080,9 @@ var TopoloClient = class _TopoloClient {
2074
2080
  requireConfirmForWrites;
2075
2081
  timeoutMs;
2076
2082
  fetchImpl;
2083
+ refreshCredential;
2084
+ refreshInFlight = null;
2077
2085
  debug;
2078
- serviceCatalogCache = null;
2079
- actionCatalogCache = null;
2080
2086
  /**
2081
2087
  * Process-wide cache for slug → app-id lookups. Keyed by
2082
2088
  * `${authBaseUrl}|${slug}` so multiple `TopoloClient` instances pointing at
@@ -2101,6 +2107,7 @@ var TopoloClient = class _TopoloClient {
2101
2107
  this.requireConfirmForWrites = options.requireConfirmForWrites !== false;
2102
2108
  this.timeoutMs = options.timeoutMs ?? 3e4;
2103
2109
  this.fetchImpl = options.fetch ?? fetch;
2110
+ this.refreshCredential = options.refreshCredential;
2104
2111
  this.debug = options.debug;
2105
2112
  }
2106
2113
  emit(event) {
@@ -2118,11 +2125,11 @@ var TopoloClient = class _TopoloClient {
2118
2125
  async request(opts) {
2119
2126
  const method = opts.method ?? "GET";
2120
2127
  if (this.requireConfirmForWrites && WRITE_METHODS.has(method) && !opts.confirm) {
2121
- throw new TopoloAuthError(
2128
+ throw new TopoloConfirmationRequiredError(
2122
2129
  `Mutating ${method} requests require { confirm: true }. This guardrail prevents agents from issuing writes without an explicit human-in-the-loop acknowledgement.`
2123
2130
  );
2124
2131
  }
2125
- const resolvedService = await this.resolveRequestService(opts.service);
2132
+ const resolvedService = opts.baseUrl ? { serviceKey: normalizeLookupKey(opts.service), baseUrl: requireHttpOrigin(opts.baseUrl) } : await this.resolveRequestService(opts.service);
2126
2133
  const baseUrl = resolvedService.baseUrl;
2127
2134
  const url = new URL(opts.path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
2128
2135
  if (opts.query) {
@@ -2153,20 +2160,36 @@ var TopoloClient = class _TopoloClient {
2153
2160
  headers.set(TOPOLO_RESOURCE_TYPE_HEADER, resourceType);
2154
2161
  headers.set(TOPOLO_RESOURCE_ID_HEADER, resourceId);
2155
2162
  }
2156
- const controller = new AbortController();
2157
- const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
2158
- const signal = opts.signal ? mergeSignals(opts.signal, controller.signal) : controller.signal;
2159
2163
  const urlStr = url.toString();
2160
2164
  const startedAt = Date.now();
2161
2165
  this.emit({ phase: "request", method, service: resolvedService.serviceKey, path: opts.path, url: urlStr, requestId });
2166
+ const fetchOnce = async () => {
2167
+ const controller = new AbortController();
2168
+ const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
2169
+ const signal = opts.signal ? mergeSignals(opts.signal, controller.signal) : controller.signal;
2170
+ try {
2171
+ return await this.fetchImpl(urlStr, {
2172
+ method,
2173
+ headers: new Headers(headers),
2174
+ body: opts.body !== void 0 ? JSON.stringify(opts.body) : null,
2175
+ signal
2176
+ });
2177
+ } finally {
2178
+ clearTimeout(timeoutId);
2179
+ }
2180
+ };
2162
2181
  let res;
2163
2182
  try {
2164
- res = await this.fetchImpl(urlStr, {
2165
- method,
2166
- headers,
2167
- body: opts.body !== void 0 ? JSON.stringify(opts.body) : null,
2168
- signal
2169
- });
2183
+ const attemptedCredential = this.credential;
2184
+ res = await fetchOnce();
2185
+ if (res.status === 401 && attemptedCredential.kind === "access_token" && this.refreshCredential) {
2186
+ await res.body?.cancel().catch(() => void 0);
2187
+ const refreshed = await this.refreshRejectedAccessToken(attemptedCredential.accessToken);
2188
+ if (refreshed) {
2189
+ applyAuthHeaders(headers, refreshed);
2190
+ res = await fetchOnce();
2191
+ }
2192
+ }
2170
2193
  } catch (err) {
2171
2194
  this.emit({
2172
2195
  phase: "error",
@@ -2179,8 +2202,6 @@ var TopoloClient = class _TopoloClient {
2179
2202
  error: err instanceof Error ? err.message : String(err)
2180
2203
  });
2181
2204
  throw err;
2182
- } finally {
2183
- clearTimeout(timeoutId);
2184
2205
  }
2185
2206
  const contentType = res.headers.get("Content-Type") ?? "";
2186
2207
  const parsed = contentType.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => null);
@@ -2219,6 +2240,19 @@ var TopoloClient = class _TopoloClient {
2219
2240
  });
2220
2241
  return parsed;
2221
2242
  }
2243
+ async refreshRejectedAccessToken(attemptedAccessToken) {
2244
+ if (this.credential.kind === "access_token" && this.credential.accessToken !== attemptedAccessToken) {
2245
+ return this.credential;
2246
+ }
2247
+ if (!this.refreshCredential) return null;
2248
+ this.refreshInFlight ??= this.refreshCredential().then((credential) => {
2249
+ if (credential) this.credential = credential;
2250
+ return credential;
2251
+ }).catch(() => null).finally(() => {
2252
+ this.refreshInFlight = null;
2253
+ });
2254
+ return this.refreshInFlight;
2255
+ }
2222
2256
  async resolveRequestService(service) {
2223
2257
  const serviceKey = normalizeLookupKey(service);
2224
2258
  if (!serviceKey) {
@@ -2244,9 +2278,8 @@ var TopoloClient = class _TopoloClient {
2244
2278
  );
2245
2279
  }
2246
2280
  async resolveLiveCallableService(serviceKey) {
2247
- const catalog = await this.listServices();
2248
- const match = catalog.find((entry) => catalogEntryMatches(entry, serviceKey));
2249
- if (!match) return null;
2281
+ const match = await this.getService(serviceKey).catch(() => null);
2282
+ if (!match || !catalogEntryMatches(match, serviceKey)) return null;
2250
2283
  const baseUrl = resolveCatalogServiceUrl(match, {
2251
2284
  serviceKey,
2252
2285
  overrides: this.serviceUrls
@@ -2320,12 +2353,13 @@ var TopoloClient = class _TopoloClient {
2320
2353
  * The live Auth catalog is the authority because it is scoped to the caller's
2321
2354
  * organization and effective permissions.
2322
2355
  */
2323
- async listServices() {
2324
- const now = Date.now();
2325
- if (this.serviceCatalogCache && this.serviceCatalogCache.expiresAt > now) {
2326
- return this.serviceCatalogCache.entries;
2327
- }
2328
- const res = await this.request({ service: "developers", method: "GET", path: "/api/services/catalog" });
2356
+ async searchServices(options = {}) {
2357
+ const res = await this.request({
2358
+ service: "developers",
2359
+ method: "GET",
2360
+ path: "/api/services/catalog",
2361
+ query: catalogSearchQuery(options)
2362
+ });
2329
2363
  const raw = res.data?.services ?? res.services;
2330
2364
  if (!Array.isArray(raw)) {
2331
2365
  throw new TopoloSdkError(
@@ -2335,6 +2369,7 @@ var TopoloClient = class _TopoloClient {
2335
2369
  }
2336
2370
  const entries = [];
2337
2371
  for (const item of raw) {
2372
+ if (!isRecord(item)) continue;
2338
2373
  const s = item;
2339
2374
  const appId = typeof s.app_id === "string" ? s.app_id : typeof s.id === "string" ? s.id : null;
2340
2375
  if (!appId) continue;
@@ -2359,11 +2394,30 @@ var TopoloClient = class _TopoloClient {
2359
2394
  apiKeyResources
2360
2395
  });
2361
2396
  }
2362
- this.serviceCatalogCache = {
2363
- entries,
2364
- expiresAt: now + CATALOG_CACHE_TTL_MS
2365
- };
2366
- return entries;
2397
+ const page = normalizeCatalogPage(
2398
+ res.data?.page ?? res.page
2399
+ );
2400
+ return { services: entries, page };
2401
+ }
2402
+ /** @deprecated Prefer searchServices() so callers retain the continuation cursor. */
2403
+ async listServices(options = {}) {
2404
+ return (await this.searchServices(options)).services;
2405
+ }
2406
+ async getService(application) {
2407
+ const needle = application.trim();
2408
+ if (!needle) throw new TopoloSdkError("unknown_service", "application is required");
2409
+ const res = await this.request({
2410
+ service: "developers",
2411
+ method: "GET",
2412
+ path: "/api/services/catalog",
2413
+ query: { application: needle }
2414
+ });
2415
+ const raw = res.data?.application ?? res.application;
2416
+ const entry = normalizeServiceCatalogEntry(raw);
2417
+ if (!entry) {
2418
+ throw new TopoloSdkError("unknown_service", `Unknown Topolo application "${application}".`);
2419
+ }
2420
+ return entry;
2367
2421
  }
2368
2422
  /**
2369
2423
  * List callable actions from the live registry-canonical catalog (definitions
@@ -2371,16 +2425,15 @@ var TopoloClient = class _TopoloClient {
2371
2425
  * caller's organization and effective permissions; no bundled/static app
2372
2426
  * action list is used as an authorization source.
2373
2427
  */
2374
- async listActions(options = {}) {
2375
- const now = Date.now();
2376
- if (!options.service && this.actionCatalogCache && this.actionCatalogCache.expiresAt > now) {
2377
- return this.actionCatalogCache.entries;
2428
+ async searchActions(options) {
2429
+ if (!options.service?.trim()) {
2430
+ throw new TopoloSdkError("service_required", "service is required for bounded action discovery");
2378
2431
  }
2379
2432
  const res = await this.request({
2380
2433
  service: "developers",
2381
2434
  method: "GET",
2382
2435
  path: "/api/actions/catalog",
2383
- ...options.service ? { query: { service: options.service } } : {}
2436
+ query: { service: options.service, ...catalogSearchQuery(options) }
2384
2437
  });
2385
2438
  const raw = res.data?.actions ?? res.actions;
2386
2439
  if (!Array.isArray(raw)) {
@@ -2390,36 +2443,96 @@ var TopoloClient = class _TopoloClient {
2390
2443
  );
2391
2444
  }
2392
2445
  const entries = raw.map((item) => normalizeActionCatalogEntry(item)).filter((entry) => Boolean(entry));
2393
- if (!options.service) {
2394
- this.actionCatalogCache = {
2395
- entries,
2396
- expiresAt: now + CATALOG_CACHE_TTL_MS
2397
- };
2446
+ return {
2447
+ actions: entries,
2448
+ page: normalizeCatalogPage(
2449
+ res.data?.page ?? res.page
2450
+ )
2451
+ };
2452
+ }
2453
+ async getActionCatalogSummary(service) {
2454
+ const boundedService = service.trim();
2455
+ if (!boundedService) {
2456
+ throw new TopoloSdkError("service_required", "service is required for bounded action discovery");
2398
2457
  }
2399
- return entries;
2458
+ const res = await this.request({
2459
+ service: "developers",
2460
+ method: "GET",
2461
+ path: "/api/actions/catalog",
2462
+ query: { service: boundedService, summary: "true" }
2463
+ });
2464
+ const raw = res.data?.summary ?? res.summary;
2465
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
2466
+ throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is unavailable.");
2467
+ }
2468
+ const record = raw;
2469
+ if (record.algorithm !== "sha256-v1" || typeof record.digest !== "string" || !/^[a-f0-9]{64}$/.test(record.digest) || !Number.isSafeInteger(record.actionCount) || Number(record.actionCount) < 0) {
2470
+ throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is invalid.");
2471
+ }
2472
+ return {
2473
+ algorithm: "sha256-v1",
2474
+ digest: record.digest,
2475
+ actionCount: Number(record.actionCount)
2476
+ };
2477
+ }
2478
+ /** @deprecated Prefer searchActions() so callers retain the continuation cursor. */
2479
+ async listActions(options) {
2480
+ if (!options) {
2481
+ throw new TopoloSdkError("service_required", "service is required for bounded action discovery");
2482
+ }
2483
+ return (await this.searchActions(options)).actions;
2400
2484
  }
2401
2485
  async getAction(action) {
2402
- const needle = normalizeActionLookupKey(action);
2403
- const actions = await this.listActions();
2404
- const match = actions.find((entry) => actionEntryMatches(entry, needle));
2405
- if (!match) {
2486
+ const actionId = action.trim();
2487
+ if (!actionId) {
2406
2488
  throw new TopoloSdkError(
2407
2489
  "unknown_action",
2408
- `Unknown Topolo action "${action}". Run action discovery first and pass an action id, name, or tool name.`
2490
+ "action id is required"
2409
2491
  );
2410
2492
  }
2493
+ let res;
2494
+ try {
2495
+ res = await this.request({
2496
+ service: "developers",
2497
+ method: "GET",
2498
+ path: "/api/actions/catalog",
2499
+ query: { actionId }
2500
+ });
2501
+ } catch (error) {
2502
+ if (error instanceof TopoloHttpError && error.status === 404) {
2503
+ throw new TopoloSdkError("unknown_action", `Unknown or unavailable Topolo action "${action}".`);
2504
+ }
2505
+ throw error;
2506
+ }
2507
+ const raw = res.data?.action ?? res.action;
2508
+ const match = normalizeActionCatalogEntry(raw);
2509
+ if (!match) throw new TopoloSdkError("unknown_action", `Unknown or unavailable Topolo action "${action}".`);
2411
2510
  return match;
2412
2511
  }
2413
2512
  async callAction(action, input2 = {}, options = {}) {
2414
2513
  const entry = await this.getAction(action);
2514
+ const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
2515
+ if (agentAccess === "off") {
2516
+ throw new TopoloPermissionError(
2517
+ `Topolo action "${entry.actionId}" is not available for agent invocation.`,
2518
+ entry.requiredPermission ? [entry.requiredPermission] : []
2519
+ );
2520
+ }
2521
+ if ((agentAccess === "confirm" || entry.requiresConfirmation || entry.destructive) && options.confirm !== true) {
2522
+ throw new TopoloConfirmationRequiredError(
2523
+ `Topolo action "${entry.actionId}" requires { confirm: true } before invocation.`
2524
+ );
2525
+ }
2415
2526
  const mapped = mapActionInput(entry, input2);
2416
2527
  const service = entry.appSlug || entry.appId;
2417
2528
  return this.request({
2418
2529
  service,
2530
+ ...entry.apiBaseUrl ? { baseUrl: entry.apiBaseUrl } : {},
2419
2531
  method: entry.method,
2420
2532
  path: mapped.path,
2421
2533
  ...mapped.query ? { query: mapped.query } : {},
2422
2534
  ...mapped.body !== void 0 ? { body: mapped.body } : {},
2535
+ ...options.resource ? { resource: options.resource } : {},
2423
2536
  headers: {
2424
2537
  [TOPOLO_ACTION_ID_HEADER]: entry.actionId,
2425
2538
  [TOPOLO_ACTION_NAME_HEADER]: entry.name
@@ -2557,6 +2670,18 @@ function serviceKeyCandidates2(slug) {
2557
2670
  function normalizeLookupKey(value) {
2558
2671
  return String(value || "").trim().toLowerCase().replace(/\s+/g, "-");
2559
2672
  }
2673
+ function requireHttpOrigin(value) {
2674
+ let url;
2675
+ try {
2676
+ url = new URL(value);
2677
+ } catch {
2678
+ throw new TopoloSdkError("invalid_action_target", `Invalid action API origin "${value}".`);
2679
+ }
2680
+ if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password) {
2681
+ throw new TopoloSdkError("invalid_action_target", `Invalid action API origin "${value}".`);
2682
+ }
2683
+ return url.origin;
2684
+ }
2560
2685
  function deriveDevelopersFromAuth(authUrl) {
2561
2686
  if (!authUrl) return null;
2562
2687
  try {
@@ -2583,6 +2708,47 @@ function catalogEntryLookupKeys(entry) {
2583
2708
  }
2584
2709
  return [...keys];
2585
2710
  }
2711
+ function catalogSearchQuery(options) {
2712
+ const query = {};
2713
+ if (typeof options.query === "string" && options.query.trim()) query.query = options.query.trim();
2714
+ if (typeof options.cursor === "string" && options.cursor.trim()) query.cursor = options.cursor.trim();
2715
+ if (typeof options.limit === "number" && Number.isFinite(options.limit)) query.limit = Math.trunc(options.limit);
2716
+ return query;
2717
+ }
2718
+ function normalizeCatalogPage(value) {
2719
+ if (!isRecord(value)) return { nextCursor: null, hasMore: false };
2720
+ return {
2721
+ nextCursor: typeof value.nextCursor === "string" && value.nextCursor ? value.nextCursor : null,
2722
+ hasMore: value.hasMore === true
2723
+ };
2724
+ }
2725
+ function normalizeServiceCatalogEntry(item) {
2726
+ if (!isRecord(item)) return null;
2727
+ const appId = typeof item.app_id === "string" ? item.app_id : typeof item.id === "string" ? item.id : null;
2728
+ if (!appId) return null;
2729
+ const permissions = Array.isArray(item.granted_permissions) ? item.granted_permissions.filter(
2730
+ (permission) => typeof permission === "string" && permission.trim().length > 0
2731
+ ) : [];
2732
+ const catalogApiKeyResources = normalizeApiKeyResourceMetadataList(
2733
+ item.api_key_resources ?? item.apiKeyResources
2734
+ );
2735
+ const apiKeyResource = normalizeApiKeyResourceMetadata(item.api_key_resource ?? item.apiKeyResource) ?? catalogApiKeyResources[0] ?? null;
2736
+ const apiKeyResources = catalogApiKeyResources.length > 0 ? catalogApiKeyResources : apiKeyResource ? [apiKeyResource] : [];
2737
+ return {
2738
+ appId,
2739
+ slug: typeof item.app_slug === "string" ? item.app_slug : null,
2740
+ name: typeof item.name === "string" ? item.name : null,
2741
+ description: typeof item.description === "string" ? item.description : null,
2742
+ apiBaseUrl: typeof item.api_base_url === "string" ? item.api_base_url : typeof item.apiBaseUrl === "string" ? item.apiBaseUrl : null,
2743
+ launchUrl: typeof item.launch_url === "string" ? item.launch_url : typeof item.launchUrl === "string" ? item.launchUrl : null,
2744
+ status: typeof item.status === "string" ? item.status : null,
2745
+ surfaceKind: typeof item.surface_kind === "string" ? item.surface_kind : null,
2746
+ launchable: item.launchable === true,
2747
+ permissions,
2748
+ apiKeyResource,
2749
+ apiKeyResources
2750
+ };
2751
+ }
2586
2752
  function normalizeActionCatalogEntry(item) {
2587
2753
  if (!item || typeof item !== "object" || Array.isArray(item)) return null;
2588
2754
  const a = item;
@@ -2596,6 +2762,8 @@ function normalizeActionCatalogEntry(item) {
2596
2762
  }
2597
2763
  return {
2598
2764
  actionId,
2765
+ target: pickActionString(a.target_key) ?? pickActionString(a.target) ?? null,
2766
+ apiBaseUrl: pickActionString(a.api_base_url) ?? pickActionString(a.apiBaseUrl) ?? null,
2599
2767
  name,
2600
2768
  toolName: pickActionString(a.tool_name) ?? pickActionString(a.toolName) ?? null,
2601
2769
  title: pickActionString(a.title) ?? name,
@@ -2607,6 +2775,7 @@ function normalizeActionCatalogEntry(item) {
2607
2775
  path: path4,
2608
2776
  permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
2609
2777
  requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? "",
2778
+ authorizationMode: pickAuthorizationMode(a.authorization_mode ?? a.authorizationMode),
2610
2779
  inputSchema: plainObject(a.input_schema ?? a.inputSchema),
2611
2780
  outputSchema: plainObject(a.output_schema ?? a.outputSchema),
2612
2781
  readOnly: Boolean(a.read_only ?? a.readOnly),
@@ -2626,16 +2795,12 @@ function pickActionString(value) {
2626
2795
  function pickAgentAccess(value) {
2627
2796
  return value === "auto" || value === "confirm" || value === "off" ? value : null;
2628
2797
  }
2798
+ function pickAuthorizationMode(value) {
2799
+ return value === "runtime" ? "runtime" : "static";
2800
+ }
2629
2801
  function plainObject(value) {
2630
2802
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
2631
2803
  }
2632
- function normalizeActionLookupKey(value) {
2633
- return String(value || "").trim().toLowerCase();
2634
- }
2635
- function actionEntryMatches(entry, needle) {
2636
- if (!needle) return false;
2637
- return [entry.actionId, entry.name, entry.toolName].filter((value) => typeof value === "string" && value.length > 0).some((value) => value.toLowerCase() === needle);
2638
- }
2639
2804
  function mapActionInput(action, input2) {
2640
2805
  const remaining = { ...input2 || {} };
2641
2806
  const path4 = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
@@ -3039,6 +3204,9 @@ var TopoloOAuth2 = class {
3039
3204
  return parsed;
3040
3205
  }
3041
3206
  };
3207
+ function contextKey(contextType, organizationId) {
3208
+ return contextType === "organization" && organizationId ? `org:${organizationId}` : "personal";
3209
+ }
3042
3210
  var ENVIRONMENT_DEFAULT_SERVICE_URLS = {
3043
3211
  production: {
3044
3212
  developers: "https://developers.topolo.app"
@@ -3132,6 +3300,28 @@ function writeProfileConfig(config, environment, profile) {
3132
3300
  };
3133
3301
  return { ...config, profiles };
3134
3302
  }
3303
+ function writeContextCredential(config, environment, ctx, makeActive = true) {
3304
+ const key = contextKey(ctx.contextType, ctx.organizationId);
3305
+ const profile = profileForEnvironment(config, environment);
3306
+ const contexts = { ...profile.contexts ?? {}, [key]: ctx };
3307
+ const next = { contexts };
3308
+ if (makeActive) {
3309
+ next.activeContext = key;
3310
+ next.credential = ctx.credential;
3311
+ next.oauthClientId = ctx.oauthClientId ?? profile.oauthClientId;
3312
+ }
3313
+ return writeProfileConfig(config, environment, next);
3314
+ }
3315
+ function activateContext(config, environment, key) {
3316
+ const profile = profileForEnvironment(config, environment);
3317
+ const ctx = profile.contexts?.[key];
3318
+ if (!ctx) return config;
3319
+ return writeProfileConfig(config, environment, {
3320
+ activeContext: key,
3321
+ credential: ctx.credential,
3322
+ oauthClientId: ctx.oauthClientId ?? profile.oauthClientId
3323
+ });
3324
+ }
3135
3325
  function clearProfileCredential(config, environment) {
3136
3326
  const next = { ...config };
3137
3327
  if (environment === "production") {
@@ -3163,7 +3353,7 @@ async function resolveCredential(environment = selectedEnvironment()) {
3163
3353
  return profileForEnvironment(config, environment).credential ?? null;
3164
3354
  }
3165
3355
  var REFRESH_SKEW_SECONDS = 60;
3166
- async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
3356
+ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
3167
3357
  const prefix = envVarPrefix(environment);
3168
3358
  if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || process.env.TOPOLO_API_KEY || process.env.TOPOLO_ACCESS_TOKEN) {
3169
3359
  return resolveCredential(environment);
@@ -3174,7 +3364,7 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
3174
3364
  if (!cred || cred.kind !== "access_token") return cred ?? null;
3175
3365
  if (!cred.refreshToken || !cred.expiresAt || !profile.oauthClientId) return cred;
3176
3366
  const now = Math.floor(Date.now() / 1e3);
3177
- if (cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
3367
+ if (!options.force && cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
3178
3368
  try {
3179
3369
  const oauth = new TopoloOAuth2({
3180
3370
  serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
@@ -3189,9 +3379,20 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
3189
3379
  ...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: cred.refreshToken },
3190
3380
  expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
3191
3381
  };
3192
- await writeConfig(
3193
- environment === "production" && !config.profiles?.production ? { ...config, credential: refreshed } : writeProfileConfig(config, environment, { credential: refreshed })
3194
- );
3382
+ const activeContext = profile.activeContext;
3383
+ const activeEntry = activeContext ? profile.contexts?.[activeContext] : void 0;
3384
+ const updated = activeContext && activeEntry ? writeProfileConfig(config, environment, {
3385
+ credential: refreshed,
3386
+ contexts: {
3387
+ ...profile.contexts ?? {},
3388
+ [activeContext]: {
3389
+ ...activeEntry,
3390
+ credential: refreshed,
3391
+ oauthClientId: activeEntry.oauthClientId ?? profile.oauthClientId
3392
+ }
3393
+ }
3394
+ }) : environment === "production" && !config.profiles?.production ? { ...config, credential: refreshed } : writeProfileConfig(config, environment, { credential: refreshed });
3395
+ await writeConfig(updated);
3195
3396
  return refreshed;
3196
3397
  } catch {
3197
3398
  return cred;
@@ -3243,7 +3444,14 @@ function oauthClientRegistrationUrl(environment = selectedEnvironment()) {
3243
3444
  function apiKeyIssuanceUrl(environment = selectedEnvironment()) {
3244
3445
  return `${developerConsoleUrl(environment)}/credentials`;
3245
3446
  }
3246
- var DEFAULT_CLI_CLIENT_IDS = {};
3447
+ var DEFAULT_CLI_CLIENT_IDS = {
3448
+ // First-party `topolo-cli` device-flow client, seeded per environment by Auth
3449
+ // (core/TopoloAuth/scripts/seed-first-party-oauth-clients.sql). Each env's
3450
+ // Auth must have this client before the CLI ships pointing at it.
3451
+ production: "topolo-cli",
3452
+ staging: "topolo-cli",
3453
+ development: "topolo-cli"
3454
+ };
3247
3455
  function resolveOAuthClientId(config, environment = selectedEnvironment()) {
3248
3456
  const envSpecific = process.env[`${envVarPrefix(environment)}_CLI_CLIENT_ID`];
3249
3457
  if (envSpecific) return envSpecific;
@@ -3301,18 +3509,43 @@ function normalizeError(err) {
3301
3509
  const code = err.code;
3302
3510
  const hintMap = {
3303
3511
  auth_error: "Run `topolo auth login` or set TOPOLO_API_KEY.",
3512
+ confirmation_required: "Review the action and rerun with --confirm to allow it.",
3304
3513
  permission_denied: "Check your API key scopes or role assignments in TopoloDevelopers."
3305
3514
  };
3306
3515
  return {
3307
3516
  message: err.message,
3308
3517
  code,
3309
3518
  hint: code ? hintMap[code] : void 0,
3310
- exitCode: code === "auth_error" ? 2 : code === "permission_denied" ? 3 : 1
3519
+ exitCode: code === "auth_error" || code === "confirmation_required" ? 2 : code === "permission_denied" ? 3 : 1
3311
3520
  };
3312
3521
  }
3313
3522
  return { message: String(err), exitCode: 1 };
3314
3523
  }
3315
3524
 
3525
+ // src/version.ts
3526
+ import { readFileSync } from "fs";
3527
+ import { dirname as dirname2, join as join2 } from "path";
3528
+ import { fileURLToPath } from "url";
3529
+ function readCliVersion() {
3530
+ try {
3531
+ const here = dirname2(fileURLToPath(import.meta.url));
3532
+ const candidates = [
3533
+ join2(here, "package.json"),
3534
+ join2(here, "..", "package.json")
3535
+ ];
3536
+ for (const path4 of candidates) {
3537
+ try {
3538
+ const pkg = JSON.parse(readFileSync(path4, "utf8"));
3539
+ if (typeof pkg.version === "string" && pkg.version) return pkg.version;
3540
+ } catch {
3541
+ }
3542
+ }
3543
+ } catch {
3544
+ }
3545
+ return "0.0.0";
3546
+ }
3547
+ var CLI_VERSION = readCliVersion();
3548
+
3316
3549
  // src/commands/auth.ts
3317
3550
  function registerAuthCommands(program2) {
3318
3551
  const auth = program2.command("auth").description("Manage Topolo credentials");
@@ -3450,18 +3683,15 @@ async function runDeviceFlow(options) {
3450
3683
  } finally {
3451
3684
  spinner?.stop();
3452
3685
  }
3686
+ const credential = {
3687
+ kind: "access_token",
3688
+ accessToken: token.access_token,
3689
+ ...token.refresh_token ? { refreshToken: token.refresh_token } : {},
3690
+ expiresAt: Math.floor(Date.now() / 1e3) + token.expires_in
3691
+ };
3692
+ const context = await resolveLoginContext(credential, resolved.serviceUrls, options.clientId);
3453
3693
  const updated = await readConfig();
3454
- await writeConfig(
3455
- writeProfileConfig(updated, options.environment, {
3456
- credential: {
3457
- kind: "access_token",
3458
- accessToken: token.access_token,
3459
- ...token.refresh_token ? { refreshToken: token.refresh_token } : {},
3460
- expiresAt: Math.floor(Date.now() / 1e3) + token.expires_in
3461
- },
3462
- oauthClientId: options.clientId
3463
- })
3464
- );
3694
+ await writeConfig(writeContextCredential(updated, options.environment, context));
3465
3695
  emit(
3466
3696
  {
3467
3697
  ok: true,
@@ -3476,6 +3706,29 @@ async function runDeviceFlow(options) {
3476
3706
  () => "\n \u2713 Signed in. Run `topolo whoami` to see your identity."
3477
3707
  );
3478
3708
  }
3709
+ async function resolveLoginContext(credential, serviceUrls, clientId) {
3710
+ try {
3711
+ const topolo = createTopolo({
3712
+ credential,
3713
+ agent: { clientName: "topolo-cli", clientVersion: CLI_VERSION },
3714
+ serviceUrls
3715
+ });
3716
+ const me = await topolo.identity.whoami();
3717
+ const org = me.organization;
3718
+ if (org?.id) {
3719
+ return {
3720
+ contextType: "organization",
3721
+ organizationId: org.id,
3722
+ organizationSlug: org.slug,
3723
+ organizationName: org.name,
3724
+ credential,
3725
+ oauthClientId: clientId
3726
+ };
3727
+ }
3728
+ } catch {
3729
+ }
3730
+ return { contextType: "personal", credential, oauthClientId: clientId };
3731
+ }
3479
3732
  function tryOpenBrowser(url) {
3480
3733
  try {
3481
3734
  const os = platform2();
@@ -3540,30 +3793,6 @@ function sleep(ms) {
3540
3793
  return new Promise((resolve4) => setTimeout(resolve4, ms));
3541
3794
  }
3542
3795
 
3543
- // src/version.ts
3544
- import { readFileSync } from "fs";
3545
- import { dirname as dirname2, join as join2 } from "path";
3546
- import { fileURLToPath } from "url";
3547
- function readCliVersion() {
3548
- try {
3549
- const here = dirname2(fileURLToPath(import.meta.url));
3550
- const candidates = [
3551
- join2(here, "package.json"),
3552
- join2(here, "..", "package.json")
3553
- ];
3554
- for (const path4 of candidates) {
3555
- try {
3556
- const pkg = JSON.parse(readFileSync(path4, "utf8"));
3557
- if (typeof pkg.version === "string" && pkg.version) return pkg.version;
3558
- } catch {
3559
- }
3560
- }
3561
- } catch {
3562
- }
3563
- return "0.0.0";
3564
- }
3565
- var CLI_VERSION = readCliVersion();
3566
-
3567
3796
  // src/client.ts
3568
3797
  async function buildClient(options) {
3569
3798
  const environment = selectedEnvironment();
@@ -3580,6 +3809,7 @@ async function buildClient(options) {
3580
3809
  },
3581
3810
  serviceUrls: profile.serviceUrls,
3582
3811
  requireConfirmForWrites: options?.requireConfirmForWrites,
3812
+ ...credential.kind === "access_token" && credential.refreshToken ? { refreshCredential: () => refreshCredentialIfNeeded(environment, { force: true }) } : {},
3583
3813
  ...isDebugEnabled() ? { debug: stderrDebugLogger } : {}
3584
3814
  });
3585
3815
  }
@@ -3642,93 +3872,6 @@ function formatPretty(info) {
3642
3872
  return lines.join("\n");
3643
3873
  }
3644
3874
 
3645
- // src/commands/actions.ts
3646
- function registerActionsCommand(program2) {
3647
- const actions = program2.command("actions").description("List and call permission-scoped Topolo actions from the live catalog").option("-s, --service <service>", "Filter by app id, slug, or name").option("--json", "Output JSON", true).option("--no-json", "Output human-readable table").action(async (opts) => {
3648
- const mode = resolveOutputMode(opts.json);
3649
- try {
3650
- const topolo = await buildClient();
3651
- if (!topolo) {
3652
- throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
3653
- }
3654
- const rows = await topolo.client.listActions({ service: opts.service });
3655
- emit({ actions: rows }, mode, () => formatActionRows(rows));
3656
- } catch (err) {
3657
- fail(err, mode);
3658
- }
3659
- });
3660
- 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) => {
3661
- const mode = resolveOutputMode(opts.json);
3662
- try {
3663
- const topolo = await buildClient();
3664
- if (!topolo) {
3665
- throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
3666
- }
3667
- const entry = await topolo.client.getAction(action);
3668
- emit(entry, mode, () => formatActionDetail(entry));
3669
- } catch (err) {
3670
- fail(err, mode);
3671
- }
3672
- });
3673
- 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) => {
3674
- const mode = resolveOutputMode(opts.json);
3675
- try {
3676
- const topolo = await buildClient();
3677
- if (!topolo) {
3678
- throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
3679
- }
3680
- const input2 = parseJsonObject(opts.data || "{}");
3681
- const result = await topolo.client.callAction(action, input2, { confirm: opts.confirm === true });
3682
- emit(result, mode, () => JSON.stringify(result, null, 2));
3683
- } catch (err) {
3684
- fail(err, mode);
3685
- }
3686
- });
3687
- }
3688
- function parseJsonObject(value) {
3689
- let parsed;
3690
- try {
3691
- parsed = JSON.parse(value);
3692
- } catch {
3693
- throw new Error("--data must be valid JSON");
3694
- }
3695
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
3696
- throw new Error("--data must be a JSON object");
3697
- }
3698
- return parsed;
3699
- }
3700
- function formatActionRows(rows) {
3701
- if (rows.length === 0) return "No actions available for this credential.";
3702
- const serviceWidth = Math.max(7, ...rows.map((row) => (row.appSlug || row.appId).length));
3703
- const nameWidth = Math.max(6, ...rows.map((row) => row.name.length));
3704
- const methodWidth = Math.max(6, ...rows.map((row) => row.method.length));
3705
- const lines = [
3706
- `${"SERVICE".padEnd(serviceWidth)} ${"ACTION".padEnd(nameWidth)} ${"METHOD".padEnd(methodWidth)} CONFIRM PERMISSION`,
3707
- `${"-".repeat(serviceWidth)} ${"-".repeat(nameWidth)} ${"-".repeat(methodWidth)} ------- ----------`
3708
- ];
3709
- for (const row of rows) {
3710
- const confirmation = row.requiresConfirmation ? "yes" : "no";
3711
- lines.push(
3712
- `${(row.appSlug || row.appId).padEnd(serviceWidth)} ${row.name.padEnd(nameWidth)} ${row.method.padEnd(methodWidth)} ${confirmation.padEnd(7)} ${row.requiredPermission}`
3713
- );
3714
- }
3715
- return lines.join("\n");
3716
- }
3717
- function formatActionDetail(action) {
3718
- return [
3719
- `Action: ${action.name}`,
3720
- `ID: ${action.actionId}`,
3721
- `Tool: ${action.toolName || "-"}`,
3722
- `Service: ${action.appSlug || action.appId}`,
3723
- `Method: ${action.method}`,
3724
- `Path: ${action.path}`,
3725
- `Permission: ${action.requiredPermission}`,
3726
- `Confirmation: ${action.requiresConfirmation ? "required" : "not required"}`,
3727
- "",
3728
- action.description
3729
- ].join("\n");
3730
- }
3731
-
3732
3875
  // src/commands/api.ts
3733
3876
  var STATIC_APP_IDS = new Set(Object.keys(DEFAULT_APP_URLS));
3734
3877
  var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
@@ -3832,6 +3975,113 @@ function parseQueryPairs(pairs) {
3832
3975
  return out;
3833
3976
  }
3834
3977
 
3978
+ // src/commands/actions.ts
3979
+ function registerActionsCommand(program2) {
3980
+ const actions = program2.command("actions").description("Search permission-scoped Topolo actions for one application").option("-s, --service <service>", "App id, slug, or name (required for search)").option("-q, --query <query>", "Filter by action id, name, or title").option("--cursor <cursor>", "Continuation cursor from the previous page").option("--limit <limit>", "Page size (1-100)", "25").option("--json", "Output JSON").option("--no-json", "Output human-readable table").action(async (opts) => {
3981
+ const mode = resolveOutputMode(opts.json);
3982
+ try {
3983
+ const topolo = await buildClient();
3984
+ if (!topolo) {
3985
+ throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
3986
+ }
3987
+ if (!opts.service?.trim()) throw new Error("--service is required for action search");
3988
+ const result = await topolo.client.searchActions({
3989
+ service: opts.service.trim(),
3990
+ query: opts.query,
3991
+ cursor: opts.cursor,
3992
+ limit: parsePageSize(opts.limit)
3993
+ });
3994
+ emit(result, mode, () => formatActionRows(result.actions, result.page.nextCursor));
3995
+ } catch (err) {
3996
+ fail(err, mode);
3997
+ }
3998
+ });
3999
+ actions.command("get <action>").description("Describe one Topolo action by exact action id").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (action, opts, command) => {
4000
+ const mode = resolveOutputMode(opts.json ?? command.parent?.opts().json);
4001
+ try {
4002
+ const topolo = await buildClient();
4003
+ if (!topolo) {
4004
+ throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
4005
+ }
4006
+ const entry = await topolo.client.getAction(action);
4007
+ emit(entry, mode, () => formatActionDetail(entry));
4008
+ } catch (err) {
4009
+ fail(err, mode);
4010
+ }
4011
+ });
4012
+ actions.command("call <action>").description("Call a permission-scoped Topolo action").option("-d, --data <json>", "JSON action input", "{}").option("--resource-type <type>", "App-local resource alias, such as workspace").option("--resource-id <id>", "Resource id for the selected resource type").option("--confirm", "Acknowledge a mutating action").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (action, opts, command) => {
4013
+ const mode = resolveOutputMode(opts.json ?? command.parent?.opts().json);
4014
+ try {
4015
+ const topolo = await buildClient();
4016
+ if (!topolo) {
4017
+ throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
4018
+ }
4019
+ const input2 = parseJsonObject(opts.data || "{}");
4020
+ const resource = parseResourceOption(opts.resourceType, opts.resourceId);
4021
+ const result = await topolo.client.callAction(action, input2, {
4022
+ confirm: opts.confirm === true,
4023
+ ...resource ? { resource } : {}
4024
+ });
4025
+ emit(result, mode, () => JSON.stringify(result, null, 2));
4026
+ } catch (err) {
4027
+ fail(err, mode);
4028
+ }
4029
+ });
4030
+ }
4031
+ function parseJsonObject(value) {
4032
+ let parsed;
4033
+ try {
4034
+ parsed = JSON.parse(value);
4035
+ } catch {
4036
+ throw new Error("--data must be valid JSON");
4037
+ }
4038
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
4039
+ throw new Error("--data must be a JSON object");
4040
+ }
4041
+ return parsed;
4042
+ }
4043
+ function formatActionRows(rows, nextCursor = null) {
4044
+ if (rows.length === 0) return "No actions available for this credential.";
4045
+ const serviceWidth = Math.max(7, ...rows.map((row) => (row.appSlug || row.appId).length));
4046
+ const nameWidth = Math.max(6, ...rows.map((row) => row.name.length));
4047
+ const methodWidth = Math.max(6, ...rows.map((row) => row.method.length));
4048
+ const lines = [
4049
+ `${"SERVICE".padEnd(serviceWidth)} ${"ACTION".padEnd(nameWidth)} ${"METHOD".padEnd(methodWidth)} CONFIRM PERMISSION`,
4050
+ `${"-".repeat(serviceWidth)} ${"-".repeat(nameWidth)} ${"-".repeat(methodWidth)} ------- ----------`
4051
+ ];
4052
+ for (const row of rows) {
4053
+ const confirmation = row.requiresConfirmation ? "yes" : "no";
4054
+ lines.push(
4055
+ `${(row.appSlug || row.appId).padEnd(serviceWidth)} ${row.name.padEnd(nameWidth)} ${row.method.padEnd(methodWidth)} ${confirmation.padEnd(7)} ${row.requiredPermission}`
4056
+ );
4057
+ }
4058
+ if (nextCursor) lines.push("", `Next cursor: ${nextCursor}`);
4059
+ return lines.join("\n");
4060
+ }
4061
+ function parsePageSize(value) {
4062
+ const parsed = Number.parseInt(value || "25", 10);
4063
+ if (!Number.isFinite(parsed) || parsed < 1 || parsed > 100) {
4064
+ throw new Error("--limit must be an integer between 1 and 100");
4065
+ }
4066
+ return parsed;
4067
+ }
4068
+ function formatActionDetail(action) {
4069
+ return [
4070
+ `Action: ${action.name}`,
4071
+ `ID: ${action.actionId}`,
4072
+ `Tool: ${action.toolName || "-"}`,
4073
+ `Service: ${action.appSlug || action.appId}`,
4074
+ `Target: ${action.target || "-"}`,
4075
+ `API URL: ${action.apiBaseUrl || "-"}`,
4076
+ `Method: ${action.method}`,
4077
+ `Path: ${action.path}`,
4078
+ `Permission: ${action.requiredPermission}`,
4079
+ `Confirmation: ${action.requiresConfirmation ? "required" : "not required"}`,
4080
+ "",
4081
+ action.description
4082
+ ].join("\n");
4083
+ }
4084
+
3835
4085
  // src/commands/apps.ts
3836
4086
  import { execSync } from "child_process";
3837
4087
  import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
@@ -5120,7 +5370,7 @@ function packageJson(application, target, options) {
5120
5370
  dependencies["@topolo/sdk"] = topoloPackageVersion("@topolo/sdk", target);
5121
5371
  scripts.deploy = "npm run build && wrangler deploy";
5122
5372
  }
5123
- devDependencies.wrangler = "^4.40.2";
5373
+ devDependencies.wrangler = "^4.91.0";
5124
5374
  }
5125
5375
  if (target === "platform" && hasScaffoldResources(options.resources)) {
5126
5376
  scripts["check:deploy-config"] = "wrangler deploy --config apps/api/wrangler.toml --env staging --dry-run --outdir .wrangler/api-build && wrangler deploy --config apps/web/wrangler.toml --env staging --dry-run --outdir .wrangler/web-build";
@@ -5140,11 +5390,11 @@ function topoloPackageVersion(packageName, target) {
5140
5390
  if (target === "developer") {
5141
5391
  if (packageName === "@topolo/login") return "^0.1.0";
5142
5392
  if (packageName === "@topolo-io/auth-client") return "^1.0.0";
5143
- if (packageName === "@topolo/sdk") return "^0.4.9";
5393
+ if (packageName === "@topolo/sdk") return "^0.8.1";
5144
5394
  return "^0.1.0";
5145
5395
  }
5146
5396
  const platformPackageVersions = {
5147
- "@topolo/sdk": "^0.4.9",
5397
+ "@topolo/sdk": "^0.8.1",
5148
5398
  "@topolo/login": "^0.1.0",
5149
5399
  "@topolo-io/app-funnel": "^0.1.6",
5150
5400
  "@topolo-io/app-launcher": "^0.1.15",
@@ -8372,15 +8622,20 @@ var CLOUDCONTROL_LOCAL_APPLICATION = {
8372
8622
  deployTargets: []
8373
8623
  };
8374
8624
  function registerAppsCommand(program2) {
8375
- const apps = program2.command("apps").description("List Topolo platform applications available to the current credential").option("--json", "Output JSON", true).option("--no-json", "Output human-readable table").action(async (opts) => {
8625
+ const apps = program2.command("apps").description("Search Topolo platform applications available to the current credential").option("-q, --query <query>", "Filter by app id, slug, or name").option("--cursor <cursor>", "Continuation cursor from the previous page").option("--limit <limit>", "Page size (1-100)", "25").option("--json", "Output JSON", true).option("--no-json", "Output human-readable table").action(async (opts) => {
8376
8626
  const mode = resolveOutputMode(opts.json);
8377
8627
  try {
8378
8628
  const topolo = await buildClient();
8379
8629
  if (!topolo) {
8380
8630
  throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
8381
8631
  }
8382
- const rows = applicationRowsFromCatalog(await topolo.client.listServices());
8383
- emit({ applications: rows }, mode, () => formatRows(rows));
8632
+ const result = await topolo.client.searchServices({
8633
+ query: opts.query,
8634
+ cursor: opts.cursor,
8635
+ limit: parsePageSize2(opts.limit)
8636
+ });
8637
+ const rows = applicationRowsFromCatalog(result.services);
8638
+ emit({ applications: rows, page: result.page }, mode, () => formatRows(rows));
8384
8639
  } catch (err) {
8385
8640
  fail(err, mode);
8386
8641
  }
@@ -8392,7 +8647,8 @@ function registerAppsCommand(program2) {
8392
8647
  if (!topolo) {
8393
8648
  throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
8394
8649
  }
8395
- const app = resolveCatalogApplication(await topolo.client.listServices(), application);
8650
+ const entry = await topolo.client.getService(application);
8651
+ const app = applicationRowsFromCatalog([entry])[0];
8396
8652
  emit({ application: app }, mode, () => formatApplicationRow(app));
8397
8653
  } catch (err) {
8398
8654
  fail(err, mode);
@@ -8512,16 +8768,12 @@ function applicationRowsFromCatalog(catalog) {
8512
8768
  deployTargets: 0
8513
8769
  })).sort((left, right) => left.id.localeCompare(right.id));
8514
8770
  }
8515
- function resolveCatalogApplication(catalog, application) {
8516
- const needle = application.trim().toLowerCase();
8517
- const rows = applicationRowsFromCatalog(catalog);
8518
- const match = rows.find(
8519
- (row) => row.id.toLowerCase() === needle || row.name.toLowerCase() === needle || row.services.some((service) => service.toLowerCase() === needle)
8520
- );
8521
- if (!match) {
8522
- throw new Error(`Unknown application "${application}". Run \`topolo apps --json\` to discover applications available to this credential.`);
8771
+ function parsePageSize2(value) {
8772
+ const parsed = Number.parseInt(value || "25", 10);
8773
+ if (!Number.isFinite(parsed) || parsed < 1 || parsed > 100) {
8774
+ throw new Error("--limit must be an integer between 1 and 100");
8523
8775
  }
8524
- return match;
8776
+ return parsed;
8525
8777
  }
8526
8778
  function resolveApplication(application) {
8527
8779
  const id = application.trim().toLowerCase();
@@ -9074,7 +9326,7 @@ async function requirePlatformAdminSession() {
9074
9326
 
9075
9327
  // src/commands/services.ts
9076
9328
  function registerServicesCommand(program2) {
9077
- program2.command("services").description("List every Topolo service the SDK can route to").option("--json", "Output JSON", true).option("--no-json", "Output human-readable table").action(async (opts) => {
9329
+ program2.command("services").description("Search Topolo services the SDK can route to").option("-q, --query <query>", "Filter by app id, slug, or name").option("--cursor <cursor>", "Continuation cursor from the previous page").option("--limit <limit>", "Page size (1-100)", "25").option("--json", "Output JSON", true).option("--no-json", "Output human-readable table").action(async (opts) => {
9078
9330
  const mode = resolveOutputMode(opts.json);
9079
9331
  try {
9080
9332
  const environment = selectedEnvironment();
@@ -9083,8 +9335,13 @@ function registerServicesCommand(program2) {
9083
9335
  if (!topolo) {
9084
9336
  throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
9085
9337
  }
9086
- const rows = buildRows(await topolo.client.listServices(), config.serviceUrls, environment);
9087
- emit({ environment, services: rows }, mode, () => formatRows2(rows, environment));
9338
+ const result = await topolo.client.searchServices({
9339
+ query: opts.query,
9340
+ cursor: opts.cursor,
9341
+ limit: parsePageSize3(opts.limit)
9342
+ });
9343
+ const rows = buildRows(result.services, config.serviceUrls, environment);
9344
+ emit({ environment, services: rows, page: result.page }, mode, () => formatRows2(rows, environment, result.page.nextCursor));
9088
9345
  } catch (err) {
9089
9346
  fail(err, mode);
9090
9347
  }
@@ -9111,7 +9368,7 @@ function buildRows(catalog, overrides, environment = selectedEnvironment()) {
9111
9368
  };
9112
9369
  }).sort((left, right) => left.id.localeCompare(right.id));
9113
9370
  }
9114
- function formatRows2(rows, environment = selectedEnvironment()) {
9371
+ function formatRows2(rows, environment = selectedEnvironment(), nextCursor = null) {
9115
9372
  const idWidth = Math.max(3, ...rows.map((r) => r.id.length));
9116
9373
  const urlWidth = Math.max(7, ...rows.map((r) => (r.effectiveUrl ?? "-").length));
9117
9374
  const lines = [];
@@ -9126,8 +9383,14 @@ function formatRows2(rows, environment = selectedEnvironment()) {
9126
9383
  `${r.id.padEnd(idWidth)} ${effectiveUrl.padEnd(urlWidth)} ${r.overridden ? "yes" : "".padEnd(8)} ${r.environmentSafe ? "yes" : "no"} ${apiKeyResource}`
9127
9384
  );
9128
9385
  }
9386
+ if (nextCursor) lines.push("", `Next cursor: ${nextCursor}`);
9129
9387
  return lines.join("\n");
9130
9388
  }
9389
+ function parsePageSize3(value) {
9390
+ const parsed = Number.parseInt(value || "25", 10);
9391
+ if (!Number.isFinite(parsed) || parsed < 1 || parsed > 100) throw new Error("--limit must be an integer between 1 and 100");
9392
+ return parsed;
9393
+ }
9131
9394
  function isStagingUrl(value) {
9132
9395
  const url = value.toLowerCase();
9133
9396
  return url.includes(".stg.topolo.us") || url.includes("localhost") || url.includes("127.0.0.1") || url.includes("topolo-staging");
@@ -9900,6 +10163,167 @@ function formatStatus(rows) {
9900
10163
  ).join("\n");
9901
10164
  }
9902
10165
 
10166
+ // src/commands/context.ts
10167
+ function makeClient(credential, serviceUrls) {
10168
+ return createTopolo({
10169
+ credential,
10170
+ agent: { clientName: "topolo-cli", clientVersion: CLI_VERSION },
10171
+ serviceUrls
10172
+ });
10173
+ }
10174
+ function unwrap(res) {
10175
+ const r = res ?? {};
10176
+ return r.data ?? r;
10177
+ }
10178
+ async function fetchMe(credential, serviceUrls) {
10179
+ const topolo = makeClient(credential, serviceUrls);
10180
+ const data = unwrap(await topolo.client.request({ service: "auth", method: "GET", path: "/api/auth/me" }));
10181
+ const user = data.user ?? {};
10182
+ const rawMemberships = user.organizationMemberships || data.organizations || [];
10183
+ const memberships = rawMemberships.filter((m) => m && m.id);
10184
+ const activeType = user.orgId ? "organization" : "personal";
10185
+ return { memberships, activeType, activeOrgId: user.orgId || void 0 };
10186
+ }
10187
+ function credentialFromTokens(tokens) {
10188
+ const accessToken = tokens.accessToken || tokens.access_token;
10189
+ const refreshToken = tokens.refreshToken || tokens.refresh_token;
10190
+ const expiresIn = tokens.expiresIn || tokens.expires_in;
10191
+ const expiresAtRaw = tokens.expiresAt || tokens.expires_at;
10192
+ return {
10193
+ kind: "access_token",
10194
+ accessToken,
10195
+ ...refreshToken ? { refreshToken } : {},
10196
+ ...expiresAtRaw ? { expiresAt: expiresAtRaw } : expiresIn ? { expiresAt: Math.floor(Date.now() / 1e3) + expiresIn } : {}
10197
+ };
10198
+ }
10199
+ async function exchangeForContext(credential, serviceUrls, target) {
10200
+ const topolo = makeClient(credential, serviceUrls);
10201
+ const data = unwrap(
10202
+ await topolo.client.request({
10203
+ service: "auth",
10204
+ method: "PUT",
10205
+ path: "/api/me/active-context",
10206
+ body: {
10207
+ contextType: target.type,
10208
+ contextId: target.type === "organization" ? target.id : null
10209
+ },
10210
+ confirm: true
10211
+ })
10212
+ );
10213
+ const tokens = data.tokens || {};
10214
+ return credentialFromTokens(tokens);
10215
+ }
10216
+ function labelFor(ctx) {
10217
+ if (ctx.contextType === "personal") return "Personal";
10218
+ return ctx.organizationName || ctx.organizationSlug || ctx.organizationId || "organization";
10219
+ }
10220
+ function registerContextCommand(program2) {
10221
+ const context = program2.command("context").description("Show and switch the active context (personal or an organization)");
10222
+ context.command("ls", { isDefault: true }).alias("list").description("List the contexts you can act in and which is active").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
10223
+ const mode = resolveOutputMode(opts.json);
10224
+ try {
10225
+ const environment = selectedEnvironment();
10226
+ const config = await readConfig();
10227
+ const profile = profileForEnvironment(config, environment);
10228
+ const stored = profile.contexts ?? {};
10229
+ const activeKey = profile.activeContext;
10230
+ const rows = /* @__PURE__ */ new Map();
10231
+ for (const [key, ctx] of Object.entries(stored)) {
10232
+ rows.set(key, { key, type: ctx.contextType, label: labelFor(ctx), signedIn: true, active: key === activeKey });
10233
+ }
10234
+ const cred = await refreshCredentialIfNeeded(environment);
10235
+ if (cred) {
10236
+ try {
10237
+ const me = await fetchMe(cred, resolvedProfileConfig(config, environment).serviceUrls);
10238
+ const personalKey = contextKey("personal");
10239
+ if (!rows.has(personalKey)) {
10240
+ rows.set(personalKey, { key: personalKey, type: "personal", label: "Personal", signedIn: false, active: personalKey === activeKey });
10241
+ }
10242
+ for (const m of me.memberships) {
10243
+ const key = contextKey("organization", m.id);
10244
+ if (!rows.has(key)) {
10245
+ rows.set(key, { key, type: "organization", label: m.name || m.slug || m.id, signedIn: false, active: key === activeKey });
10246
+ }
10247
+ }
10248
+ } catch {
10249
+ }
10250
+ }
10251
+ const list = [...rows.values()];
10252
+ emit({ environment, active: activeKey ?? null, contexts: list }, mode, () => {
10253
+ if (list.length === 0) return "No contexts yet. Run `topolo auth login` first.";
10254
+ const lines = [`Contexts (${environmentLabel(environment)}):`, ""];
10255
+ for (const r of list) {
10256
+ const marks = [r.active ? "active" : "", r.signedIn ? "signed in" : "available"].filter(Boolean).join(", ");
10257
+ lines.push(` ${r.active ? "*" : " "} ${r.label} (${r.type}) [${marks}]`);
10258
+ }
10259
+ lines.push("", "Switch with: topolo context use <organization|personal>");
10260
+ return lines.join("\n");
10261
+ });
10262
+ } catch (err) {
10263
+ fail(err, mode);
10264
+ }
10265
+ });
10266
+ context.command("use <target>").description('Switch the active context (an organization name/slug, or "personal")').option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (target, opts) => {
10267
+ const mode = resolveOutputMode(opts.json);
10268
+ try {
10269
+ const environment = selectedEnvironment();
10270
+ const config = await readConfig();
10271
+ const profile = profileForEnvironment(config, environment);
10272
+ const serviceUrls = resolvedProfileConfig(config, environment).serviceUrls;
10273
+ const cred = await refreshCredentialIfNeeded(environment);
10274
+ if (!cred) {
10275
+ throw new Error("Not signed in. Run `topolo auth login` first.");
10276
+ }
10277
+ const wantPersonal = /^(personal|me)$/i.test(target.trim());
10278
+ let resolved;
10279
+ if (wantPersonal) {
10280
+ resolved = { contextType: "personal" };
10281
+ } else {
10282
+ const me = await fetchMe(cred, serviceUrls);
10283
+ const needle = target.trim().toLowerCase();
10284
+ const match = me.memberships.find(
10285
+ (m) => m.id === target || m.slug?.toLowerCase() === needle || m.name?.toLowerCase() === needle
10286
+ );
10287
+ if (!match) {
10288
+ const names = me.memberships.map((m) => m.slug || m.name || m.id);
10289
+ throw new Error(
10290
+ `You are not a member of "${target}". Available: personal${names.length ? ", " + names.join(", ") : ""}.`
10291
+ );
10292
+ }
10293
+ resolved = {
10294
+ contextType: "organization",
10295
+ organizationId: match.id,
10296
+ organizationSlug: match.slug,
10297
+ organizationName: match.name
10298
+ };
10299
+ }
10300
+ const key = contextKey(resolved.contextType, resolved.organizationId);
10301
+ const existing = profile.contexts?.[key];
10302
+ let updated = config;
10303
+ if (existing?.credential) {
10304
+ updated = activateContext(config, environment, key);
10305
+ } else {
10306
+ const newCredential = await exchangeForContext(cred, serviceUrls, {
10307
+ type: resolved.contextType,
10308
+ id: resolved.organizationId
10309
+ });
10310
+ resolved.credential = newCredential;
10311
+ resolved.oauthClientId = existing?.oauthClientId ?? profile.oauthClientId;
10312
+ updated = writeContextCredential(config, environment, resolved, true);
10313
+ }
10314
+ await writeConfig(updated);
10315
+ emit(
10316
+ { environment, active: key, context: { type: resolved.contextType, label: labelFor(resolved) } },
10317
+ mode,
10318
+ () => `
10319
+ \u2713 Now acting as ${labelFor(resolved)} (${resolved.contextType}).`
10320
+ );
10321
+ } catch (err) {
10322
+ fail(err, mode);
10323
+ }
10324
+ });
10325
+ }
10326
+
9903
10327
  // src/index.ts
9904
10328
  var program = new Command();
9905
10329
  var argv;
@@ -9929,6 +10353,7 @@ registerPlatformCommand(program);
9929
10353
  registerServicesCommand(program);
9930
10354
  registerSeedCommands(program);
9931
10355
  registerSkillsCommand(program);
10356
+ registerContextCommand(program);
9932
10357
  program.parseAsync(argv).catch((err) => {
9933
10358
  process.stderr.write(`Unexpected error: ${err instanceof Error ? err.message : String(err)}
9934
10359
  `);