unbound-cli 1.16.23 → 1.16.25

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unbound-cli",
3
- "version": "1.16.23",
3
+ "version": "1.16.25",
4
4
  "description": "CLI tool for Unbound - AI Gateway management",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -41,6 +41,12 @@ const DATASETS = {
41
41
  summary: 'MCP calls to servers your org has not sanctioned (blocked or warned).',
42
42
  tableColumns: ['Timestamp', 'User', 'MCP Server', 'MCP Tool', 'Status'],
43
43
  },
44
+ unofficial: {
45
+ key: 'unofficial',
46
+ title: 'Unofficial MCP Usage',
47
+ summary: 'MCP calls blocked because the publisher is not an official one.',
48
+ tableColumns: ['Timestamp', 'User', 'MCP Server', 'MCP Tool', 'Status'],
49
+ },
44
50
  'mcp-servers': {
45
51
  key: 'mcp-servers',
46
52
  title: 'MCP Servers',
@@ -272,6 +278,7 @@ DATASETS
272
278
  terminal ${DATASETS.terminal.summary}
273
279
  mcp-actions ${DATASETS['mcp-actions'].summary}
274
280
  unsanctioned ${DATASETS.unsanctioned.summary}
281
+ unofficial ${DATASETS.unofficial.summary}
275
282
  mcp-servers ${DATASETS['mcp-servers'].summary}
276
283
 
277
284
  Examples:
@@ -326,6 +333,21 @@ Examples:
326
333
  $ unbound analytics tool-use unsanctioned --format csv -o unsanctioned.csv
327
334
  ${FILTER_HELP}${AGENT_HELP}`, { server: true });
328
335
 
336
+ attach(toolUse, 'unofficial', DATASETS.unofficial, `
337
+ Columns:
338
+ Timestamp, User, Email, AI Tool, MCP Server, MCP Tool,
339
+ Status, Sanction Message, User Prompt
340
+
341
+ A narrower slice of unsanctioned: only the calls blocked because the MCP
342
+ publisher is not an official one. No dashboard page shows this on its own,
343
+ so the CLI and the API are the only ways to read it.
344
+
345
+ Examples:
346
+ $ unbound analytics tool-use unofficial
347
+ $ unbound analytics tool-use unofficial --time-window THIRTY_DAYS
348
+ $ unbound analytics tool-use unofficial --format csv -o unofficial.csv
349
+ ${FILTER_HELP}${AGENT_HELP}`, { server: true });
350
+
329
351
  attach(toolUse, 'mcp-servers', DATASETS['mcp-servers'], `
330
352
  Columns:
331
353
  MCP Server, Actions, Average Risk Score
@@ -57,6 +57,36 @@ FILTERS
57
57
  than failing — 'tool_type=CURSOR' returns every tool. An invalid VALUE for a
58
58
  known key IS rejected, with the supported values listed. Run --check first and
59
59
  confirm the count moved.
60
+
61
+ FILTER VALUES
62
+ Most filters take IDs or names. These take a fixed vocabulary:
63
+
64
+ weeks ai-usage kinds only: 4, 8 or 12 (default 8). These cover
65
+ whole weeks, so --last and --start/--end do not apply.
66
+ status ai-usage-users-export: active | silent
67
+ band ai-usage-teams-export: high | mid | low
68
+ trend ai-usage-teams-export: up | down | flat
69
+ enforcement users-export, analytics-tasks-export. Repeatable:
70
+ data:audit, data:block, mcp:audit, mcp:block, mcp:warn,
71
+ mcp:approval, terminal:audit, terminal:block,
72
+ terminal:warn, terminal:approval, skills:inject
73
+ policy users-export, analytics-tasks-export: cmd:<id>, data:<id>,
74
+ finding:<name>, sanction:unsanctioned, sanction:unofficial
75
+ sort_field users-export: total_cost (default), total_tokens,
76
+ task_count, avg_cost_per_task, cache_rate, models_used,
77
+ policy_enforcements
78
+ risk_level discovery-risky-users-export. Repeatable:
79
+ low | medium | high | critical
80
+ An unrecognised value is DROPPED, not rejected, so a typo
81
+ exports every risk level. Confirm with --check.
82
+ autonomy_level discovery-risky-users-export. Repeatable:
83
+ read_only | ask_first | auto_edit | full_auto
84
+ Dropped on a typo, same as risk_level above.
85
+ columns discovery-risky-users-export. Repeatable; omit for the
86
+ default seven. user, email, system_user, device_id,
87
+ last_sync, risk_score, risk_level, risk_factors,
88
+ autonomy_level, tools, tool_count, permission_risk,
89
+ config_file_risk. The file keeps that order whatever you ask for.
60
90
  `;
61
91
 
62
92
  /** `--last 7d` / `--last 24h` -> an absolute window the server understands. */
@@ -201,8 +231,12 @@ Examples:
201
231
  $ unbound download start logs-tasks-export --start 2026-07-01 --end 2026-07-31
202
232
  $ unbound download start users-export --last 30d
203
233
  $ unbound download start analytics-export --filter dataset=terminal-runs --last 7d
234
+ $ unbound download start ai-usage-teams-export --filter weeks=12
204
235
  $ unbound download start logs-export --check --last 90d --json
205
236
 
237
+ The two ai-usage kinds cover whole weeks, not a date range: pass
238
+ '--filter weeks=4|8|12' (default 8). --last and --start/--end do not apply.
239
+
206
240
  Admin or Analytics Viewer. Exports are kept for ${RETENTION_HOURS} hours.
207
241
  ${FILTER_HELP}${AGENT_HELP}`)
