@orchagent/cli 0.3.31 → 0.3.34

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/lib/api.js CHANGED
@@ -48,6 +48,7 @@ exports.starAgent = starAgent;
48
48
  exports.unstarAgent = unstarAgent;
49
49
  exports.forkAgent = forkAgent;
50
50
  exports.searchAgents = searchAgents;
51
+ exports.searchMyAgents = searchMyAgents;
51
52
  exports.fetchLlmKeys = fetchLlmKeys;
52
53
  exports.downloadCodeBundle = downloadCodeBundle;
53
54
  exports.uploadCodeBundle = uploadCodeBundle;
@@ -233,8 +234,16 @@ async function updateOrg(config, payload) {
233
234
  headers: { 'Content-Type': 'application/json' },
234
235
  });
235
236
  }
236
- async function listPublicAgents(config) {
237
- return publicRequest(config, '/public/agents');
237
+ async function listPublicAgents(config, options) {
238
+ const params = new URLSearchParams();
239
+ if (options?.sort)
240
+ params.append('sort', options.sort);
241
+ if (options?.tags?.length)
242
+ params.append('tags', options.tags.join(','));
243
+ if (options?.type)
244
+ params.append('type', options.type);
245
+ const queryStr = params.toString();
246
+ return publicRequest(config, `/public/agents${queryStr ? `?${queryStr}` : ''}`);
238
247
  }
239
248
  async function getPublicAgent(config, org, agent, version) {
240
249
  return publicRequest(config, `/public/agents/${org}/${agent}/${version}`);
@@ -250,7 +259,7 @@ async function createAgent(config, data) {
250
259
  });
251
260
  }
252
261
  async function starAgent(config, agentId) {
253
- await request(config, 'POST', `/agents/${agentId}/star`);
262
+ return request(config, 'POST', `/agents/${agentId}/star`);
254
263
  }
