@sentio/cli 3.6.0-rc.2 → 3.6.0-rc.3

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/lib/index.js CHANGED
@@ -143006,7 +143006,10 @@ function createAlertGetCommand() {
143006
143006
  function createAlertCreateCommand() {
143007
143007
  return withOutputOptions4(
143008
143008
  withSharedProjectOptions4(withAuthOptions4(new Command("create").description("Create an alert rule")))
143009
- ).showHelpAfterError().option("--file <path>", "Read request JSON or YAML from file. Use --doc to show the full alert request format").option("--stdin", "Read request JSON or YAML from stdin. Use --doc to show the full alert request format").option("--doc", "Print the full alert request file format and exit").option("--type <type>", "Alert type: METRIC, LOG, or SQL").option("--subject <text>", "Alert subject/title").option("--message <text>", "Optional alert message template").option("--query <text>", "Inline log query or SQL when not using --file/--stdin").option("--event <name>", "Inline event query for METRIC alerts").option("--metric <name>", "Inline metric query for METRIC alerts").option("--alias <alias>", "Alias for the inline METRIC query").option("--source-name <name>", "Optional source name for the inline METRIC query").option("--filter <selector>", "Inline METRIC query filter like amount>0 or meta.chain=1", collectOption2, []).option("--group-by <field>", "Inline METRIC query group-by field", collectOption2, []).option(
143009
+ ).showHelpAfterError().option("--file <path>", "Read request JSON or YAML from file. Use --doc to show the full alert request format").option("--stdin", "Read request JSON or YAML from stdin. Use --doc to show the full alert request format").option("--doc", "Print the full alert request file format and exit").option("--type <type>", "Alert type: METRIC, LOG, or SQL").option("--subject <text>", "Alert subject/title").option("--message <text>", "Optional alert message template").option(
143010
+ "--query <text>",
143011
+ "Inline query. LOG: Elasticsearch query-string syntax (e.g. amount:>1000, status:error). SQL: full SQL statement."
143012
+ ).option("--event <name>", "Inline event query for METRIC alerts").option("--metric <name>", "Inline metric query for METRIC alerts").option("--alias <alias>", "Alias for the inline METRIC query").option("--source-name <name>", "Optional source name for the inline METRIC query").option("--filter <selector>", "Inline METRIC query filter like amount>0 or meta.chain=1", collectOption2, []).option("--group-by <field>", "Inline METRIC query group-by field", collectOption2, []).option(
143010
143013
  "--aggr <aggregation>",
143011
143014
  "Inline aggregation. METRIC: avg|sum|min|max|count. EVENTS: total|unique|AAU|DAU|WAU|MAU"
143012
143015
  ).option("--func <function>", "Inline function like topk(1), bottomk(1), delta(1m)", collectOption2, []).option("--op <operator>", "Condition operator like >, >=, ==, !=, <, <=, between").option("--threshold <value>", "Condition threshold", parseNumber).option("--threshold2 <value>", "Second threshold for between", parseNumber).option("--for <duration>", "Evaluate over the last duration, for example 5m or 1h").option("--interval <duration>", "Alert evaluation interval, for example 1m or 5m").option("--time-column <column>", "SQL alert time column for column-based conditions").option("--value-column <column>", "SQL alert value column for column-based conditions").option("--sql-aggr <aggregation>", "SQL aggregation: COUNT, SUM, AVG, MAX, MIN, LAST").addHelpText(
@@ -143014,7 +143017,7 @@ function createAlertCreateCommand() {
143014
143017
  `
143015
143018
 
143016
143019
  Examples:
143017
- $ sentio alert create --project sentio/coinbase --type LOG --subject "Large transfer logs" --query 'amount > 1000' --op '>' --threshold 0
143020
+ $ sentio alert create --project sentio/coinbase --type LOG --subject "Large transfer logs" --query 'amount:>1000' --op '>' --threshold 0
143018
143021
  $ sentio alert create --project sentio/coinbase --type SQL --subject "Large transfer(SQL demo)" --query 'select timestamp, amount from transfer where amount > 1000' --time-column timestamp --value-column amount --sql-aggr MAX --op '>' --threshold 1000
143019
143022
  $ sentio alert create --project sentio/coinbase --type METRIC --subject "Burn spike" --metric burn --filter meta.chain=1 --aggr avg --group-by meta.address --op '>' --threshold 100
143020
143023
  $ sentio alert create --project sentio/coinbase --type METRIC --subject "Transfer anomaly" --event Transfer --filter amount>0 --aggr total --func 'delta(1m)' --op '>' --threshold 100
@@ -143637,11 +143640,14 @@ Metric alert example:
143637
143640
  disabled: false
143638
143641
 
143639
143642
  Log alert example:
143643
+ NOTE: logCondition.query uses Elasticsearch query-string syntax.
143644
+ Ranges MUST use field:>value form \u2014 C-style "field > value" is rejected at evaluation time.
143645
+ Examples: amount:>1000000 timestamp:>=2024-01-01 amount:[1000 TO 9999] status:error
143640
143646
  rule:
143641
143647
  alertType: LOG
143642
143648
  subject: large transfer logs
143643
143649
  logCondition:
143644
- query: amount > 1000
143650
+ query: amount:>1000
143645
143651
  comparisonOp: ">"
143646
143652
  threshold: 0
143647
143653
 
@@ -144874,7 +144880,13 @@ function createDashboardAddPanelCommand() {
144874
144880
  "Event filter or metric label selector like field:value or amount>0",
144875
144881
  collectOption3,
144876
144882
  []
144877
- ).option("--group-by <field>", "Group by event property or metric label", collectOption3, []).option("--aggr <aggregation>", "Event: total|unique|AAU|DAU|WAU|MAU. Metric: avg|sum|min|max|count").option("--func <function>", "Function like topk(1), bottomk(1)", collectOption3, []).addHelpText(
144883
+ ).option("--group-by <field>", "Group by event property or metric label", collectOption3, []).option("--aggr <aggregation>", "Event: total|unique|AAU|DAU|WAU|MAU. Metric: avg|sum|min|max|count").option("--func <function>", "Function like topk(1), bottomk(1)", collectOption3, []).option(
144884
+ "--time-range-start <value>",
144885
+ "Panel time range start: relative (e.g. -24h, -7d, -30m) or ISO date (e.g. 2024-01-01T00:00:00Z)"
144886
+ ).option(
144887
+ "--time-range-end <value>",
144888
+ "Panel time range end: relative (e.g. now, -1h) or ISO date. Defaults to now when --time-range-start is set."
144889
+ ).option("--time-range-step <seconds>", "Panel time range step in seconds (e.g. 3600)").addHelpText(
144878
144890
  "after",
144879
144891
  `
144880
144892
 
@@ -144911,7 +144923,17 @@ Metric insights panel examples:
144911
144923
  --metric burn --filter meta.chain=1 --aggr avg --group-by meta.address
144912
144924
  $ sentio dashboard add-panel abc123 --project owner/slug \\
144913
144925
  --panel-name "Burn Rate Delta" --type LINE \\
144914
- --metric burn --aggr sum
144926
+ --metric burn --aggr sum
144927
+
144928
+ Panel time range override examples:
144929
+ $ sentio dashboard add-panel abc123 --project owner/slug \\
144930
+ --panel-name "Last 24h Transfers" --type LINE \\
144931
+ --event Transfer --aggr total \\
144932
+ --time-range-start -24h --time-range-end now --time-range-step 3600
144933
+ $ sentio dashboard add-panel abc123 --project owner/slug \\
144934
+ --panel-name "Jan 2024 Volume" --type BAR \\
144935
+ --sql "SELECT date, sum(amount) FROM Transfer GROUP BY date" \\
144936
+ --time-range-start 2024-01-01T00:00:00Z --time-range-end 2024-02-01T00:00:00Z
144915
144937
  `
144916
144938
  ).action(async (dashboardId, options, command) => {
144917
144939
  try {
@@ -144979,14 +145001,18 @@ function buildDashboardCreateBody(options, project) {
144979
145001
  };
144980
145002
  }
144981
145003
  function normalizeDashboardInit(input) {
145004
+ const emptyLayouts = {
145005
+ responsiveLayouts: {
145006
+ lg: { layouts: [] },
145007
+ md: { layouts: [] },
145008
+ sm: { layouts: [] },
145009
+ xs: { layouts: [] }
145010
+ }
145011
+ };
144982
145012
  if (input === void 0) {
144983
145013
  return {
144984
145014
  panels: {},
144985
- layouts: {
144986
- responsiveLayouts: {
144987
- lg: { layouts: [] }
144988
- }
144989
- }
145015
+ layouts: emptyLayouts
144990
145016
  };
144991
145017
  }
144992
145018
  if (!input || typeof input !== "object" || Array.isArray(input)) {
@@ -144995,11 +145021,7 @@ function normalizeDashboardInit(input) {
144995
145021
  const dashboard = input;
144996
145022
  return {
144997
145023
  panels: isRecord(dashboard.panels) ? dashboard.panels : {},
144998
- layouts: isRecord(dashboard.layouts) ? dashboard.layouts : {
144999
- responsiveLayouts: {
145000
- lg: { layouts: [] }
145001
- }
145002
- }
145024
+ layouts: isRecord(dashboard.layouts) ? dashboard.layouts : emptyLayouts
145003
145025
  };
145004
145026
  }
145005
145027
  async function runDashboardAddPanel(dashboardId, options) {
@@ -145023,11 +145045,13 @@ async function runDashboardAddPanel(dashboardId, options) {
145023
145045
  const chartType = normalizeChartType(options.type);
145024
145046
  const panelId = generatePanelId();
145025
145047
  const chart = buildPanelChart(chartType, options);
145048
+ const timeRangeOverride = buildTimeRangeOverride(options);
145026
145049
  const newPanel = {
145027
145050
  id: panelId,
145028
145051
  name: options.panelName,
145029
145052
  dashboardId,
145030
- chart
145053
+ chart,
145054
+ ...timeRangeOverride ? { timeRangeOverride } : {}
145031
145055
  };
145032
145056
  const existingLayouts = dashboard.layouts?.responsiveLayouts?.lg?.layouts ?? [];
145033
145057
  let maxBottom = 0;
@@ -145046,15 +145070,17 @@ async function runDashboardAddPanel(dashboardId, options) {
145046
145070
  };
145047
145071
  const panels = { ...dashboard.panels ?? {} };
145048
145072
  panels[panelId] = newPanel;
145049
- const updatedLayouts = [...existingLayouts, newLayout];
145073
+ const existingResponsive = dashboard.layouts?.responsiveLayouts ?? {};
145074
+ const updatedResponsive = { ...existingResponsive };
145075
+ for (const bp2 of ["lg", "md", "sm", "xs"]) {
145076
+ const existing = existingResponsive[bp2]?.layouts ?? [];
145077
+ updatedResponsive[bp2] = { layouts: [...existing, newLayout] };
145078
+ }
145050
145079
  const dashboardJson = {
145051
145080
  ...dashboard,
145052
145081
  panels,
145053
145082
  layouts: {
145054
- responsiveLayouts: {
145055
- ...dashboard.layouts?.responsiveLayouts ?? {},
145056
- lg: { layouts: updatedLayouts }
145057
- }
145083
+ responsiveLayouts: updatedResponsive
145058
145084
  }
145059
145085
  };
145060
145086
  const importResponse = await import_api14.WebService.importDashboard({
@@ -145072,6 +145098,40 @@ async function runDashboardAddPanel(dashboardId, options) {
145072
145098
  dashboard: importData.dashboard
145073
145099
  });
145074
145100
  }
145101
+ function buildTimeRangeLike(value) {
145102
+ const relMatch = value.match(/^(-?\d+)\s*([smhdwMy])$/);
145103
+ if (relMatch) {
145104
+ return { relativeTime: { unit: relMatch[2], value: Number(relMatch[1]) } };
145105
+ }
145106
+ if (value === "now" || value === "0") {
145107
+ return { relativeTime: { unit: "h", value: 0 } };
145108
+ }
145109
+ const ts2 = Date.parse(value);
145110
+ if (!Number.isNaN(ts2)) {
145111
+ return { absoluteTime: String(ts2) };
145112
+ }
145113
+ throw new CliError(
145114
+ `Invalid time range value "${value}". Use a relative offset (e.g. -24h, -7d, -30m, now) or an ISO date string.`
145115
+ );
145116
+ }
145117
+ function buildTimeRangeOverride(options) {
145118
+ if (!options.timeRangeStart && !options.timeRangeEnd) {
145119
+ return void 0;
145120
+ }
145121
+ const timeRange = {};
145122
+ if (options.timeRangeStart) {
145123
+ timeRange.start = buildTimeRangeLike(options.timeRangeStart);
145124
+ }
145125
+ if (options.timeRangeEnd) {
145126
+ timeRange.end = buildTimeRangeLike(options.timeRangeEnd);
145127
+ } else {
145128
+ timeRange.end = { relativeTime: { unit: "h", value: 0 } };
145129
+ }
145130
+ if (options.timeRangeStep) {
145131
+ timeRange.step = options.timeRangeStep;
145132
+ }
145133
+ return { enabled: true, timeRange };
145134
+ }
145075
145135
  function buildPanelChart(chartType, options) {
145076
145136
  if (options.sql) {
145077
145137
  const sqlSize = Number.parseInt(String(options.size ?? "100"), 10) || 100;
@@ -145141,7 +145201,7 @@ function withOutputOptions8(command) {
145141
145201
  return command.option("--json", "Print raw JSON response").option("--yaml", "Print raw YAML response");
145142
145202
  }
145143
145203
  function handleDashboardCommandError(error, command) {
145144
- if (error instanceof CliError && (error.message.startsWith("Project is required.") || error.message.startsWith("Invalid project ") || error.message.startsWith("Dashboard ") || error.message.startsWith("Provide --file or --stdin") || error.message.startsWith("Use either --file or --stdin") || error.message.startsWith("Expected JSON or YAML") || error.message.startsWith("Invalid JSON or YAML") || error.message.startsWith("Dashboard initialization data") || error.message.startsWith("Provide exactly one data source") || error.message.startsWith("Use exactly one of --sql") || error.message.startsWith("Invalid chart type") || error.message.startsWith("Invalid aggregation") || error.message.startsWith("Invalid metric aggregation") || error.message.startsWith("Invalid filter") || error.message.startsWith("Invalid metric selector"))) {
145204
+ if (error instanceof CliError && (error.message.startsWith("Project is required.") || error.message.startsWith("Invalid project ") || error.message.startsWith("Dashboard ") || error.message.startsWith("Provide --file or --stdin") || error.message.startsWith("Use either --file or --stdin") || error.message.startsWith("Expected JSON or YAML") || error.message.startsWith("Invalid JSON or YAML") || error.message.startsWith("Dashboard initialization data") || error.message.startsWith("Provide exactly one data source") || error.message.startsWith("Use exactly one of --sql") || error.message.startsWith("Invalid chart type") || error.message.startsWith("Invalid aggregation") || error.message.startsWith("Invalid metric aggregation") || error.message.startsWith("Invalid filter") || error.message.startsWith("Invalid metric selector") || error.message.startsWith("Invalid time range value"))) {
145145
145205
  console.error(error.message);
145146
145206
  if (command) {
145147
145207
  console.error();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentio/cli",
3
- "version": "3.6.0-rc.2",
3
+ "version": "3.6.0-rc.3",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -136,7 +136,10 @@ function createAlertCreateCommand() {
136
136
  .option('--type <type>', 'Alert type: METRIC, LOG, or SQL')
137
137
  .option('--subject <text>', 'Alert subject/title')
138
138
  .option('--message <text>', 'Optional alert message template')
139
- .option('--query <text>', 'Inline log query or SQL when not using --file/--stdin')
139
+ .option(
140
+ '--query <text>',
141
+ 'Inline query. LOG: Elasticsearch query-string syntax (e.g. amount:>1000, status:error). SQL: full SQL statement.'
142
+ )
140
143
  .option('--event <name>', 'Inline event query for METRIC alerts')
141
144
  .option('--metric <name>', 'Inline metric query for METRIC alerts')
142
145
  .option('--alias <alias>', 'Alias for the inline METRIC query')
@@ -161,7 +164,7 @@ function createAlertCreateCommand() {
161
164
  `
162
165
 
163
166
  Examples:
164
- $ sentio alert create --project sentio/coinbase --type LOG --subject "Large transfer logs" --query 'amount > 1000' --op '>' --threshold 0
167
+ $ sentio alert create --project sentio/coinbase --type LOG --subject "Large transfer logs" --query 'amount:>1000' --op '>' --threshold 0
165
168
  $ sentio alert create --project sentio/coinbase --type SQL --subject "Large transfer(SQL demo)" --query 'select timestamp, amount from transfer where amount > 1000' --time-column timestamp --value-column amount --sql-aggr MAX --op '>' --threshold 1000
166
169
  $ sentio alert create --project sentio/coinbase --type METRIC --subject "Burn spike" --metric burn --filter meta.chain=1 --aggr avg --group-by meta.address --op '>' --threshold 100
167
170
  $ sentio alert create --project sentio/coinbase --type METRIC --subject "Transfer anomaly" --event Transfer --filter amount>0 --aggr total --func 'delta(1m)' --op '>' --threshold 100
@@ -916,11 +919,14 @@ Metric alert example:
916
919
  disabled: false
917
920
 
918
921
  Log alert example:
922
+ NOTE: logCondition.query uses Elasticsearch query-string syntax.
923
+ Ranges MUST use field:>value form — C-style "field > value" is rejected at evaluation time.
924
+ Examples: amount:>1000000 timestamp:>=2024-01-01 amount:[1000 TO 9999] status:error
919
925
  rule:
920
926
  alertType: LOG
921
927
  subject: large transfer logs
922
928
  logCondition:
923
- query: amount > 1000
929
+ query: amount:>1000
924
930
  comparisonOp: ">"
925
931
  threshold: 0
926
932
 
@@ -50,6 +50,9 @@ interface AddPanelOptions extends DashboardOptions {
50
50
  groupBy?: string[]
51
51
  aggr?: string
52
52
  func?: string[]
53
+ timeRangeStart?: string
54
+ timeRangeEnd?: string
55
+ timeRangeStep?: string
53
56
  }
54
57
 
55
58
  export function createDashboardCommand() {
@@ -162,6 +165,15 @@ function createDashboardAddPanelCommand() {
162
165
  .option('--group-by <field>', 'Group by event property or metric label', collectOption, [])
163
166
  .option('--aggr <aggregation>', 'Event: total|unique|AAU|DAU|WAU|MAU. Metric: avg|sum|min|max|count')
164
167
  .option('--func <function>', 'Function like topk(1), bottomk(1)', collectOption, [])
168
+ .option(
169
+ '--time-range-start <value>',
170
+ 'Panel time range start: relative (e.g. -24h, -7d, -30m) or ISO date (e.g. 2024-01-01T00:00:00Z)'
171
+ )
172
+ .option(
173
+ '--time-range-end <value>',
174
+ 'Panel time range end: relative (e.g. now, -1h) or ISO date. Defaults to now when --time-range-start is set.'
175
+ )
176
+ .option('--time-range-step <seconds>', 'Panel time range step in seconds (e.g. 3600)')
165
177
  .addHelpText(
166
178
  'after',
167
179
  `
@@ -199,7 +211,17 @@ Metric insights panel examples:
199
211
  --metric burn --filter meta.chain=1 --aggr avg --group-by meta.address
200
212
  $ sentio dashboard add-panel abc123 --project owner/slug \\
201
213
  --panel-name "Burn Rate Delta" --type LINE \\
202
- --metric burn --aggr sum
214
+ --metric burn --aggr sum
215
+
216
+ Panel time range override examples:
217
+ $ sentio dashboard add-panel abc123 --project owner/slug \\
218
+ --panel-name "Last 24h Transfers" --type LINE \\
219
+ --event Transfer --aggr total \\
220
+ --time-range-start -24h --time-range-end now --time-range-step 3600
221
+ $ sentio dashboard add-panel abc123 --project owner/slug \\
222
+ --panel-name "Jan 2024 Volume" --type BAR \\
223
+ --sql "SELECT date, sum(amount) FROM Transfer GROUP BY date" \\
224
+ --time-range-start 2024-01-01T00:00:00Z --time-range-end 2024-02-01T00:00:00Z
203
225
  `
204
226
  )
205
227
  .action(async (dashboardId, options, command) => {
@@ -279,14 +301,19 @@ function buildDashboardCreateBody(options: DashboardCreateOptions, project: { ow
279
301
  }
280
302
 
281
303
  function normalizeDashboardInit(input: unknown) {
304
+ const emptyLayouts = {
305
+ responsiveLayouts: {
306
+ lg: { layouts: [] },
307
+ md: { layouts: [] },
308
+ sm: { layouts: [] },
309
+ xs: { layouts: [] }
310
+ }
311
+ }
312
+
282
313
  if (input === undefined) {
283
314
  return {
284
315
  panels: {},
285
- layouts: {
286
- responsiveLayouts: {
287
- lg: { layouts: [] }
288
- }
289
- }
316
+ layouts: emptyLayouts
290
317
  }
291
318
  }
292
319
 
@@ -297,13 +324,7 @@ function normalizeDashboardInit(input: unknown) {
297
324
  const dashboard = input as Record<string, unknown>
298
325
  return {
299
326
  panels: isRecord(dashboard.panels) ? dashboard.panels : {},
300
- layouts: isRecord(dashboard.layouts)
301
- ? dashboard.layouts
302
- : {
303
- responsiveLayouts: {
304
- lg: { layouts: [] }
305
- }
306
- }
327
+ layouts: isRecord(dashboard.layouts) ? dashboard.layouts : emptyLayouts
307
328
  }
308
329
  }
309
330
 
@@ -335,11 +356,13 @@ async function runDashboardAddPanel(dashboardId: string, options: AddPanelOption
335
356
  const panelId = generatePanelId()
336
357
  const chart = buildPanelChart(chartType, options)
337
358
 
338
- const newPanel = {
359
+ const timeRangeOverride = buildTimeRangeOverride(options)
360
+ const newPanel: Record<string, unknown> = {
339
361
  id: panelId,
340
362
  name: options.panelName,
341
363
  dashboardId,
342
- chart
364
+ chart,
365
+ ...(timeRangeOverride ? { timeRangeOverride } : {})
343
366
  }
344
367
 
345
368
  // 3. Compute layout position: place below all existing panels
@@ -364,16 +387,18 @@ async function runDashboardAddPanel(dashboardId: string, options: AddPanelOption
364
387
  const panels = { ...(dashboard.panels ?? {}) }
365
388
  panels[panelId] = newPanel as never
366
389
 
367
- const updatedLayouts = [...existingLayouts, newLayout]
390
+ const existingResponsive = dashboard.layouts?.responsiveLayouts ?? {}
391
+ const updatedResponsive: Record<string, unknown> = { ...existingResponsive }
392
+ for (const bp of ['lg', 'md', 'sm', 'xs'] as const) {
393
+ const existing = (existingResponsive as Record<string, { layouts?: unknown[] } | undefined>)[bp]?.layouts ?? []
394
+ updatedResponsive[bp] = { layouts: [...existing, newLayout] }
395
+ }
368
396
 
369
397
  const dashboardJson: Record<string, unknown> = {
370
398
  ...dashboard,
371
399
  panels,
372
400
  layouts: {
373
- responsiveLayouts: {
374
- ...(dashboard.layouts?.responsiveLayouts ?? {}),
375
- lg: { layouts: updatedLayouts }
376
- }
401
+ responsiveLayouts: updatedResponsive
377
402
  }
378
403
  }
379
404
 
@@ -393,6 +418,43 @@ async function runDashboardAddPanel(dashboardId: string, options: AddPanelOption
393
418
  })
394
419
  }
395
420
 
421
+ function buildTimeRangeLike(value: string): Record<string, unknown> {
422
+ const relMatch = value.match(/^(-?\d+)\s*([smhdwMy])$/)
423
+ if (relMatch) {
424
+ return { relativeTime: { unit: relMatch[2], value: Number(relMatch[1]) } }
425
+ }
426
+ if (value === 'now' || value === '0') {
427
+ return { relativeTime: { unit: 'h', value: 0 } }
428
+ }
429
+ const ts = Date.parse(value)
430
+ if (!Number.isNaN(ts)) {
431
+ return { absoluteTime: String(ts) }
432
+ }
433
+ throw new CliError(
434
+ `Invalid time range value "${value}". Use a relative offset (e.g. -24h, -7d, -30m, now) or an ISO date string.`
435
+ )
436
+ }
437
+
438
+ function buildTimeRangeOverride(options: AddPanelOptions): Record<string, unknown> | undefined {
439
+ if (!options.timeRangeStart && !options.timeRangeEnd) {
440
+ return undefined
441
+ }
442
+ const timeRange: Record<string, unknown> = {}
443
+ if (options.timeRangeStart) {
444
+ timeRange.start = buildTimeRangeLike(options.timeRangeStart)
445
+ }
446
+ if (options.timeRangeEnd) {
447
+ timeRange.end = buildTimeRangeLike(options.timeRangeEnd)
448
+ } else {
449
+ // default end to "now" when start is provided
450
+ timeRange.end = { relativeTime: { unit: 'h', value: 0 } }
451
+ }
452
+ if (options.timeRangeStep) {
453
+ timeRange.step = options.timeRangeStep
454
+ }
455
+ return { enabled: true, timeRange }
456
+ }
457
+
396
458
  function buildPanelChart(chartType: string, options: AddPanelOptions) {
397
459
  if (options.sql) {
398
460
  const sqlSize = Number.parseInt(String(options.size ?? '100'), 10) || 100
@@ -496,7 +558,8 @@ function handleDashboardCommandError(error: unknown, command?: Command) {
496
558
  error.message.startsWith('Invalid aggregation') ||
497
559
  error.message.startsWith('Invalid metric aggregation') ||
498
560
  error.message.startsWith('Invalid filter') ||
499
- error.message.startsWith('Invalid metric selector'))
561
+ error.message.startsWith('Invalid metric selector') ||
562
+ error.message.startsWith('Invalid time range value'))
500
563
  ) {
501
564
  console.error(error.message)
502
565
  if (command) {