208
242
  .action(async (kind, opts) => {
@@ -35,24 +35,44 @@ const KINDS = {
35
35
  'logs-export': {
36
36
  summary: 'Every LLM call: prompt, model, tokens, status, user and task ID.',
37
37
  filters: ['model_ids', 'application_ids', 'status_codes', 'application_owner_email',
38
- 'application_types', 'tool_types'],
38
+ 'application_types', 'tool_types', 'search'],
39
39
  },
40
40
  'logs-tasks-export': {
41
41
  summary: 'One row per task (agent session), with its thread ID.',
42
- filters: ['model_ids', 'tool_types', 'application_owner_email', 'user_ids'],
42
+ filters: ['model_ids', 'tool_types', 'application_owner_email', 'user_ids', 'search'],
43
43
  },
44
44
  'analytics-export': {
45
- summary: 'Tool Use rows. Needs dataset=terminal-runs|mcp-actions|unsanctioned|mcp-servers.',
46
- filters: ['dataset', 'risk_levels', 'command_families', 'server', 'intent_attribution',
47
- 'policy_ids', 'tool_types', 'user_ids'],
45
+ summary: 'Tool Use rows. Needs dataset=terminal-runs|mcp-actions|unsanctioned|unofficial|mcp-servers.',
46
+ filters: ['dataset', 'risk_levels', 'command_families', 'policy_ids', 'policy_actions',
47
+ 'server', 'server_name', 'canonical_group_id', 'grouped', 'intent_attribution',
48
+ 'has_policy_match', 'application_types', 'application_ids', 'application_owner_email',
49
+ 'tool_types', 'user_ids', 'department_names', 'manager_names', 'search',
50
+ 'sort_field', 'sort_order'],
48
51
  },
49
52
  'analytics-tasks-export': {
50
53
  summary: 'Analytics > Tasks, optionally narrowed to one heatmap day.',
51
- filters: ['user_ids', 'tool_types', 'date', 'tz'],
54
+ filters: ['user_ids', 'department_names', 'manager_names', 'tool_types', 'date', 'tz',
55
+ 'enforcement', 'policy'],
52
56
  },
53
57
  'users-export': {
54
58
  summary: 'Analytics > Users: per-user cost, tokens and activity.',
55
- filters: ['user_ids', 'sort_field', 'sort_order'],
59
+ filters: ['user_ids', 'department_names', 'manager_names', 'tool_types', 'search',
60
+ 'enforcement', 'policy', 'sort_field', 'sort_order'],
61
+ },
62
+ 'ai-usage-teams-export': {
63
+ summary: 'Analytics > Usage: weekly AI adoption per team, one column per week.',
64
+ filters: ['weeks', 'department_names', 'manager_names', 'search', 'band', 'trend',
65
+ 'group_by', 'sort_by', 'sort_dir'],
66
+ },
67
+ 'ai-usage-users-export': {
68
+ summary: 'Analytics > Usage: the people inside a team. Needs department_names.',
69
+ filters: ['weeks', 'department_names', 'manager_names', 'search', 'status',
70
+ 'sort_by', 'sort_dir'],
71
+ },
72
+ 'discovery-risky-users-export': {
73
+ summary: 'AI Tools Discovery > Users Risk Leaderboard. Pick your own columns.',
74
+ filters: ['search', 'filter', 'high_risk', 'risk_level', 'autonomy_level', 'tool',
75
+ 'sort_by', 'sort_direction', 'columns'],
56
76
  },
57
77
  };
58
78
 
@@ -48,6 +48,9 @@ test('every dataset names only columns the export actually returns', () => {
48
48
  'Targets', 'User Prompt', 'Risk Score', 'Risk Reason', 'Initiator', 'Matched Policies'],
49
49
  unsanctioned: ['Timestamp', 'User', 'Email', 'AI Tool', 'MCP Server', 'MCP Tool',
50
50
  'Status', 'Sanction Message', 'User Prompt'],
51
+ // The server builds unofficial from the same column set as unsanctioned.
52
+ unofficial: ['Timestamp', 'User', 'Email', 'AI Tool', 'MCP Server', 'MCP Tool',
53
+ 'Status', 'Sanction Message', 'User Prompt'],
51
54
  'mcp-servers': ['MCP Server', 'Actions', 'Average Risk Score'],
52
55
  };
53
56
 
@@ -66,3 +69,16 @@ test('every dataset names only columns the export actually returns', () => {
66
69
  test('--json works when --format was never passed', () => {
67
70
  assert.strictEqual(resolveFormat({ json: true, format: undefined }), 'json');
68
71
  });
72
+
73
+ test('every Tool Use dataset the server has is reachable as a subcommand', () => {
74
+ // 'unofficial' has no dashboard page, so the CLI and the API are the only
75
+ // ways to read it; dropping it here makes it unreachable rather than awkward.
76
+ assert.deepEqual(Object.keys(DATASETS), [
77
+ 'terminal', 'mcp-actions', 'unsanctioned', 'unofficial', 'mcp-servers',
78
+ ]);
79
+ for (const [name, meta] of Object.entries(DATASETS)) {
80
+ assert.ok(meta.key, `${name} needs the server's dataset key`);
81
+ assert.ok(meta.summary && meta.summary.length > 10, `${name} needs a summary`);
82
+ assert.ok(meta.tableColumns.length > 0, `${name} needs table columns`);
83
+ }
84
+ });
@@ -69,6 +69,44 @@ test('every advertised kind names the filters it accepts', () => {
69
69
  }
70
70
  });
71
71
 
72
+ test('every kind the backend serves is listed', () => {
73
+ // Literal list, in the order help prints them. The backend registry cannot be
74
+ // queried, so a kind added there is only discoverable once it is added here
75
+ // too — this is what makes that omission a failing test rather than silence.
76
+ assert.deepEqual(Object.keys(KINDS), [
77
+ 'logs-export',
78
+ 'logs-tasks-export',
79
+ 'analytics-export',
80
+ 'analytics-tasks-export',
81
+ 'users-export',
82
+ 'ai-usage-teams-export',
83
+ 'ai-usage-users-export',
84
+ 'discovery-risky-users-export',
85
+ ]);
86
+ });
87
+
88
+ test('the filters a kind advertises include the ones that change what is exported', () => {
89
+ // An unknown filter key is ignored by the server rather than rejected, so a
90
+ // missing entry here reads to the caller as "this kind cannot be narrowed"
91
+ // and they take a wider file than they asked for.
92
+ const required = {
93
+ 'logs-export': ['search', 'tool_types'],
94
+ 'logs-tasks-export': ['search', 'user_ids'],
95
+ 'analytics-export': ['dataset', 'department_names', 'manager_names', 'search'],
96
+ 'analytics-tasks-export': ['department_names', 'manager_names', 'enforcement', 'policy'],
97
+ 'users-export': ['department_names', 'manager_names', 'tool_types', 'search',
98
+ 'enforcement', 'policy'],
99
+ 'ai-usage-teams-export': ['weeks', 'band', 'trend'],
100
+ 'ai-usage-users-export': ['weeks', 'department_names', 'status'],
101
+ 'discovery-risky-users-export': ['risk_level', 'autonomy_level', 'columns'],
102
+ };
103
+ for (const [kind, filters] of Object.entries(required)) {
104
+ for (const filter of filters) {
105
+ assert.ok(KINDS[kind].filters.includes(filter), `${kind} must advertise ${filter}`);
106
+ }
107
+ }
108
+ });
109
+
72
110
  test('isJobId accepts a UUID and rejects anything else', () => {
73
111
  assert.ok(isJobId('4f2a1c9e-1b2c-4d5e-8f90-a1b2c3d4e5f6'));
74
112
  assert.ok(isJobId('4F2A1C9E-1B2C-4D5E-8F90-A1B2C3D4E5F6'));