255
264
  async function unstarAgent(config, agentId) {
256
265
  await request(config, 'DELETE', `/agents/${agentId}/star`);
@@ -266,9 +275,68 @@ async function searchAgents(config, query, options) {
266
275
  params.append('sort', options.sort);
267
276
  if (options?.tags?.length)
268
277
  params.append('tags', options.tags.join(','));
278
+ if (options?.type)
279
+ params.append('type', options.type);
269
280
  const queryStr = params.toString();
270
281
  return publicRequest(config, `/public/agents${queryStr ? `?${queryStr}` : ''}`);
271
282
  }
283
+ /**
284
+ * Search within the authenticated user's own agents (public and private).
285
+ * Uses the authenticated /agents endpoint with client-side filtering.
286
+ */
287
+ async function searchMyAgents(config, query, options) {
288
+ let agents = await listMyAgents(config);
289
+ // Deduplicate: keep only latest version per agent name
290
+ const latestByName = new Map();
291
+ for (const agent of agents) {
292
+ const existing = latestByName.get(agent.name);
293
+ if (!existing || new Date(agent.created_at) > new Date(existing.created_at)) {
294
+ latestByName.set(agent.name, agent);
295
+ }
296
+ }
297
+ agents = Array.from(latestByName.values());
298
+ // Apply type filter
299
+ if (options?.type) {
300
+ const typeFilter = options.type;
301
+ if (typeFilter === 'agents') {
302
+ agents = agents.filter(a => a.type === 'prompt' || a.type === 'code');
303
+ }
304
+ else if (typeFilter === 'skills' || typeFilter === 'skill') {
305
+ agents = agents.filter(a => a.type === 'skill');
306
+ }
307
+ else if (typeFilter === 'code' || typeFilter === 'prompt') {
308
+ agents = agents.filter(a => a.type === typeFilter);
309
+ }
310
+ }
311
+ // Apply search filter (match against name and description)
312
+ if (query) {
313
+ const words = query.toLowerCase().replace(/-/g, ' ').split(/\s+/);
314
+ agents = agents.filter(a => {
315
+ const name = (a.name || '').toLowerCase().replace(/-/g, ' ');
316
+ const desc = (a.description || '').toLowerCase();
317
+ return words.every(w => name.includes(w) || desc.includes(w));
318
+ });
319
+ }
320
+ // Map Agent to PublicAgent-compatible objects
321
+ const org = await getOrg(config);
322
+ return agents.map(a => ({
323
+ id: a.id,
324
+ org_name: org.name || org.slug || 'unknown',
325
+ org_slug: a.org_slug || org.slug || 'unknown',
326
+ name: a.name,
327
+ version: a.version,
328
+ type: a.type,
329
+ description: a.description,
330
+ stars_count: a.stars_count ?? 0,
331
+ tags: a.tags ?? [],
332
+ default_endpoint: a.default_endpoint || 'analyze',
333
+ created_at: a.created_at,
334
+ supported_providers: a.supported_providers ?? ['any'],
335
+ is_public: a.is_public,
336
+ pricing_mode: a.pricing_mode,
337
+ price_per_call_cents: a.price_per_call_cents,
338
+ }));
339
+ }
272
340
  async function fetchLlmKeys(config) {
273
341
  const result = await request(config, 'GET', '/llm-keys/export');
274
342
  return result.keys;
@@ -43,6 +43,8 @@ exports.getResolvedConfig = getResolvedConfig;
43
43
  exports.getConfigPath = getConfigPath;
44
44
  exports.getDefaultFormats = getDefaultFormats;
45
45
  exports.setDefaultFormats = setDefaultFormats;
46
+ exports.getDefaultScope = getDefaultScope;
47
+ exports.setDefaultScope = setDefaultScope;
46
48
  const promises_1 = __importDefault(require("fs/promises"));
47
49
  const path_1 = __importDefault(require("path"));
48
50
  const os_1 = __importDefault(require("os"));
@@ -129,3 +131,12 @@ async function setDefaultFormats(formats) {
129
131
  config.default_formats = formats;
130
132
  await saveConfig(config);
131
133
  }
134
+ async function getDefaultScope() {
135
+ const config = await loadConfig();
136
+ return config.default_scope;
137
+ }
138
+ async function setDefaultScope(scope) {
139
+ const config = await loadConfig();
140
+ config.default_scope = scope;
141
+ await saveConfig(config);
142
+ }
@@ -1,128 +1,150 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.checkServerLlmKeys = checkServerLlmKeys;
4
- exports.checkLocalLlmEnvVars = checkLocalLlmEnvVars;
5
3
  exports.runLlmChecks = runLlmChecks;
6
4
  const config_1 = require("../../config");
7
5
  const api_1 = require("../../api");
8
- // Common LLM provider environment variables
9
- const LLM_ENV_VARS = [
10
- { name: 'OPENAI_API_KEY', provider: 'OpenAI' },
11
- { name: 'ANTHROPIC_API_KEY', provider: 'Anthropic' },
12
- { name: 'GOOGLE_API_KEY', provider: 'Google' },
13
- { name: 'GEMINI_API_KEY', provider: 'Gemini' },
6
+ // All supported LLM providers (single source of truth)
7
+ const PROVIDERS = [
8
+ { id: 'openai', displayName: 'OpenAI', envVars: ['OPENAI_API_KEY'], keyPrefix: 'sk-' },
9
+ { id: 'anthropic', displayName: 'Anthropic', envVars: ['ANTHROPIC_API_KEY'], keyPrefix: 'sk-ant-' },
10
+ { id: 'gemini', displayName: 'Gemini', envVars: ['GEMINI_API_KEY', 'GOOGLE_API_KEY'] },
11
+ { id: 'ollama', displayName: 'Ollama', envVars: ['OLLAMA_HOST'], isEndpoint: true },
14
12
  ];
15
13
  /**
16
- * Check if LLM keys are configured on the server.
14
+ * Get a format hint for a key value, or null if the format looks OK.
15
+ * Hints are informational only — never errors or warnings.
17
16
  */
18
- async function checkServerLlmKeys() {
19
- const config = await (0, config_1.getResolvedConfig)();
20
- if (!config.apiKey) {
21
- return {
22
- category: 'llm',
23
- name: 'server_llm_keys',
24
- status: 'warning',
25
- message: 'Cannot check server LLM keys (not logged in)',
26
- details: { reason: 'no api key' },
27
- };
17
+ function getFormatHint(provider, value) {
18
+ if ('isEndpoint' in provider && provider.isEndpoint)
19
+ return null;
20
+ if (!('keyPrefix' in provider) || !provider.keyPrefix)
21
+ return null;
22
+ const prefix = provider.keyPrefix;
23
+ if (!value.startsWith(prefix)) {
24
+ return `Key doesn't match expected format (${prefix}...)`;
28
25
  }
29
- try {
30
- const keys = await (0, api_1.fetchLlmKeys)(config);
31
- if (keys.length === 0) {
32
- return {
33
- category: 'llm',
34
- name: 'server_llm_keys',
35
- status: 'warning',
36
- message: 'No LLM keys configured on server',
37
- fix: 'Run `orch keys add <provider>` or add keys at orchagent.io/settings',
38
- details: { count: 0, providers: [] },
39
- };
40
- }
41
- const providers = keys.map((k) => k.provider);
42
- // Warn if only one provider configured (no fallback for rate limits)
43
- if (keys.length === 1) {
44
- return {
45
- category: 'llm',
46
- name: 'server_llm_keys',
47
- status: 'warning',
48
- message: `Only 1 LLM provider configured (${providers[0]}). Consider adding a backup for rate limit fallback.`,
49
- fix: 'Run: orchagent keys add <provider>',
50
- details: { count: keys.length, providers },
51
- };
52
- }
53
- return {
54
- category: 'llm',
55
- name: 'server_llm_keys',
56
- status: 'success',
57
- message: `Server LLM keys configured (${providers.join(', ')})`,
58
- details: { count: keys.length, providers },
59
- };
60
- }
61
- catch (err) {
62
- if (err instanceof api_1.ApiError && err.status === 401) {
63
- return {
64
- category: 'llm',
65
- name: 'server_llm_keys',
66
- status: 'warning',
67
- message: 'Cannot check server LLM keys (auth failed)',
68
- details: { error: err.message },
69
- };
26
+ return null;
27
+ }
28
+ /**
29
+ * Gather per-provider status from server keys and local env vars.
30
+ */
31
+ function gatherProviderStatuses(serverProviders) {
32
+ return PROVIDERS.map((provider) => {
33
+ // Server status
34
+ const server = serverProviders === null ? null : serverProviders.includes(provider.id);
35
+ // Local status check each env var
36
+ let local = false;
37
+ let localEnvVar;
38
+ let formatHint;
39
+ for (const envVar of provider.envVars) {
40
+ const value = process.env[envVar];
41
+ if (value) {
42
+ local = true;
43
+ localEnvVar = envVar;
44
+ const hint = getFormatHint(provider, value);
45
+ if (hint)
46
+ formatHint = hint;
47
+ break;
48
+ }
70
49
  }
71
50
  return {
72
- category: 'llm',
73
- name: 'server_llm_keys',
74
- status: 'warning',
75
- message: 'Could not check server LLM keys',
76
- details: { error: err instanceof Error ? err.message : 'unknown error' },
51
+ providerId: provider.id,
52
+ displayName: provider.displayName,
53
+ server,
54
+ local,
55
+ localEnvVar,
56
+ formatHint,
77
57
  };
58
+ });
59
+ }
60
+ /**
61
+ * Build a human-readable location string from server/local status.
62
+ */
63
+ function locationString(status) {
64
+ if (status.server === null) {
65
+ // Server unknown (offline)
66
+ if (status.local)
67
+ return 'Server unknown, local configured';
68
+ return 'Server unknown, not local';
78
69
  }
70
+ if (status.server && status.local)
71
+ return 'Configured (server + local)';
72
+ if (status.server)
73
+ return 'Configured (server)';
74
+ if (status.local)
75
+ return 'Configured (local)';
76
+ return 'Not configured';
79
77
  }
80
78
  /**
81
- * Check if common LLM provider API keys are set in environment.
79
+ * Run all LLM configuration checks with per-provider breakdown.
80
+ *
81
+ * When skipServer is true, server status is null for all providers
82
+ * (shown as "unknown" in output). Use this when the gateway is unreachable.
82
83
  */
83
- async function checkLocalLlmEnvVars() {
84
- const configuredProviders = [];
85
- const details = {};
86
- for (const { name, provider } of LLM_ENV_VARS) {
87
- const isSet = !!process.env[name];
88
- details[name] = isSet;
89
- if (isSet) {
90
- configuredProviders.push(provider);
84
+ async function runLlmChecks(options) {
85
+ let serverProviders = null;
86
+ if (!options?.skipServer) {
87
+ try {
88
+ const config = await (0, config_1.getResolvedConfig)();
89
+ if (config.apiKey) {
90
+ const keys = await (0, api_1.fetchLlmKeys)(config);
91
+ serverProviders = keys.map((k) => k.provider);
92
+ }
93
+ }
94
+ catch (err) {
95
+ // If we can't reach the server, treat as unknown
96
+ if (err instanceof api_1.ApiError && err.status === 401) {
97
+ // Auth failed — server providers unknown
98
+ }
99
+ // Network error or other — server providers unknown
91
100
  }
92
101
  }
93
- if (configuredProviders.length === 0) {
94
- return {
102
+ const statuses = gatherProviderStatuses(serverProviders);
103
+ const results = [];
104
+ // Per-provider results
105
+ for (const status of statuses) {
106
+ const configured = status.server === true || status.local;
107
+ results.push({
95
108
  category: 'llm',
96
- name: 'local_llm_env',
97
- status: 'warning',
98
- message: 'No local LLM API keys found in environment',
99
- fix: 'Set OPENAI_API_KEY, ANTHROPIC_API_KEY, or similar for local runs',
100
- details,
101
- };
109
+ name: `llm_provider_${status.providerId}`,
110
+ status: configured ? 'success' : 'info',
111
+ message: `${status.providerId} ${locationString(status)}`,
112
+ details: {
113
+ providerId: status.providerId,
114
+ displayName: status.displayName,
115
+ server: status.server,
116
+ local: status.local,
117
+ ...(status.localEnvVar && { localEnvVar: status.localEnvVar }),
118
+ ...(status.formatHint && { formatHint: status.formatHint }),
119
+ },
120
+ });
102
121
  }
103
- // Deduplicate (Google and Gemini might both be set)
104
- const uniqueProviders = [...new Set(configuredProviders)];
105
- return {
106
- category: 'llm',
107
- name: 'local_llm_env',
108
- status: 'success',
109
- message: `Local LLM keys found (${uniqueProviders.join(', ')})`,
110
- details,
111
- };
112
- }
113
- /**
114
- * Run all LLM configuration checks.
115
- * If server keys are configured, local keys warning becomes informational.
116
- */
117
- async function runLlmChecks() {
118
- const serverResult = await checkServerLlmKeys();
119
- const localResult = await checkLocalLlmEnvVars();
120
- // If server keys are configured, downgrade local keys warning to info
121
- // Users who only use server-side calls don't need local keys
122
- if (serverResult.status === 'success' && localResult.status === 'warning') {
123
- localResult.status = 'info';
124
- localResult.message = 'No local LLM API keys (using server keys)';
125
- localResult.fix = undefined;
122
+ // Summary result
123
+ const configuredCount = statuses.filter((s) => s.server === true || s.local).length;
124
+ const firstUnconfigured = statuses.find((s) => s.server !== true && !s.local);
125
+ let summaryStatus;
126
+ let summaryMessage;
127
+ let summaryFix;
128
+ if (configuredCount === 0) {
129
+ summaryStatus = 'warning';
130
+ summaryMessage = 'No LLM providers configured';
131
+ summaryFix = firstUnconfigured ? `Run: orch keys add ${firstUnconfigured.providerId}` : undefined;
132
+ }
133
+ else {
134
+ summaryStatus = 'success';
135
+ summaryMessage =
136
+ configuredCount < 2
137
+ ? 'Tip: Multiple providers enable automatic rate limit fallback.'
138
+ : `${configuredCount} providers configured`;
139
+ summaryFix = firstUnconfigured ? `Run: orch keys add ${firstUnconfigured.providerId}` : undefined;
126
140
  }
127
- return [serverResult, localResult];
141
+ results.push({
142
+ category: 'llm',
143
+ name: 'llm_provider_summary',
144
+ status: summaryStatus,
145
+ message: summaryMessage,
146
+ fix: summaryFix,
147
+ details: { configuredCount, totalProviders: PROVIDERS.length },
148
+ });
149
+ return results;
128
150
  }
@@ -19,8 +19,81 @@ const CATEGORY_NAMES = {
19
19
  configuration: 'Configuration',
20
20
  connectivity: 'Connectivity',
21
21
  authentication: 'Authentication',
22
- llm: 'LLM Configuration',
22
+ llm: 'LLM Providers',
23
23
  };
24
+ // Symbol for server-unknown state
25
+ const UNKNOWN_SYMBOL = chalk_1.default.dim('?');
26
+ /**
27
+ * Render the LLM section with per-provider table layout.
28
+ */
29
+ function renderLlmSection(checks, verbose) {
30
+ const providerChecks = checks.filter((c) => c.name.startsWith('llm_provider_') && c.name !== 'llm_provider_summary');
31
+ const summaryCheck = checks.find((c) => c.name === 'llm_provider_summary');
32
+ // Calculate padding for aligned columns
33
+ const maxIdLen = Math.max(...providerChecks.map((c) => {
34
+ const id = c.details?.providerId || '';
35
+ return id.length;
36
+ }));
37
+ for (const check of providerChecks) {
38
+ const id = check.details?.providerId || '';
39
+ const padded = id.padEnd(maxIdLen);
40
+ const serverVal = check.details?.server;
41
+ const localVal = check.details?.local;
42
+ const configured = serverVal === true || localVal;
43
+ // Pick symbol: ✓ for configured, ✗ for not configured, ? for server-unknown
44
+ let symbol;
45
+ if (configured) {
46
+ symbol = SYMBOLS.success;
47
+ }
48
+ else if (serverVal === null) {
49
+ symbol = UNKNOWN_SYMBOL;
50
+ }
51
+ else {
52
+ symbol = SYMBOLS.error;
53
+ }
54
+ // Build location text
55
+ let location;
56
+ if (serverVal === null) {
57
+ location = localVal ? 'Server unknown, local configured' : 'Server unknown, not local';
58
+ }
59
+ else if (serverVal && localVal) {
60
+ location = 'Configured (server + local)';
61
+ }
62
+ else if (serverVal) {
63
+ location = 'Configured (server)';
64
+ }
65
+ else if (localVal) {
66
+ location = 'Configured (local)';
67
+ }
68
+ else {
69
+ location = 'Not configured';
70
+ }
71
+ process.stdout.write(` ${symbol} ${padded} ${location}\n`);
72
+ // Show format hint as dim indented text
73
+ const formatHint = check.details?.formatHint;
74
+ if (formatHint) {
75
+ process.stdout.write(chalk_1.default.dim(` \u26a0 ${formatHint}\n`));
76
+ }
77
+ // Verbose: show details
78
+ if (verbose && check.details) {
79
+ for (const [key, value] of Object.entries(check.details)) {
80
+ if (key === 'providerId' || key === 'displayName' || key === 'formatHint')
81
+ continue;
82
+ const displayValue = typeof value === 'object' ? JSON.stringify(value) : String(value);
83
+ process.stdout.write(chalk_1.default.dim(` ${key}: ${displayValue}\n`));
84
+ }
85
+ }
86
+ }
87
+ // Summary tip/fix
88
+ if (summaryCheck) {
89
+ process.stdout.write('\n');
90
+ const symbol = SYMBOLS[summaryCheck.status] || SYMBOLS.info;
91
+ process.stdout.write(` ${symbol} ${summaryCheck.message}\n`);
92
+ if (summaryCheck.fix) {
93
+ process.stdout.write(chalk_1.default.dim(` \u2192 ${summaryCheck.fix}\n`));
94
+ }
95
+ }
96
+ }
24
97
  /**
25
98
  * Group check results by category.
26
99
  */
@@ -65,18 +138,23 @@ function printHumanOutput(results, summary, verbose) {
65
138
  for (const [category, checks] of groups) {
66
139
  const displayName = CATEGORY_NAMES[category] || category;
67
140
  process.stdout.write(chalk_1.default.bold(`${displayName}\n`));
68
- for (const check of checks) {
69
- const symbol = SYMBOLS[check.status] || SYMBOLS.info;
70
- process.stdout.write(` ${symbol} ${check.message}\n`);
71
- // Show fix suggestion for warnings/errors (not for success/info)
72
- if (check.fix && (check.status === 'warning' || check.status === 'error')) {
73
- process.stdout.write(chalk_1.default.dim(` \u2192 ${check.fix}\n`));
74
- }
75
- // Show details in verbose mode
76
- if (verbose && check.details) {
77
- for (const [key, value] of Object.entries(check.details)) {
78
- const displayValue = typeof value === 'object' ? JSON.stringify(value) : String(value);
79
- process.stdout.write(chalk_1.default.dim(` ${key}: ${displayValue}\n`));
141
+ if (category === 'llm') {
142
+ renderLlmSection(checks, verbose);
143
+ }
144
+ else {
145
+ for (const check of checks) {
146
+ const symbol = SYMBOLS[check.status] || SYMBOLS.info;
147
+ process.stdout.write(` ${symbol} ${check.message}\n`);
148
+ // Show fix suggestion for warnings/errors (not for success/info)
149
+ if (check.fix && (check.status === 'warning' || check.status === 'error')) {
150
+ process.stdout.write(chalk_1.default.dim(` \u2192 ${check.fix}\n`));
151
+ }
152
+ // Show details in verbose mode
153
+ if (verbose && check.details) {
154
+ for (const [key, value] of Object.entries(check.details)) {
155
+ const displayValue = typeof value === 'object' ? JSON.stringify(value) : String(value);
156
+ process.stdout.write(chalk_1.default.dim(` ${key}: ${displayValue}\n`));
157
+ }
80
158
  }
81
159
  }
82
160
  }
@@ -41,18 +41,9 @@ async function runAllChecks() {
41
41
  message: 'Skipped (gateway unreachable)',
42
42
  details: { skipped: true, reason: 'gateway unreachable' },
43
43
  });
44
- results.push({
45
- category: 'llm',
46
- name: 'server_llm_keys',
47
- status: 'warning',
48
- message: 'Skipped (gateway unreachable)',
49
- details: { skipped: true, reason: 'gateway unreachable' },
50
- });
51
- // Still check local LLM env vars since they don't require network
52
- const localLlmCheck = (await (0, llm_1.runLlmChecks)()).find((r) => r.name === 'local_llm_env');
53
- if (localLlmCheck) {
54
- results.push(localLlmCheck);
55
- }
44
+ // LLM checks with server status unknown — local env vars still checked
45
+ const llmResults = await (0, llm_1.runLlmChecks)({ skipServer: true });
46
+ results.push(...llmResults);
56
47
  }
57
48
  return results;
58
49
  }
@@ -11,17 +11,17 @@ const pricing_1 = require("./pricing");
11
11
  function printJson(value) {
12
12
  process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
13
13
  }
14
- function printAgentsTable(agents) {
15
- const table = new cli_table3_1.default({
16
- head: [
17
- chalk_1.default.bold('Agent'),
18
- chalk_1.default.bold('Type'),
19
- chalk_1.default.bold('Providers'),
20
- chalk_1.default.bold('Stars'),
21
- chalk_1.default.bold('Price'),
22
- chalk_1.default.bold('Description'),
23
- ],
24
- });
14
+ function printAgentsTable(agents, options) {
15
+ const head = [
16
+ chalk_1.default.bold('Agent'),
17
+ chalk_1.default.bold('Type'),
18
+ ...(options?.showVisibility ? [chalk_1.default.bold('Visibility')] : []),
19
+ chalk_1.default.bold('Providers'),
20
+ chalk_1.default.bold('Stars'),
21
+ chalk_1.default.bold('Price'),
22
+ chalk_1.default.bold('Description'),
23
+ ];
24
+ const table = new cli_table3_1.default({ head });
25
25
  agents.forEach((agent) => {
26
26
  const fullName = `${agent.org_slug}/${agent.name}`;
27
27
  const type = agent.type || 'code';
@@ -34,7 +34,14 @@ function printAgentsTable(agents) {
34
34
  ? agent.description.slice(0, 27) + '...'
35
35
  : agent.description
36
36
  : '-';
37
- table.push([fullName, type, providers, stars.toString(), priceColored, desc]);
37
+ const visibility = agent.is_public === false
38
+ ? chalk_1.default.yellow('private')
39
+ : chalk_1.default.green('public');
40
+ const row = [fullName, type];
41
+ if (options?.showVisibility)
42
+ row.push(visibility);
43
+ row.push(providers, stars.toString(), priceColored, desc);
44
+ table.push(row);
38
45
  });
39
46
  process.stdout.write(`${table.toString()}\n`);
40
47
  }