securenow 5.3.0 → 5.3.2

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/NPM_README.md CHANGED
@@ -16,6 +16,7 @@ OpenTelemetry instrumentation library for Node.js applications. Send distributed
16
16
 
17
17
  - [Installation](#installation)
18
18
  - [Quick Start](#quick-start)
19
+ - [CLI — Command Line Interface](#cli--command-line-interface)
19
20
  - [Framework-Specific Setup](#framework-specific-setup)
20
21
  - [Express.js](#expressjs)
21
22
  - [Next.js](#nextjs)
@@ -96,6 +97,286 @@ You'll see confirmation in the console:
96
97
 
97
98
  ---
98
99
 
100
+ ## CLI — Command Line Interface
101
+
102
+ The `securenow` CLI gives you full access to the SecureNow platform from the terminal — no browser required for day-to-day workflows. Zero additional dependencies.
103
+
104
+ ### Getting Started
105
+
106
+ ```bash
107
+ # Log in (opens browser for OAuth)
108
+ npx securenow login
109
+
110
+ # Or use a token for CI/headless environments
111
+ npx securenow login --token <YOUR_JWT>
112
+
113
+ # Check who you're logged in as
114
+ npx securenow whoami
115
+ ```
116
+
117
+ ### Managing Applications
118
+
119
+ ```bash
120
+ # List all applications
121
+ npx securenow apps
122
+
123
+ # Create a new application
124
+ npx securenow apps create my-production-app --hosts api.example.com,app.example.com
125
+
126
+ # Get application details (including the app key)
127
+ npx securenow apps info <app-id>
128
+
129
+ # Set a default app so you don't need --app on every command
130
+ npx securenow apps default <app-key>
131
+
132
+ # Delete an application
133
+ npx securenow apps delete <app-id> --force
134
+ ```
135
+
136
+ ### Viewing Traces
137
+
138
+ ```bash
139
+ # List recent traces (uses default app, or specify --app)
140
+ npx securenow traces
141
+ npx securenow traces --app <key> --limit 50
142
+
143
+ # Show detailed spans for a trace
144
+ npx securenow traces show <traceId>
145
+
146
+ # AI-powered security analysis of a trace
147
+ npx securenow traces analyze <traceId>
148
+ ```
149
+
150
+ ### Viewing Logs
151
+
152
+ ```bash
153
+ # List recent logs
154
+ npx securenow logs
155
+ npx securenow logs --app <key> --minutes 30 --level error
156
+
157
+ # Show logs for a specific trace
158
+ npx securenow logs trace <traceId>
159
+ ```
160
+
161
+ ### Security Issues
162
+
163
+ ```bash
164
+ # List all issues
165
+ npx securenow issues
166
+ npx securenow issues --status open
167
+
168
+ # Show issue details with AI analysis
169
+ npx securenow issues show <issue-id>
170
+
171
+ # Resolve an issue
172
+ npx securenow issues resolve <issue-id>
173
+ ```
174
+
175
+ ### Notifications
176
+
177
+ ```bash
178
+ # List notifications
179
+ npx securenow notifications
180
+
181
+ # Check unread count
182
+ npx securenow notifications unread
183
+
184
+ # Mark as read
185
+ npx securenow notifications read <id>
186
+ npx securenow notifications read-all
187
+ ```
188
+
189
+ ### Alerting
190
+
191
+ ```bash
192
+ # View alert rules, channels, and history
193
+ npx securenow alerts rules
194
+ npx securenow alerts channels
195
+ npx securenow alerts history --limit 20
196
+ ```
197
+
198
+ ### IP Intelligence & Blocklist
199
+
200
+ ```bash
201
+ # Look up any IP — geo, abuse score, verdict, risk factors
202
+ npx securenow ip 203.0.113.42
203
+
204
+ # Show traces from a specific IP
205
+ npx securenow ip traces 203.0.113.42
206
+
207
+ # Manage the blocklist
208
+ npx securenow blocklist
209
+ npx securenow blocklist add 203.0.113.42 --reason "Brute force scanner"
210
+ npx securenow blocklist remove <id>
211
+ npx securenow blocklist stats
212
+
213
+ # Manage trusted IPs
214
+ npx securenow trusted
215
+ npx securenow trusted add 10.0.0.1 --label "Office VPN"
216
+ npx securenow trusted remove <id>
217
+ ```
218
+
219
+ ### Forensic Queries
220
+
221
+ Ask questions in plain English — the AI translates them to SQL and runs them against your data.
222
+
223
+ ```bash
224
+ # Run a forensic query
225
+ npx securenow forensics "show top 10 attacking IPs in the last hour"
226
+ npx securenow forensics "which endpoints had 5xx errors today"
227
+
228
+ # Browse the saved query library
229
+ npx securenow forensics library
230
+ ```
231
+
232
+ ### API Map
233
+
234
+ ```bash
235
+ # View all discovered API endpoints
236
+ npx securenow api-map
237
+
238
+ # API map statistics
239
+ npx securenow api-map stats
240
+ ```
241
+
242
+ ### Analytics & Dashboard
243
+
244
+ ```bash
245
+ # Response code analytics
246
+ npx securenow analytics --app <key>
247
+
248
+ # Full dashboard overview (apps, protection status, unread alerts)
249
+ npx securenow status
250
+ ```
251
+
252
+ ### Instances
253
+
254
+ ```bash
255
+ # List ClickHouse instances
256
+ npx securenow instances
257
+
258
+ # Test an instance connection
259
+ npx securenow instances test <id>
260
+ ```
261
+
262
+ ### Configuration
263
+
264
+ ```bash
265
+ # View all config
266
+ npx securenow config get
267
+
268
+ # Set values
269
+ npx securenow config set apiUrl https://custom-api.example.com
270
+ npx securenow config set defaultApp <app-key>
271
+ npx securenow config set format json
272
+
273
+ # Show config file paths
274
+ npx securenow config path
275
+ ```
276
+
277
+ Config files are stored in `~/.securenow/`:
278
+
279
+ | File | Description |
280
+ |------|-------------|
281
+ | `config.json` | API URL, default app, output format |
282
+ | `credentials.json` | Auth token (file permissions: 0600) |
283
+
284
+ ### Initialize Instrumentation
285
+
286
+ ```bash
287
+ # Interactive setup — creates instrumentation files for Next.js
288
+ npx securenow init
289
+ npx securenow init --ts --src --force
290
+ ```
291
+
292
+ ### Global Flags
293
+
294
+ Every command supports these flags:
295
+
296
+ | Flag | Short | Description |
297
+ |------|-------|-------------|
298
+ | `--json` | `-j` | Output as JSON for scripting and CI/CD |
299
+ | `--help` | | Show help for the command |
300
+ | `--app <key>` | | Override the default application key |
301
+
302
+ ### Environment Variables
303
+
304
+ | Variable | Description |
305
+ |----------|-------------|
306
+ | `SECURENOW_API_URL` | Override the API base URL |
307
+ | `SECURENOW_DEBUG` | Show stack traces on errors |
308
+ | `NO_COLOR` | Disable colored output |
309
+
310
+ ### CI/CD Integration
311
+
312
+ ```bash
313
+ # Authenticate with a token in CI
314
+ npx securenow login --token $SECURENOW_TOKEN
315
+
316
+ # Use --json for machine-readable output
317
+ npx securenow issues --json | jq '.[] | select(.severity == "critical")'
318
+
319
+ # Check for critical issues in a pipeline
320
+ ISSUES=$(npx securenow issues --json --status open)
321
+ CRITICAL=$(echo "$ISSUES" | jq '[.[] | select(.severity == "critical")] | length')
322
+ if [ "$CRITICAL" -gt "0" ]; then
323
+ echo "Found $CRITICAL critical issues!"
324
+ exit 1
325
+ fi
326
+ ```
327
+
328
+ ### Complete Command Reference
329
+
330
+ | Category | Command | Description |
331
+ |----------|---------|-------------|
332
+ | **Auth** | `login` | Authenticate via browser or `--token` |
333
+ | | `logout` | Clear credentials |
334
+ | | `whoami` | Show session info |
335
+ | **Apps** | `apps` | List applications |
336
+ | | `apps create <name>` | Create application |
337
+ | | `apps info <id>` | Application details |
338
+ | | `apps delete <id>` | Delete application |
339
+ | | `apps default <key>` | Set default app |
340
+ | **Observe** | `traces` | List traces |
341
+ | | `traces show <id>` | Trace details |
342
+ | | `traces analyze <id>` | AI trace analysis |
343
+ | | `logs` | List logs |
344
+ | | `logs trace <id>` | Logs for a trace |
345
+ | | `analytics` | Response analytics |
346
+ | | `status` | Dashboard overview |
347
+ | **Detect** | `issues` | List issues |
348
+ | | `issues show <id>` | Issue details |
349
+ | | `issues resolve <id>` | Resolve issue |
350
+ | | `notifications` | List notifications |
351
+ | | `notifications unread` | Unread count |
352
+ | | `notifications read <id>` | Mark read |
353
+ | | `notifications read-all` | Mark all read |
354
+ | | `alerts rules` | Alert rules |
355
+ | | `alerts channels` | Alert channels |
356
+ | | `alerts history` | Alert history |
357
+ | **Investigate** | `ip <addr>` | IP intelligence |
358
+ | | `ip traces <addr>` | Traces from IP |
359
+ | | `forensics "<query>"` | NL forensic query |
360
+ | | `forensics library` | Saved queries |
361
+ | | `api-map` | API endpoints |
362
+ | | `api-map stats` | API stats |
363
+ | **Remediate** | `blocklist` | Blocked IPs |
364
+ | | `blocklist add <ip>` | Block IP |
365
+ | | `blocklist remove <id>` | Unblock IP |
366
+ | | `blocklist stats` | Block stats |
367
+ | | `trusted` | Trusted IPs |
368
+ | | `trusted add <ip>` | Add trusted IP |
369
+ | | `trusted remove <id>` | Remove trusted |
370
+ | **Settings** | `instances` | List instances |
371
+ | | `instances test <id>` | Test connection |
372
+ | | `config get` | Show config |
373
+ | | `config set <k> <v>` | Set config value |
374
+ | | `config path` | Config file paths |
375
+ | | `init` | Setup instrumentation |
376
+ | | `version` | Show version |
377
+
378
+ ---
379
+
99
380
  ## Framework-Specific Setup
100
381
 
101
382
  ### Express.js
package/README.md CHANGED
@@ -36,7 +36,37 @@ SECURENOW_INSTANCE=http://your-otlp-collector:4318
36
36
  npx securenow init
37
37
  ```
38
38
 
39
- **Done!** 🎉 See [Next.js Complete Guide](./docs/NEXTJS-GUIDE.md) for details.
39
+ **Done!** See [Next.js Complete Guide](./docs/NEXTJS-GUIDE.md) for details.
40
+
41
+ ---
42
+
43
+ ### CLI — Manage Everything from the Terminal
44
+
45
+ ```bash
46
+ # Authenticate
47
+ npx securenow login
48
+
49
+ # Create an app and get the key
50
+ npx securenow apps create my-app
51
+
52
+ # Set it as default so you don't need --app every time
53
+ npx securenow config set defaultApp <key>
54
+
55
+ # View traces, logs, issues
56
+ npx securenow traces
57
+ npx securenow logs
58
+ npx securenow issues
59
+
60
+ # IP intelligence, forensic queries, blocklist
61
+ npx securenow ip 1.2.3.4
62
+ npx securenow forensics "show top attacking IPs in the last hour"
63
+ npx securenow blocklist add 1.2.3.4 --reason "scanner"
64
+
65
+ # Full dashboard overview
66
+ npx securenow status
67
+ ```
68
+
69
+ Run `npx securenow help` for all commands. See the [CLI Reference](#cli-reference) below.
40
70
 
41
71
  ---
42
72
 
@@ -173,7 +203,113 @@ SecureNow automatically instruments:
173
203
 
174
204
  ---
175
205
 
176
- ## 🆘 Support
206
+ ## CLI Reference
207
+
208
+ After installing the package, the `securenow` CLI is available via `npx securenow` or globally after `npm install -g securenow`.
209
+
210
+ ### Authentication
211
+
212
+ | Command | Description |
213
+ |---------|-------------|
214
+ | `securenow login` | Log in via browser (opens OAuth flow) |
215
+ | `securenow login --token <TOKEN>` | Log in with a token (for CI/headless) |
216
+ | `securenow logout` | Clear stored credentials |
217
+ | `securenow whoami` | Show current session info |
218
+
219
+ ### Applications
220
+
221
+ | Command | Description |
222
+ |---------|-------------|
223
+ | `securenow apps` | List all applications |
224
+ | `securenow apps create <name>` | Create app and get the app key |
225
+ | `securenow apps info <id>` | Show application details |
226
+ | `securenow apps delete <id>` | Delete an application |
227
+ | `securenow apps default <key>` | Set default app for all commands |
228
+
229
+ ### Observability
230
+
231
+ | Command | Description |
232
+ |---------|-------------|
233
+ | `securenow traces --app <key>` | List recent traces |
234
+ | `securenow traces show <traceId>` | Show trace spans |
235
+ | `securenow traces analyze <traceId>` | AI security analysis of a trace |
236
+ | `securenow logs --app <key>` | View logs (with `--minutes`, `--level`) |
237
+ | `securenow logs trace <traceId>` | View logs for a specific trace |
238
+ | `securenow analytics` | Response code analytics overview |
239
+ | `securenow status` | Full dashboard summary |
240
+
241
+ ### Detect & Respond
242
+
243
+ | Command | Description |
244
+ |---------|-------------|
245
+ | `securenow issues` | List security issues |
246
+ | `securenow issues show <id>` | Show issue details and AI analysis |
247
+ | `securenow issues resolve <id>` | Mark an issue as resolved |
248
+ | `securenow notifications` | List notifications |
249
+ | `securenow notifications unread` | Show unread count |
250
+ | `securenow notifications read <id>` | Mark notification as read |
251
+ | `securenow notifications read-all` | Mark all as read |
252
+ | `securenow alerts rules` | List alert rules |
253
+ | `securenow alerts channels` | List alert channels |
254
+ | `securenow alerts history` | View alert history |
255
+
256
+ ### Investigate
257
+
258
+ | Command | Description |
259
+ |---------|-------------|
260
+ | `securenow ip <address>` | IP intelligence lookup (geo, abuse score, verdict) |
261
+ | `securenow ip traces <address>` | Show traces originating from an IP |
262
+ | `securenow forensics "<query>"` | Natural language forensic query (NL to SQL) |
263
+ | `securenow forensics library` | View saved query library |
264
+ | `securenow api-map` | View discovered API endpoints |
265
+ | `securenow api-map stats` | API map statistics |
266
+
267
+ ### Remediation
268
+
269
+ | Command | Description |
270
+ |---------|-------------|
271
+ | `securenow blocklist` | List blocked IPs |
272
+ | `securenow blocklist add <ip>` | Block an IP (`--reason <reason>`) |
273
+ | `securenow blocklist remove <id>` | Remove from blocklist |
274
+ | `securenow blocklist stats` | Blocklist statistics |
275
+ | `securenow trusted` | List trusted IPs |
276
+ | `securenow trusted add <ip>` | Add trusted IP (`--label <label>`) |
277
+ | `securenow trusted remove <id>` | Remove trusted IP |
278
+
279
+ ### Settings
280
+
281
+ | Command | Description |
282
+ |---------|-------------|
283
+ | `securenow instances` | List ClickHouse instances |
284
+ | `securenow instances test <id>` | Test instance connection |
285
+ | `securenow config get` | Show all config values |
286
+ | `securenow config set <key> <value>` | Set a config value |
287
+ | `securenow config path` | Show config file locations |
288
+ | `securenow init` | Initialize instrumentation files |
289
+ | `securenow version` | Show CLI version |
290
+
291
+ ### Global Flags
292
+
293
+ | Flag | Description |
294
+ |------|-------------|
295
+ | `--json` | Output as JSON (works on every command) |
296
+ | `--help` | Show help for any command |
297
+ | `--app <key>` | Specify app key (or set default with `config set defaultApp`) |
298
+
299
+ ### Configuration
300
+
301
+ Credentials and settings are stored in `~/.securenow/`:
302
+
303
+ | File | Purpose |
304
+ |------|---------|
305
+ | `~/.securenow/config.json` | API URL, default app, preferences |
306
+ | `~/.securenow/credentials.json` | Auth token (restricted permissions) |
307
+
308
+ Override the API URL with `securenow config set apiUrl <url>` or the `SECURENOW_API_URL` environment variable.
309
+
310
+ ---
311
+
312
+ ## Support
177
313
 
178
314
  - **Website:** [securenow.ai](http://securenow.ai/)
179
315
  - **Issues:** Report bugs and request features
@@ -181,6 +317,6 @@ SecureNow automatically instruments:
181
317
 
182
318
  ---
183
319
 
184
- ## 📄 License
320
+ ## License
185
321
 
186
322
  ISC
package/cli/apps.js CHANGED
@@ -9,7 +9,8 @@ async function list(args, flags) {
9
9
  const s = ui.spinner('Fetching applications');
10
10
 
11
11
  try {
12
- const apps = await api.get('/applications');
12
+ const data = await api.get('/applications');
13
+ const apps = data.applications || [];
13
14
  s.stop(`Found ${apps.length} application${apps.length !== 1 ? 's' : ''}`);
14
15
  console.log('');
15
16
 
@@ -101,7 +102,8 @@ async function info(args, flags) {
101
102
 
102
103
  const s = ui.spinner('Fetching application details');
103
104
  try {
104
- const app = await api.get(`/applications/${id}`);
105
+ const data = await api.get(`/applications/${id}`);
106
+ const app = data.application || data;
105
107
  s.stop('Application details loaded');
106
108
 
107
109
  if (flags.json) {
package/cli/client.js CHANGED
@@ -60,7 +60,9 @@ function request(method, endpoint, { body, query, token, raw } = {}) {
60
60
  }
61
61
  if (res.statusCode >= 400) {
62
62
  const msg = parsed?.error || parsed?.message || `Request failed (HTTP ${res.statusCode})`;
63
- reject(new CLIError(msg, res.statusCode));
63
+ const details = parsed?.details || parsed?.unauthorizedKeys;
64
+ const err = new CLIError(details ? `${msg} — ${details}` : msg, res.statusCode);
65
+ reject(err);
64
66
  return;
65
67
  }
66
68
 
package/cli/monitor.js CHANGED
@@ -21,14 +21,14 @@ async function tracesList(args, flags) {
21
21
  const s = ui.spinner('Fetching traces');
22
22
  try {
23
23
  const query = {
24
- serviceName: appKey,
24
+ appKeys: appKey,
25
25
  limit: flags.limit || 20,
26
26
  };
27
- if (flags.start) query.start = flags.start;
28
- if (flags.end) query.end = flags.end;
27
+ if (flags.start) query.from = flags.start;
28
+ if (flags.end) query.to = flags.end;
29
29
 
30
- const data = await api.get('/traces/recent', { query });
31
- const traces = Array.isArray(data) ? data : data.traces || [];
30
+ const data = await api.get('/traces', { query });
31
+ const traces = data.traces || [];
32
32
  s.stop(`Found ${traces.length} trace${traces.length !== 1 ? 's' : ''}`);
33
33
 
34
34
  if (flags.json) { ui.json(traces); return; }
@@ -37,7 +37,7 @@ async function tracesList(args, flags) {
37
37
  const rows = traces.map(t => [
38
38
  ui.c.dim(ui.truncate(t.traceID || t.traceId || t._id, 16)),
39
39
  t.operationName || t.name || t.serviceName || '—',
40
- ui.httpStatusColor(t.statusCode || t.httpStatusCode || '—'),
40
+ ui.httpStatusColor(t.statusCode || t.httpStatusCode || t.responseStatusCode || '—'),
41
41
  ui.durationColor(t.durationNano ? t.durationNano / 1e6 : t.duration),
42
42
  t.httpMethod || t.method || '—',
43
43
  ui.truncate(t.httpUrl || t.url || t.httpRoute || '', 40),
@@ -62,37 +62,31 @@ async function tracesShow(args, flags) {
62
62
 
63
63
  const s = ui.spinner('Fetching trace details');
64
64
  try {
65
- const data = await api.get(`/traces/${traceId}`);
65
+ const appKey = resolveApp(flags);
66
+ const traceQuery = appKey ? { appKeys: appKey } : {};
67
+ const data = await api.get(`/traces/${traceId}`, { query: traceQuery });
66
68
  s.stop('Trace loaded');
67
69
 
68
70
  if (flags.json) { ui.json(data); return; }
69
71
 
70
- const trace = data.trace || data;
71
- console.log('');
72
- ui.heading(`Trace ${traceId}`);
72
+ const spans = data.spans || [];
73
73
  console.log('');
74
+ ui.heading(`Trace ${data.traceId || traceId}`);
74
75
 
75
- if (trace.spans && trace.spans.length) {
76
- ui.subheading(`Spans (${trace.spans.length})`);
76
+ if (spans.length) {
77
+ ui.subheading(`Spans (${spans.length})`);
77
78
  console.log('');
78
- const rows = trace.spans.map(span => [
79
+ const rows = spans.map(span => [
79
80
  ui.c.dim(ui.truncate(span.spanID || span.spanId, 16)),
80
81
  span.operationName || span.name || '—',
81
- ui.httpStatusColor(span.statusCode || '—'),
82
+ ui.httpStatusColor(span.statusCode || span.responseStatusCode || '—'),
82
83
  ui.durationColor(span.durationNano ? span.durationNano / 1e6 : span.duration),
83
84
  span.kind || '—',
84
85
  ]);
85
86
  ui.table(['Span ID', 'Operation', 'Status', 'Duration', 'Kind'], rows);
86
- }
87
-
88
- if (trace.rootSpan || trace.serviceName) {
87
+ } else {
89
88
  console.log('');
90
- ui.keyValue([
91
- ['Service', trace.serviceName || '—'],
92
- ['Root Operation', trace.rootOperationName || trace.rootSpan?.operationName || '—'],
93
- ['Duration', trace.durationMs ? `${trace.durationMs}ms` : '—'],
94
- ['Timestamp', trace.startTime ? new Date(trace.startTime).toLocaleString() : '—'],
95
- ]);
89
+ ui.info('No spans found for this trace.');
96
90
  }
97
91
  console.log('');
98
92
  } catch (err) {
@@ -116,11 +110,39 @@ async function tracesAnalyze(args, flags) {
116
110
 
117
111
  if (flags.json) { ui.json(result); return; }
118
112
 
113
+ const analysis = result.analysis;
119
114
  console.log('');
120
115
  ui.heading('AI Trace Analysis');
121
116
  console.log('');
122
- console.log(result.analysis || result.message || JSON.stringify(result, null, 2));
123
- console.log('');
117
+
118
+ if (typeof analysis === 'object' && analysis !== null) {
119
+ if (analysis.summary) {
120
+ ui.subheading('Summary');
121
+ console.log(`\n ${analysis.summary}\n`);
122
+ }
123
+ if (analysis.riskLevel) {
124
+ console.log(` ${ui.c.bold('Risk Level:')} ${ui.statusBadge(analysis.riskLevel)}\n`);
125
+ }
126
+ if (analysis.securityIssues?.length) {
127
+ ui.subheading('Security Issues');
128
+ console.log('');
129
+ analysis.securityIssues.forEach((issue, i) => {
130
+ console.log(` ${i + 1}. ${typeof issue === 'string' ? issue : issue.description || JSON.stringify(issue)}`);
131
+ });
132
+ console.log('');
133
+ }
134
+ if (analysis.recommendations?.length) {
135
+ ui.subheading('Recommendations');
136
+ console.log('');
137
+ analysis.recommendations.forEach((rec, i) => {
138
+ console.log(` ${i + 1}. ${typeof rec === 'string' ? rec : rec.description || JSON.stringify(rec)}`);
139
+ });
140
+ console.log('');
141
+ }
142
+ } else {
143
+ console.log(analysis || JSON.stringify(result, null, 2));
144
+ console.log('');
145
+ }
124
146
  } catch (err) {
125
147
  s.fail('Analysis failed');
126
148
  throw err;
@@ -142,15 +164,15 @@ async function logsList(args, flags) {
142
164
  const minutes = parseInt(flags.minutes || '60', 10);
143
165
  const now = Date.now();
144
166
  const query = {
145
- serviceName: appKey,
167
+ appKeys: appKey,
146
168
  limit: flags.limit || 50,
147
- start: flags.start || new Date(now - minutes * 60 * 1000).toISOString(),
148
- end: flags.end || new Date(now).toISOString(),
169
+ from: flags.start || new Date(now - minutes * 60 * 1000).toISOString(),
170
+ to: flags.end || new Date(now).toISOString(),
149
171
  };
150
- if (flags.level) query.level = flags.level;
172
+ if (flags.level) query.severity = flags.level;
151
173
 
152
174
  const data = await api.get('/logs', { query });
153
- const logs = Array.isArray(data) ? data : data.logs || [];
175
+ const logs = data.logs || [];
154
176
  s.stop(`Found ${logs.length} log${logs.length !== 1 ? 's' : ''}`);
155
177
 
156
178
  if (flags.json) { ui.json(logs); return; }
@@ -190,7 +212,7 @@ async function logsTrace(args, flags) {
190
212
  const s = ui.spinner('Fetching logs for trace');
191
213
  try {
192
214
  const data = await api.get(`/logs/trace/${traceId}`);
193
- const logs = Array.isArray(data) ? data : data.logs || [];
215
+ const logs = data.logs || [];
194
216
  s.stop(`Found ${logs.length} log${logs.length !== 1 ? 's' : ''}`);
195
217
 
196
218
  if (flags.json) { ui.json(logs); return; }
@@ -225,10 +247,10 @@ async function issuesList(args, flags) {
225
247
  if (flags.status) query.status = flags.status;
226
248
 
227
249
  const data = await api.get('/issues', { query });
228
- const issues = Array.isArray(data) ? data : data.issues || [];
250
+ const issues = data.issues || [];
229
251
  s.stop(`Found ${issues.length} issue${issues.length !== 1 ? 's' : ''}`);
230
252
 
231
- if (flags.json) { ui.json(issues); return; }
253
+ if (flags.json) { ui.json(data); return; }
232
254
 
233
255
  console.log('');
234
256
  const rows = issues.map(i => [
@@ -242,6 +264,9 @@ async function issuesList(args, flags) {
242
264
  ]);
243
265
 
244
266
  ui.table(['ID', 'Severity', 'Status', 'Title', 'App', 'Count', 'Last Seen'], rows);
267
+ if (data.total != null) {
268
+ console.log(ui.c.dim(` Total: ${data.total}`));
269
+ }
245
270
  console.log('');
246
271
  } catch (err) {
247
272
  s.fail('Failed to fetch issues');
@@ -259,7 +284,8 @@ async function issuesShow(args, flags) {
259
284
 
260
285
  const s = ui.spinner('Fetching issue');
261
286
  try {
262
- const issue = await api.get(`/issues/${id}`);
287
+ const data = await api.get(`/issues/${id}`);
288
+ const issue = data.issue || data;
263
289
  s.stop('Issue loaded');
264
290
 
265
291
  if (flags.json) { ui.json(issue); return; }
@@ -320,8 +346,9 @@ async function notificationsList(args, flags) {
320
346
  try {
321
347
  const query = { limit: flags.limit || 20, page: flags.page || 1 };
322
348
  const data = await api.get('/notifications', { query });
323
- const notifications = Array.isArray(data) ? data : data.notifications || data.data || [];
324
- s.stop(`Found ${notifications.length} notification${notifications.length !== 1 ? 's' : ''}`);
349
+ const notifications = data.notifications || [];
350
+ const pagination = data.pagination;
351
+ s.stop(`Found ${notifications.length} notification${notifications.length !== 1 ? 's' : ''}${pagination ? ` (page ${pagination.page}/${pagination.totalPages})` : ''}`);
325
352
 
326
353
  if (flags.json) { ui.json(data); return; }
327
354
 
@@ -353,7 +380,7 @@ async function notificationsRead(args, flags) {
353
380
 
354
381
  const s = ui.spinner('Marking as read');
355
382
  try {
356
- await api.put(`/notifications/${id}`, { read: true });
383
+ await api.put(`/notifications/${id}/read`);
357
384
  s.stop('Notification marked as read');
358
385
  } catch (err) {
359
386
  s.fail('Failed to mark notification');
@@ -377,7 +404,7 @@ async function notificationsUnread() {
377
404
  requireAuth();
378
405
  try {
379
406
  const data = await api.get('/notifications/unread-count');
380
- const count = data.count ?? data.unreadCount ?? data;
407
+ const count = data.count ?? 0;
381
408
  console.log(`\n ${ui.c.bold(String(count))} unread notification${count !== 1 ? 's' : ''}\n`);
382
409
  } catch (err) {
383
410
  throw err;
@@ -390,11 +417,12 @@ async function status(args, flags) {
390
417
  requireAuth();
391
418
  const s = ui.spinner('Fetching dashboard overview');
392
419
  try {
393
- const [apps, unread] = await Promise.all([
420
+ const [appsData, unreadData] = await Promise.all([
394
421
  api.get('/applications'),
395
422
  api.get('/notifications/unread-count').catch(() => ({ count: 0 })),
396
423
  ]);
397
424
 
425
+ const apps = appsData.applications || [];
398
426
  s.stop('Dashboard loaded');
399
427
 
400
428
  console.log('');
@@ -403,7 +431,7 @@ async function status(args, flags) {
403
431
 
404
432
  ui.keyValue([
405
433
  ['Applications', String(apps.length)],
406
- ['Unread Alerts', String(unread.count ?? unread.unreadCount ?? 0)],
434
+ ['Unread Alerts', String(unreadData.count ?? 0)],
407
435
  ]);
408
436
 
409
437
  if (apps.length > 0) {
@@ -420,16 +448,18 @@ async function status(args, flags) {
420
448
  const appKey = resolveApp(flags);
421
449
  if (appKey) {
422
450
  try {
423
- const protectionData = await api.get('/applications/protection-status', { query: { serviceName: appKey } });
424
- if (protectionData) {
425
- ui.subheading(`Protection Status (${appKey})`);
451
+ const protectionData = await api.get('/applications/protection-status');
452
+ const statuses = protectionData.statuses;
453
+ if (statuses && Object.keys(statuses).length > 0) {
454
+ ui.subheading('Protection Status');
426
455
  console.log('');
427
- const status = protectionData.status || protectionData;
428
- if (typeof status === 'object') {
429
- ui.keyValue(Object.entries(status).map(([k, v]) => [k, String(v)]));
430
- } else {
431
- console.log(` ${status}`);
432
- }
456
+ const rows = Object.entries(statuses).map(([id, s]) => [
457
+ ui.c.dim(ui.truncate(id, 12)),
458
+ s.protected ? ui.c.green('● protected') : ui.c.red('○ unprotected'),
459
+ String(s.traceCount || 0),
460
+ s.lastTrace ? ui.timeAgo(s.lastTrace) : ui.c.dim('—'),
461
+ ]);
462
+ ui.table(['App ID', 'Status', 'Traces (15m)', 'Last Trace'], rows);
433
463
  }
434
464
  } catch {}
435
465
  }
package/cli/security.js CHANGED
@@ -15,7 +15,7 @@ async function alertRulesList(args, flags) {
15
15
  const s = ui.spinner('Fetching alert rules');
16
16
  try {
17
17
  const data = await api.get('/alert-rules');
18
- const rules = Array.isArray(data) ? data : data.rules || [];
18
+ const rules = data.alertRules || [];
19
19
  s.stop(`Found ${rules.length} rule${rules.length !== 1 ? 's' : ''}`);
20
20
 
21
21
  if (flags.json) { ui.json(rules); return; }
@@ -44,7 +44,7 @@ async function alertChannelsList(args, flags) {
44
44
  const s = ui.spinner('Fetching alert channels');
45
45
  try {
46
46
  const data = await api.get('/alert-channels');
47
- const channels = Array.isArray(data) ? data : data.channels || [];
47
+ const channels = data.alertChannels || [];
48
48
  s.stop(`Found ${channels.length} channel${channels.length !== 1 ? 's' : ''}`);
49
49
 
50
50
  if (flags.json) { ui.json(channels); return; }
@@ -73,10 +73,10 @@ async function alertHistoryList(args, flags) {
73
73
  try {
74
74
  const query = { limit: flags.limit || 20 };
75
75
  const data = await api.get('/alert-history', { query });
76
- const history = Array.isArray(data) ? data : data.alerts || data.history || [];
77
- s.stop(`Found ${history.length} alert${history.length !== 1 ? 's' : ''}`);
76
+ const history = data.alerts || [];
77
+ s.stop(`Found ${history.length} alert${history.length !== 1 ? 's' : ''}${data.totalItems ? ` (${data.totalItems} total)` : ''}`);
78
78
 
79
- if (flags.json) { ui.json(history); return; }
79
+ if (flags.json) { ui.json(data); return; }
80
80
 
81
81
  console.log('');
82
82
  const rows = history.map(h => [
@@ -102,10 +102,10 @@ async function blocklistList(args, flags) {
102
102
  const s = ui.spinner('Fetching blocklist');
103
103
  try {
104
104
  const data = await api.get('/blocklist');
105
- const items = Array.isArray(data) ? data : data.blocklist || data.items || [];
106
- s.stop(`Found ${items.length} blocked IP${items.length !== 1 ? 's' : ''}`);
105
+ const items = data.blockedIps || [];
106
+ s.stop(`Found ${items.length} blocked IP${items.length !== 1 ? 's' : ''}${data.total ? ` (${data.total} total)` : ''}`);
107
107
 
108
- if (flags.json) { ui.json(items); return; }
108
+ if (flags.json) { ui.json(data); return; }
109
109
 
110
110
  console.log('');
111
111
  const rows = items.map(b => [
@@ -174,7 +174,8 @@ async function blocklistStats(args, flags) {
174
174
  requireAuth();
175
175
  const s = ui.spinner('Fetching blocklist stats');
176
176
  try {
177
- const stats = await api.get('/blocklist/stats');
177
+ const data = await api.get('/blocklist/stats');
178
+ const stats = data.stats || data;
178
179
  s.stop('Stats loaded');
179
180
 
180
181
  if (flags.json) { ui.json(stats); return; }
@@ -182,8 +183,13 @@ async function blocklistStats(args, flags) {
182
183
  console.log('');
183
184
  ui.heading('Blocklist Statistics');
184
185
  console.log('');
185
- const pairs = Object.entries(stats).map(([k, v]) => [k, String(v)]);
186
- ui.keyValue(pairs);
186
+ ui.keyValue([
187
+ ['Total Active', String(stats.totalActive ?? '—')],
188
+ ['Total Removed', String(stats.totalRemoved ?? '—')],
189
+ ['Manual Blocks', String(stats.manualCount ?? '—')],
190
+ ['Automation Blocks', String(stats.automationCount ?? '—')],
191
+ ['Active Rules', String(stats.activeAutomationRules ?? '—')],
192
+ ]);
187
193
  console.log('');
188
194
  } catch (err) {
189
195
  s.fail('Failed to fetch stats');
@@ -198,7 +204,7 @@ async function trustedList(args, flags) {
198
204
  const s = ui.spinner('Fetching trusted IPs');
199
205
  try {
200
206
  const data = await api.get('/trusted-ips');
201
- const items = Array.isArray(data) ? data : data.trustedIps || data.items || [];
207
+ const items = data.trustedIps || [];
202
208
  s.stop(`Found ${items.length} trusted IP${items.length !== 1 ? 's' : ''}`);
203
209
 
204
210
  if (flags.json) { ui.json(items); return; }
@@ -278,17 +284,40 @@ async function forensicsQuery(args, flags) {
278
284
  const body = { query };
279
285
  if (flags.instance) body.instanceId = flags.instance;
280
286
 
281
- const job = await api.post('/forensics/jobs', body);
282
- const jobId = job.jobId || job._id || job.id;
287
+ const job = await api.post('/forensics/query', body);
288
+ const jobId = job.jobId;
289
+
290
+ if (!jobId) {
291
+ s.stop('Query complete');
292
+ if (flags.json) { ui.json(job); return; }
293
+ if (job.result) {
294
+ console.log('');
295
+ if (job.sqlquery) {
296
+ ui.subheading('Generated SQL');
297
+ console.log(`\n ${ui.c.dim(job.sqlquery)}\n`);
298
+ }
299
+ const data = job.result;
300
+ if (Array.isArray(data) && data.length > 0) {
301
+ const headers = Object.keys(data[0]);
302
+ const rows = data.map(row => headers.map(h => String(row[h] ?? '')));
303
+ ui.table(headers, rows);
304
+ } else {
305
+ ui.json(data);
306
+ }
307
+ console.log('');
308
+ }
309
+ return;
310
+ }
311
+
283
312
  s.update('Processing query...');
284
313
 
285
314
  let result;
286
315
  const maxAttempts = 60;
287
316
  for (let i = 0; i < maxAttempts; i++) {
288
317
  await new Promise(r => setTimeout(r, 2000));
289
- result = await api.get(`/forensics/jobs/${jobId}`);
318
+ result = await api.get(`/forensics/query/status/${jobId}`);
290
319
  if (result.status === 'completed' || result.status === 'failed') break;
291
- s.update(`Processing query... (${i * 2}s)`);
320
+ s.update(`Processing query... (${(i + 1) * 2}s)`);
292
321
  }
293
322
 
294
323
  if (result.status === 'failed') {
@@ -308,14 +337,14 @@ async function forensicsQuery(args, flags) {
308
337
  if (flags.json) { ui.json(result); return; }
309
338
 
310
339
  console.log('');
311
- if (result.sql) {
340
+ if (result.sqlquery) {
312
341
  ui.subheading('Generated SQL');
313
- console.log(`\n ${ui.c.dim(result.sql)}\n`);
342
+ console.log(`\n ${ui.c.dim(result.sqlquery)}\n`);
314
343
  }
315
344
 
316
- if (result.results || result.data) {
317
- const data = result.results || result.data;
318
- ui.subheading(`Results (${Array.isArray(data) ? data.length : '?'} rows)`);
345
+ if (result.result) {
346
+ const data = result.result;
347
+ ui.subheading(`Results (${result.rowCount ?? (Array.isArray(data) ? data.length : '?')} rows)`);
319
348
  console.log('');
320
349
 
321
350
  if (Array.isArray(data) && data.length > 0) {
@@ -326,11 +355,6 @@ async function forensicsQuery(args, flags) {
326
355
  ui.json(data);
327
356
  }
328
357
  }
329
-
330
- if (result.explanation) {
331
- ui.subheading('Explanation');
332
- console.log(`\n ${result.explanation}\n`);
333
- }
334
358
  console.log('');
335
359
  } catch (err) {
336
360
  s.fail('Forensic query failed');
@@ -343,7 +367,7 @@ async function forensicsLibrary(args, flags) {
343
367
  const s = ui.spinner('Fetching query library');
344
368
  try {
345
369
  const data = await api.get('/forensics/query-library');
346
- const queries = Array.isArray(data) ? data : data.queries || [];
370
+ const queries = data.data || [];
347
371
  s.stop(`Found ${queries.length} saved quer${queries.length !== 1 ? 'ies' : 'y'}`);
348
372
 
349
373
  if (flags.json) { ui.json(queries); return; }
@@ -381,46 +405,42 @@ async function ipLookup(args, flags) {
381
405
  if (flags.json) { ui.json(data); return; }
382
406
 
383
407
  console.log('');
384
- ui.heading(`IP Intelligence: ${ip}`);
408
+ ui.heading(`IP Intelligence: ${data.ip || ip}`);
385
409
  console.log('');
386
410
 
387
- const info = data.ipData || data.intel || data;
388
411
  const pairs = [];
389
-
390
- if (info.country) pairs.push(['Country', info.country]);
391
- if (info.city) pairs.push(['City', info.city]);
392
- if (info.region) pairs.push(['Region', info.region]);
393
- if (info.org || info.organization) pairs.push(['Organization', info.org || info.organization]);
394
- if (info.isp) pairs.push(['ISP', info.isp]);
395
- if (info.asn) pairs.push(['ASN', String(info.asn)]);
396
- if (info.abuseScore != null) pairs.push(['Abuse Score', `${info.abuseScore}/100`]);
397
- if (info.isVpn != null) pairs.push(['VPN', info.isVpn ? ui.c.yellow('Yes') : 'No']);
398
- if (info.isTor != null) pairs.push(['Tor', info.isTor ? ui.c.red('Yes') : 'No']);
399
- if (info.isProxy != null) pairs.push(['Proxy', info.isProxy ? ui.c.yellow('Yes') : 'No']);
400
- if (info.isBot != null) pairs.push(['Bot', info.isBot ? ui.c.red('Yes') : 'No']);
401
- if (info.isCrawler != null) pairs.push(['Crawler', info.isCrawler ? ui.c.yellow('Yes') : 'No']);
402
- if (info.threatLevel) pairs.push(['Threat Level', ui.statusBadge(info.threatLevel)]);
403
- if (info.riskLevel) pairs.push(['Risk Level', ui.statusBadge(info.riskLevel)]);
412
+ if (data.countryName || data.countryCode) pairs.push(['Country', `${data.countryName || ''} ${data.countryCode ? `(${data.countryCode})` : ''}`.trim()]);
413
+ if (data.domain) pairs.push(['Domain', data.domain]);
414
+ if (data.isp) pairs.push(['ISP', data.isp]);
415
+ if (data.usageType) pairs.push(['Usage Type', data.usageType]);
416
+ if (data.abuseConfidenceScore != null) pairs.push(['Abuse Score', `${data.abuseConfidenceScore}/100`]);
417
+ if (data.securenowScore != null) pairs.push(['SecureNow Score', String(data.securenowScore)]);
418
+ if (data.verdict) pairs.push(['Verdict', data.verdict]);
419
+ if (data.isMalicious != null) pairs.push(['Malicious', data.isMalicious ? ui.c.red('Yes') : ui.c.green('No')]);
420
+ if (data.isBot != null) pairs.push(['Bot', data.isBot ? ui.c.yellow('Yes') : 'No']);
421
+ if (data.activityType) pairs.push(['Activity', data.activityType]);
422
+ if (data.totalReports != null) pairs.push(['Total Reports', String(data.totalReports)]);
423
+ if (data.lastReportedAt) pairs.push(['Last Reported', new Date(data.lastReportedAt).toLocaleString()]);
404
424
 
405
425
  if (pairs.length) {
406
426
  ui.keyValue(pairs);
407
- } else {
408
- ui.keyValue(Object.entries(info).slice(0, 20).map(([k, v]) => [k, String(v)]));
409
427
  }
410
428
 
411
- if (data.traces || data.recentActivity) {
412
- const activity = data.traces || data.recentActivity;
413
- if (Array.isArray(activity) && activity.length > 0) {
414
- ui.subheading(`Recent Activity (${activity.length})`);
415
- console.log('');
416
- const rows = activity.slice(0, 10).map(a => [
417
- a.method || '—',
418
- ui.httpStatusColor(a.statusCode || '—'),
419
- ui.truncate(a.url || a.path || '', 40),
420
- ui.timeAgo(a.timestamp),
421
- ]);
422
- ui.table(['Method', 'Status', 'URL', 'Time'], rows);
423
- }
429
+ if (data.riskFactors?.length) {
430
+ ui.subheading('Risk Factors');
431
+ console.log('');
432
+ data.riskFactors.forEach(f => console.log(` • ${f}`));
433
+ }
434
+
435
+ if (data.attackTypes?.length) {
436
+ ui.subheading('Attack Types');
437
+ console.log('');
438
+ data.attackTypes.forEach(a => console.log(` • ${a}`));
439
+ }
440
+
441
+ if (data.summary) {
442
+ ui.subheading('Summary');
443
+ console.log(`\n ${data.summary}`);
424
444
  }
425
445
  console.log('');
426
446
  } catch (err) {
@@ -440,16 +460,16 @@ async function ipTraces(args, flags) {
440
460
  const s = ui.spinner(`Fetching traces for ${ip}`);
441
461
  try {
442
462
  const data = await api.get(`/ip/${ip}/traces`);
443
- const traces = Array.isArray(data) ? data : data.traces || [];
463
+ const traces = data.traces || [];
444
464
  s.stop(`Found ${traces.length} trace${traces.length !== 1 ? 's' : ''}`);
445
465
 
446
- if (flags.json) { ui.json(traces); return; }
466
+ if (flags.json) { ui.json(data); return; }
447
467
 
448
468
  console.log('');
449
469
  const rows = traces.map(t => [
450
470
  ui.c.dim(ui.truncate(t.traceID || t.traceId, 16)),
451
471
  t.httpMethod || t.method || '—',
452
- ui.httpStatusColor(t.statusCode || t.httpStatusCode || '—'),
472
+ ui.httpStatusColor(t.statusCode || t.httpStatusCode || t.responseStatusCode || '—'),
453
473
  ui.truncate(t.httpUrl || t.url || '', 40),
454
474
  ui.durationColor(t.durationNano ? t.durationNano / 1e6 : t.duration),
455
475
  ui.timeAgo(t.timestamp),
@@ -469,22 +489,40 @@ async function apiMapList(args, flags) {
469
489
  const s = ui.spinner('Fetching API map');
470
490
  try {
471
491
  const data = await api.get('/api-map');
472
- const endpoints = Array.isArray(data) ? data : data.endpoints || data.apiMap || [];
473
- s.stop(`Found ${endpoints.length} endpoint${endpoints.length !== 1 ? 's' : ''}`);
492
+ const apiMap = data.apiMap;
493
+ s.stop('API map loaded');
474
494
 
475
495
  if (flags.json) { ui.json(data); return; }
476
496
 
497
+ if (!apiMap) {
498
+ console.log('');
499
+ ui.info(data.message || 'No API map discovered yet. Run discovery from the dashboard.');
500
+ console.log('');
501
+ return;
502
+ }
503
+
477
504
  console.log('');
478
- const rows = endpoints.map(e => [
479
- e.method || '—',
480
- e.path || e.route || e.url || '—',
481
- e.serviceName || e.app || '',
482
- e.requestCount != null ? String(e.requestCount) : '—',
483
- e.avgDuration != null ? ui.durationColor(e.avgDuration) : '—',
484
- e.errorRate != null ? (e.errorRate > 5 ? ui.c.red(`${e.errorRate}%`) : `${e.errorRate}%`) : '—',
485
- ]);
486
- ui.table(['Method', 'Path', 'App', 'Requests', 'Avg Duration', 'Error Rate'], rows);
487
- console.log('');
505
+ if (apiMap.apps && typeof apiMap.apps === 'object') {
506
+ for (const [appName, appData] of Object.entries(apiMap.apps)) {
507
+ ui.subheading(appName);
508
+ console.log('');
509
+ const endpoints = appData.endpoints || [];
510
+ if (endpoints.length) {
511
+ const rows = endpoints.map(e => [
512
+ e.method || '—',
513
+ e.path || e.route || '',
514
+ e.requestCount != null ? String(e.requestCount) : '',
515
+ e.description || ui.c.dim('—'),
516
+ ]);
517
+ ui.table(['Method', 'Path', 'Requests', 'Description'], rows);
518
+ } else {
519
+ ui.info('No endpoints discovered for this app.');
520
+ }
521
+ console.log('');
522
+ }
523
+ } else {
524
+ ui.json(apiMap);
525
+ }
488
526
  } catch (err) {
489
527
  s.fail('Failed to fetch API map');
490
528
  throw err;
@@ -495,15 +533,30 @@ async function apiMapStats(args, flags) {
495
533
  requireAuth();
496
534
  const s = ui.spinner('Fetching API map stats');
497
535
  try {
498
- const stats = await api.get('/api-map/stats');
536
+ const data = await api.get('/api-map/stats');
537
+ const stats = data.stats;
499
538
  s.stop('Stats loaded');
500
539
 
501
540
  if (flags.json) { ui.json(stats); return; }
502
541
 
542
+ if (!stats) {
543
+ console.log('');
544
+ ui.info('No API map stats available.');
545
+ console.log('');
546
+ return;
547
+ }
548
+
503
549
  console.log('');
504
550
  ui.heading('API Map Statistics');
505
551
  console.log('');
506
- ui.keyValue(Object.entries(stats).map(([k, v]) => [k, String(v)]));
552
+ ui.keyValue([
553
+ ['Total Apps', String(stats.totalApps ?? '—')],
554
+ ['Total Endpoints', String(stats.totalEndpoints ?? '—')],
555
+ ['Total Requests', String(stats.totalRequests ?? '—')],
556
+ ['Discovery Status', stats.discoveryStatus || '—'],
557
+ ['Last Discovered', stats.lastDiscoveredAt ? new Date(stats.lastDiscoveredAt).toLocaleString() : '—'],
558
+ ['Version', String(stats.version ?? '—')],
559
+ ]);
507
560
  console.log('');
508
561
  } catch (err) {
509
562
  s.fail('Failed to fetch stats');
@@ -518,7 +571,7 @@ async function instancesList(args, flags) {
518
571
  const s = ui.spinner('Fetching instances');
519
572
  try {
520
573
  const data = await api.get('/instances');
521
- const instances = Array.isArray(data) ? data : data.instances || [];
574
+ const instances = data.instances || [];
522
575
  s.stop(`Found ${instances.length} instance${instances.length !== 1 ? 's' : ''}`);
523
576
 
524
577
  if (flags.json) { ui.json(instances); return; }
@@ -529,10 +582,10 @@ async function instancesList(args, flags) {
529
582
  inst.name || inst.host || '—',
530
583
  inst.host || '—',
531
584
  inst.port != null ? String(inst.port) : '—',
532
- ui.statusBadge(inst.status || 'active'),
585
+ inst.linkedApps != null ? String(inst.linkedApps) : '',
533
586
  ui.timeAgo(inst.createdAt),
534
587
  ]);
535
- ui.table(['ID', 'Name', 'Host', 'Port', 'Status', 'Added'], rows);
588
+ ui.table(['ID', 'Name', 'Host', 'Port', 'Linked Apps', 'Added'], rows);
536
589
  console.log('');
537
590
  } catch (err) {
538
591
  s.fail('Failed to fetch instances');
@@ -551,11 +604,10 @@ async function instancesTest(args, flags) {
551
604
  const s = ui.spinner('Testing instance connection');
552
605
  try {
553
606
  const result = await api.post(`/instances/${id}/test`);
554
- if (result.success || result.connected) {
555
- s.stop('Instance connection successful');
607
+ if (result.success) {
608
+ s.stop(`Connection successful${result.storageGb ? ` (${result.storageGb} GB storage)` : ''}`);
556
609
  } else {
557
- s.fail('Instance connection failed');
558
- if (result.error) ui.error(result.error);
610
+ s.fail(result.message || 'Connection failed');
559
611
  }
560
612
 
561
613
  if (flags.json) ui.json(result);
@@ -569,11 +621,10 @@ async function instancesTest(args, flags) {
569
621
 
570
622
  async function analytics(args, flags) {
571
623
  requireAuth();
572
- const appKey = resolveApp(flags);
573
624
  const s = ui.spinner('Fetching analytics');
574
625
  try {
575
626
  const query = {};
576
- if (appKey) query.serviceName = appKey;
627
+ const appKey = resolveApp(flags);
577
628
  if (flags.instance) query.instanceId = flags.instance;
578
629
 
579
630
  const endpoints = ['2xx-responses', '3xx-responses', '4xx-responses', '5xx-responses', '500-errors'];
@@ -596,7 +647,7 @@ async function analytics(args, flags) {
596
647
 
597
648
  const pairs = endpoints.map((ep, i) => {
598
649
  const val = results[i];
599
- const count = val?.count ?? val?.total ?? (typeof val === 'number' ? val : '—');
650
+ const count = val?.meta?.count ?? '—';
600
651
  return [ep, String(count)];
601
652
  });
602
653
  ui.keyValue(pairs);
package/cli.js CHANGED
@@ -377,13 +377,11 @@ async function main() {
377
377
  }
378
378
 
379
379
  main().catch((err) => {
380
- if (err.name === 'CLIError') {
381
- ui.error(err.message);
382
- } else {
380
+ if (err.name !== 'CLIError') {
383
381
  ui.error(err.message || 'An unexpected error occurred');
384
- if (process.env.SECURENOW_DEBUG) {
385
- console.error(err.stack);
386
- }
382
+ }
383
+ if (process.env.SECURENOW_DEBUG) {
384
+ console.error(err.stack || err);
387
385
  }
388
386
  process.exit(1);
389
387
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "5.3.0",
3
+ "version": "5.3.2",
4
4
  "description": "OpenTelemetry instrumentation for Node.js and Next.js - Send traces and logs to any OTLP-compatible backend",
5
5
  "type": "commonjs",
6
6
  "main": "register.js",