maybeai-sheet-cli 0.26__tar.gz → 0.26.2__tar.gz

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.
Files changed (89) hide show
  1. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/PKG-INFO +19 -16
  2. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/README.md +18 -15
  3. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/pyproject.toml +1 -1
  4. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/commands/_phase2.py +20 -0
  5. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/commands/column.py +6 -2
  6. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/commands/db_table.py +66 -0
  7. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/commands/style.py +59 -24
  8. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/commands/workbook.py +75 -2
  9. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/endpoints.py +10 -0
  10. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/tests/test_cli.py +84 -1
  11. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/tests/test_endpoints.py +8 -1
  12. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/tests/test_phase2_cli.py +24 -11
  13. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/tests/test_two_engine_cli.py +57 -1
  14. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/.gitignore +0 -0
  15. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/__init__.py +0 -0
  16. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/chart_config.py +0 -0
  17. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/chart_style_summary.py +0 -0
  18. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/cli.py +0 -0
  19. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/client.py +0 -0
  20. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/commands/__init__.py +0 -0
  21. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/commands/base_table.py +0 -0
  22. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/commands/cell.py +0 -0
  23. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/commands/chart.py +0 -0
  24. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/commands/dashboard.py +0 -0
  25. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/commands/excel_table.py +0 -0
  26. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/commands/excel_worksheet.py +0 -0
  27. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/commands/file.py +0 -0
  28. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/commands/formula.py +0 -0
  29. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/commands/history.py +0 -0
  30. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/commands/image.py +0 -0
  31. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/commands/media.py +0 -0
  32. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/commands/pivot.py +0 -0
  33. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/commands/range.py +0 -0
  34. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/commands/raw.py +0 -0
  35. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/commands/row.py +0 -0
  36. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/commands/share.py +0 -0
  37. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/commands/sheet.py +0 -0
  38. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/commands/sql.py +0 -0
  39. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/commands/table.py +0 -0
  40. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/commands/worksheet.py +0 -0
  41. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/config.py +0 -0
  42. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/dashboard_interactions.py +0 -0
  43. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/dashboard_plan.py +0 -0
  44. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/dashboard_template.py +0 -0
  45. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/errors.py +0 -0
  46. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/formatters.py +0 -0
  47. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/importing/__init__.py +0 -0
  48. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/importing/create.py +0 -0
  49. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/importing/models.py +0 -0
  50. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/importing/replace.py +0 -0
  51. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/models/__init__.py +0 -0
  52. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/models/share.py +0 -0
  53. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/models/sheet.py +0 -0
  54. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/models/workbook.py +0 -0
  55. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/options.py +0 -0
  56. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/protocol.py +0 -0
  57. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/resolver.py +0 -0
  58. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/resources/__init__.py +0 -0
  59. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/resources/style_templates/__init__.py +0 -0
  60. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/resources/style_templates/canva_cost_benefit.json +0 -0
  61. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/resources/style_templates/canva_decision_matrix.json +0 -0
  62. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/resources/style_templates/canva_finance_report.json +0 -0
  63. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/resources/style_templates/canva_inventory.json +0 -0
  64. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/resources/style_templates/canva_marketing_calendar.json +0 -0
  65. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/resources/style_templates/canva_minimal.json +0 -0
  66. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/resources/style_templates/canva_project.json +0 -0
  67. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/resources/style_templates/canva_risk_register.json +0 -0
  68. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/resources/style_templates/canva_sales_pipeline.json +0 -0
  69. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/resources/style_templates/canva_timesheet.json +0 -0
  70. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/runtime/dashboard_render_probe.mjs +0 -0
  71. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/table_resolver.py +0 -0
  72. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/src/maybeai_sheet/update.py +0 -0
  73. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/tests/fixtures/benchmarks/build_fixtures.py +0 -0
  74. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/tests/fixtures/benchmarks/flat-records.xlsx +0 -0
  75. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/tests/fixtures/benchmarks/mixed-engine.xlsx +0 -0
  76. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/tests/fixtures/benchmarks/multi-table-layout.xlsx +0 -0
  77. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/tests/fixtures/benchmarks/rows.json +0 -0
  78. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/tests/fixtures/benchmarks/simple-grid.xlsx +0 -0
  79. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/tests/fixtures/benchmarks/simple-values.json +0 -0
  80. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/tests/test_benchmark_runner.py +0 -0
  81. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/tests/test_dashboard_plan.py +0 -0
  82. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/tests/test_dashboard_template.py +0 -0
  83. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/tests/test_formatters.py +0 -0
  84. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/tests/test_import_to_worksheet_benchmark.py +0 -0
  85. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/tests/test_import_to_worksheet_live_benchmark.py +0 -0
  86. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/tests/test_importing.py +0 -0
  87. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/tests/test_resolver.py +0 -0
  88. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/tests/test_update.py +0 -0
  89. {maybeai_sheet_cli-0.26 → maybeai_sheet_cli-0.26.2}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: maybeai-sheet-cli
3
- Version: 0.26
3
+ Version: 0.26.2
4
4
  Summary: CLI for common MaybeAI spreadsheet operations
5
5
  Project-URL: Homepage, https://github.com/OmniMCP-AI/maybeai-sheet-cli
6
6
  Project-URL: Repository, https://github.com/OmniMCP-AI/maybeai-sheet-cli
@@ -103,8 +103,14 @@ List user-visible workbooks:
103
103
 
104
104
  ```bash
105
105
  mbs workbook list-user-workbooks --limit 20
106
+ mbs workbook list-user-workbooks --order-by filename:asc --limit 20
107
+ mbs workbook delete --doc-id abc123 --yes
106
108
  ```
107
109
 
110
+ The default ordering is `updated_at DESC`. Use `--order-by FIELD[:DIRECTION]`
111
+ with `created_at`, `updated_at`, `filename`, `file_size`, or `version`; the
112
+ direction can be `asc` or `desc` and defaults to `asc` for an explicit field.
113
+
108
114
  Read a worksheet:
109
115
 
110
116
  ```bash
@@ -476,7 +482,8 @@ worksheet metadata, then routes by engine: Sheet worksheets get a frozen
476
482
  header row, persisted auto-filter, header styling, and semantic column styles
477
483
  for date, currency, percent, integer, status, and text columns; Base
478
484
  worksheets get equivalent field formatter/style metadata updates through the
479
- batched `/db_table/field/batch_update` route. The semantic classifier uses
485
+ batched field-style route. Explicit Base column-width updates use the native
486
+ `/table/field/update` route and persist as `field.property.style.width`. The semantic classifier uses
480
487
  weighted Chinese/English header keywords, common business
481
488
  abbreviations such as AOV/GMV/ARR/MRR, Base field display metadata, and Excel
482
489
  sample values. Use `--table-id` to beautify one Excel table within a multi-table
@@ -486,18 +493,12 @@ inventory, fulfillment status, 毛利率, 广告花费, 退款率, and 履约状
486
493
  `--dry-run --output json` to inspect each column's category, format, confidence,
487
494
  and reasons before applying styles.
488
495
 
489
- For explicit Base/db-table field updates, use one JSON array with
490
- `mbs db-table field batch-update` instead of many single-field calls:
491
-
492
- ```json
493
- [
494
- {"name": "revenue", "logical_type": "number", "property": {"formatter": "$#,##0.00"}},
495
- {"name": "margin_rate", "logical_type": "number", "property": {"formatter": "0.00%"}}
496
- ]
497
- ```
496
+ For explicit Base field updates, resolve fields with `base-table field list`,
497
+ then use native `base-table field update`:
498
498
 
499
499
  ```bash
500
- mbs db-table field batch-update --doc-id abc123 --name orders_large --updates field-updates.json --verify
500
+ mbs base-table field list --doc-id abc123 --table-id tbl_orders
501
+ mbs base-table field update --doc-id abc123 --table-id tbl_orders --field-id fld_revenue --formatter '$#,##0.00' --width 144 --verify
501
502
  ```
502
503
 
503
504
  For chart specs authored as `type: "json"`, keep metadata `title` when the chart
@@ -538,6 +539,7 @@ mbs
538
539
  │ ├── list-user-workbooks List the caller's workbooks with a bounded result
539
540
  │ ├── list-worksheets List worksheets and their engines in one workbook
540
541
  │ ├── search Search the caller's workbooks by query
542
+ │ ├── delete Soft-delete a workbook with explicit confirmation
541
543
  │ └── metadata Inspect workbook routing and worksheet metadata
542
544
  │
543
545
  ├── worksheet
@@ -703,6 +705,7 @@ mbs workbook copy --doc-id abc123 --title "Budget Copy"
703
705
  mbs workbook export --doc-id abc123 --out workbook.xlsx
704
706
  mbs workbook calculate --doc-id abc123
705
707
  mbs workbook list-user-workbooks --limit 20
708
+ mbs workbook list-user-workbooks --order-by updated_at:desc --limit 20
706
709
  mbs workbook list-worksheets --doc-id abc123
707
710
  mbs workbook search --query "Budget" --limit 20
708
711
  mbs workbook metadata --doc-id abc123
@@ -756,7 +759,7 @@ mbs base-table record create --doc-id abc123 --table-id tbl_orders --records rec
756
759
  mbs base-table record replace --doc-id abc123 --table-id tbl_orders --records records.json --expected-revision 7
757
760
  mbs base-table record upsert --doc-id abc123 --table-id tbl_orders --key-field-id fld_sku --records records.json
758
761
  mbs base-table field list --doc-id abc123 --table-id tbl_orders
759
- mbs base-table field metadata --doc-id abc123 --name ActualsTable
762
+ mbs base-table field list --doc-id abc123 --table-id tbl_orders
760
763
  mbs base-table field update --doc-id abc123 --table-id tbl_orders --field-id fld_amount --formatter "#,##0.00" --expected-revision 7 --verify
761
764
  mbs base-table field update --doc-id abc123 --name Orders --field amount --formatter "#,##0.00" --verify # resolves native table and field IDs
762
765
  mbs base-table formula compile --doc-id abc123 --table-id tbl_orders --field-id fld_total --expression '{fld_qty} * {fld_price}'
@@ -765,7 +768,7 @@ mbs base-table formula recalculate --doc-id abc123 --table-id tbl_orders --works
765
768
 
766
769
  # db-table
767
770
  mbs db-table read --doc-id abc123 --name orders-large --limit 100 --offset 200
768
- mbs db-table field batch-update --doc-id abc123 --name ActualsTable --updates field-updates.json
771
+ mbs db-table field batch-update --doc-id abc123 --name ActualsTable --updates field-updates.json # compatibility only
769
772
 
770
773
  # excel-worksheet row
771
774
  mbs excel-worksheet row insert --doc-id abc123 --worksheet-name Actuals --row 10 --count 2
@@ -930,8 +933,8 @@ Relationship guide:
930
933
  worksheet style planning/apply. `mbs worksheet style ...` and `mbs style ...`
931
934
  expose the same style commands. For Base worksheets, `style format`,
932
935
  `style columns-width`, and `style cell batch-set` automatically update field
933
- metadata (`formatter`, text color, background color, and width) instead of
934
- requiring agents to choose Base-specific commands.
936
+ metadata (`formatter`, text color, background color, and width) through the
937
+ native field API. Base width is stored at `field.property.style.width`.
935
938
  - Use `excel-worksheet chart` for list/get/create/update/delete of individual
936
939
  source-backed visualizations.
937
940
  - Use `excel-worksheet dashboard` for local validation and worksheet-level chart
@@ -79,8 +79,14 @@ List user-visible workbooks:
79
79
 
80
80
  ```bash
81
81
  mbs workbook list-user-workbooks --limit 20
82
+ mbs workbook list-user-workbooks --order-by filename:asc --limit 20
83
+ mbs workbook delete --doc-id abc123 --yes
82
84
  ```
83
85
 
86
+ The default ordering is `updated_at DESC`. Use `--order-by FIELD[:DIRECTION]`
87
+ with `created_at`, `updated_at`, `filename`, `file_size`, or `version`; the
88
+ direction can be `asc` or `desc` and defaults to `asc` for an explicit field.
89
+
84
90
  Read a worksheet:
85
91
 
86
92
  ```bash
@@ -452,7 +458,8 @@ worksheet metadata, then routes by engine: Sheet worksheets get a frozen
452
458
  header row, persisted auto-filter, header styling, and semantic column styles
453
459
  for date, currency, percent, integer, status, and text columns; Base
454
460
  worksheets get equivalent field formatter/style metadata updates through the
455
- batched `/db_table/field/batch_update` route. The semantic classifier uses
461
+ batched field-style route. Explicit Base column-width updates use the native
462
+ `/table/field/update` route and persist as `field.property.style.width`. The semantic classifier uses
456
463
  weighted Chinese/English header keywords, common business
457
464
  abbreviations such as AOV/GMV/ARR/MRR, Base field display metadata, and Excel
458
465
  sample values. Use `--table-id` to beautify one Excel table within a multi-table
@@ -462,18 +469,12 @@ inventory, fulfillment status, 毛利率, 广告花费, 退款率, and 履约状
462
469
  `--dry-run --output json` to inspect each column's category, format, confidence,
463
470
  and reasons before applying styles.
464
471
 
465
- For explicit Base/db-table field updates, use one JSON array with
466
- `mbs db-table field batch-update` instead of many single-field calls:
467
-
468
- ```json
469
- [
470
- {"name": "revenue", "logical_type": "number", "property": {"formatter": "$#,##0.00"}},
471
- {"name": "margin_rate", "logical_type": "number", "property": {"formatter": "0.00%"}}
472
- ]
473
- ```
472
+ For explicit Base field updates, resolve fields with `base-table field list`,
473
+ then use native `base-table field update`:
474
474
 
475
475
  ```bash
476
- mbs db-table field batch-update --doc-id abc123 --name orders_large --updates field-updates.json --verify
476
+ mbs base-table field list --doc-id abc123 --table-id tbl_orders
477
+ mbs base-table field update --doc-id abc123 --table-id tbl_orders --field-id fld_revenue --formatter '$#,##0.00' --width 144 --verify
477
478
  ```
478
479
 
479
480
  For chart specs authored as `type: "json"`, keep metadata `title` when the chart
@@ -514,6 +515,7 @@ mbs
514
515
  │ ├── list-user-workbooks List the caller's workbooks with a bounded result
515
516
  │ ├── list-worksheets List worksheets and their engines in one workbook
516
517
  │ ├── search Search the caller's workbooks by query
518
+ │ ├── delete Soft-delete a workbook with explicit confirmation
517
519
  │ └── metadata Inspect workbook routing and worksheet metadata
518
520
  │
519
521
  ├── worksheet
@@ -679,6 +681,7 @@ mbs workbook copy --doc-id abc123 --title "Budget Copy"
679
681
  mbs workbook export --doc-id abc123 --out workbook.xlsx
680
682
  mbs workbook calculate --doc-id abc123
681
683
  mbs workbook list-user-workbooks --limit 20
684
+ mbs workbook list-user-workbooks --order-by updated_at:desc --limit 20
682
685
  mbs workbook list-worksheets --doc-id abc123
683
686
  mbs workbook search --query "Budget" --limit 20
684
687
  mbs workbook metadata --doc-id abc123
@@ -732,7 +735,7 @@ mbs base-table record create --doc-id abc123 --table-id tbl_orders --records rec
732
735
  mbs base-table record replace --doc-id abc123 --table-id tbl_orders --records records.json --expected-revision 7
733
736
  mbs base-table record upsert --doc-id abc123 --table-id tbl_orders --key-field-id fld_sku --records records.json
734
737
  mbs base-table field list --doc-id abc123 --table-id tbl_orders
735
- mbs base-table field metadata --doc-id abc123 --name ActualsTable
738
+ mbs base-table field list --doc-id abc123 --table-id tbl_orders
736
739
  mbs base-table field update --doc-id abc123 --table-id tbl_orders --field-id fld_amount --formatter "#,##0.00" --expected-revision 7 --verify
737
740
  mbs base-table field update --doc-id abc123 --name Orders --field amount --formatter "#,##0.00" --verify # resolves native table and field IDs
738
741
  mbs base-table formula compile --doc-id abc123 --table-id tbl_orders --field-id fld_total --expression '{fld_qty} * {fld_price}'
@@ -741,7 +744,7 @@ mbs base-table formula recalculate --doc-id abc123 --table-id tbl_orders --works
741
744
 
742
745
  # db-table
743
746
  mbs db-table read --doc-id abc123 --name orders-large --limit 100 --offset 200
744
- mbs db-table field batch-update --doc-id abc123 --name ActualsTable --updates field-updates.json
747
+ mbs db-table field batch-update --doc-id abc123 --name ActualsTable --updates field-updates.json # compatibility only
745
748
 
746
749
  # excel-worksheet row
747
750
  mbs excel-worksheet row insert --doc-id abc123 --worksheet-name Actuals --row 10 --count 2
@@ -906,8 +909,8 @@ Relationship guide:
906
909
  worksheet style planning/apply. `mbs worksheet style ...` and `mbs style ...`
907
910
  expose the same style commands. For Base worksheets, `style format`,
908
911
  `style columns-width`, and `style cell batch-set` automatically update field
909
- metadata (`formatter`, text color, background color, and width) instead of
910
- requiring agents to choose Base-specific commands.
912
+ metadata (`formatter`, text color, background color, and width) through the
913
+ native field API. Base width is stored at `field.property.style.width`.
911
914
  - Use `excel-worksheet chart` for list/get/create/update/delete of individual
912
915
  source-backed visualizations.
913
916
  - Use `excel-worksheet dashboard` for local validation and worksheet-level chart
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "maybeai-sheet-cli"
7
- version = "0.26"
7
+ version = "0.26.2"
8
8
  description = "CLI for common MaybeAI spreadsheet operations"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -19,6 +19,26 @@ _A1_RANGE_RE = re.compile(r"^([A-Za-z]+[1-9][0-9]*):([A-Za-z]+[1-9][0-9]*)$")
19
19
  _TRUE_VALUES = frozenset({"1", "true", "t", "yes", "y", "on"})
20
20
  _FALSE_VALUES = frozenset({"0", "false", "f", "no", "n", "off"})
21
21
  _LINEAGE_MAX_RANGE_CELLS = 100
22
+ _PIXEL_DIMENSION_RE = re.compile(r"^(?P<value>\d+(?:\.\d+)?)px$", re.IGNORECASE)
23
+
24
+
25
+ def parse_pixel_dimension(raw: str, *, name: str, example: str = "140px") -> tuple[float, str | None]:
26
+ """Parse new px syntax while preserving legacy numeric CLI input."""
27
+ value = raw.strip()
28
+ match = _PIXEL_DIMENSION_RE.fullmatch(value)
29
+ if match:
30
+ return float(match.group("value")), "px"
31
+ try:
32
+ numeric = float(value)
33
+ except ValueError as exc:
34
+ raise UsageError(f"{name} must be a number or a pixel value such as {example}.") from exc
35
+ if numeric <= 0:
36
+ raise UsageError(f"{name} must be greater than 0.")
37
+ typer.echo(
38
+ f"Warning: numeric {name} uses legacy units; use {name} with an explicit px suffix (for example {example}).",
39
+ err=True,
40
+ )
41
+ return numeric, None
22
42
 
23
43
 
24
44
  def run_post_command(
@@ -14,6 +14,7 @@ from ._phase2 import (
14
14
  common_options,
15
15
  require_worksheet,
16
16
  require_yes_or_dry_run,
17
+ parse_pixel_dimension,
17
18
  run_post_command,
18
19
  )
19
20
 
@@ -186,7 +187,7 @@ def width(
186
187
  worksheet_name: Optional[str] = typer.Option(None, "--worksheet-name", help="Worksheet name."),
187
188
  start_column: str = typer.Option(..., "--start-column", help="A1-style first column."),
188
189
  end_column: str = typer.Option(..., "--end-column", help="A1-style last column."),
189
- width: int = typer.Option(..., "--width", min=1, help="Column width."),
190
+ width: str = typer.Option(..., "--width", help="Column width, for example 140px. Bare numbers remain legacy-compatible."),
190
191
  dry_run: bool = typer.Option(False, "--dry-run", help="Show the request without sending it."),
191
192
  output: Optional[str] = output_option(),
192
193
  verbose: Optional[bool] = verbose_option(),
@@ -203,11 +204,14 @@ def width(
203
204
  verbose=verbose,
204
205
  timeout=timeout,
205
206
  )
207
+ width_value, width_unit = parse_pixel_dimension(width, name="--width")
206
208
  payload = base_payload(target) | {
207
209
  "start_column": start_column,
208
210
  "end_column": end_column,
209
- "width": width,
211
+ "width": width_value,
210
212
  }
213
+ if width_unit:
214
+ payload["width_unit"] = width_unit
211
215
  run_post_command(
212
216
  state=state,
213
217
  endpoint="/api/v1/excel/set_columns_width",
@@ -603,12 +603,75 @@ def _first_tabular_value(value: Any) -> Any:
603
603
  return None
604
604
 
605
605
 
606
+ def _logical_type_from_pg_type(pg_type: str) -> str:
607
+ normalized = pg_type.strip().lower()
608
+ if normalized.startswith(("smallint", "integer", "bigint", "numeric", "real", "double precision")):
609
+ return "number"
610
+ if normalized == "boolean":
611
+ return "boolean"
612
+ if normalized.startswith(("date", "timestamp")):
613
+ return "datetime"
614
+ return "text"
615
+
616
+
617
+ def _typed_create_columns(response: dict[str, Any], headers: list[str]) -> list[dict[str, Any]] | None:
618
+ schema = response.get("columns")
619
+ if schema is None:
620
+ return None
621
+ if not isinstance(schema, list):
622
+ raise UsageError("SQL query response columns must be an array.")
623
+ if schema and all(isinstance(column, str) for column in schema):
624
+ return None
625
+ if not all(isinstance(column, dict) for column in schema):
626
+ raise UsageError("SQL query response columns must contain typed column objects.")
627
+
628
+ typed_columns: list[dict[str, Any]] = []
629
+ schema_names: list[str] = []
630
+ for index, column in enumerate(schema):
631
+ name = column.get("name")
632
+ pg_type = column.get("pg_type")
633
+ if not isinstance(name, str) or not name.strip():
634
+ raise UsageError("SQL query typed column schema must include non-empty names.")
635
+ if not isinstance(pg_type, str) or not pg_type.strip():
636
+ raise UsageError("SQL query typed column schema must include pg_type for every column.")
637
+ name = name.strip()
638
+ schema_names.append(name)
639
+ typed_columns.append(
640
+ {
641
+ "name": name,
642
+ "logical_type": _logical_type_from_pg_type(pg_type),
643
+ "pg_type": pg_type,
644
+ "index": index,
645
+ }
646
+ )
647
+ if len(set(schema_names)) != len(schema_names):
648
+ raise UsageError("SQL query typed column schema contains duplicate column names.")
649
+ if schema_names != headers:
650
+ raise UsageError("SQL query typed column schema does not match the result header row.")
651
+ return typed_columns
652
+
653
+
654
+ def _row_headers(rows: list[dict[str, Any]]) -> list[str]:
655
+ headers: list[str] = []
656
+ seen: set[str] = set()
657
+ for row in rows:
658
+ for key in row:
659
+ name = str(key).strip()
660
+ if name and name not in seen:
661
+ seen.add(name)
662
+ headers.append(name)
663
+ return headers
664
+
665
+
606
666
  def _rows_from_tabular_response(response: dict[str, Any]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
607
667
  table = _first_tabular_value(response)
608
668
  if table is None:
609
669
  raise UsageError("SQL query response did not contain tabular values.")
610
670
  if isinstance(table, list) and all(isinstance(row, dict) for row in table):
611
671
  rows = [dict(row) for row in table]
672
+ typed_columns = _typed_create_columns(response, _row_headers(rows))
673
+ if typed_columns is not None:
674
+ return rows, typed_columns
612
675
  columns = _infer_create_columns(rows)
613
676
  return rows, columns
614
677
  if not isinstance(table, list) or not all(isinstance(row, list) for row in table):
@@ -627,6 +690,9 @@ def _rows_from_tabular_response(response: dict[str, Any]) -> tuple[list[dict[str
627
690
  for idx, header in enumerate(headers)
628
691
  }
629
692
  rows.append(row)
693
+ typed_columns = _typed_create_columns(response, headers)
694
+ if typed_columns is not None:
695
+ return rows, typed_columns
630
696
  columns = [
631
697
  {
632
698
  "name": header,
@@ -29,6 +29,7 @@ from ._phase2 import (
29
29
  load_json_object_argument,
30
30
  parse_bool_text,
31
31
  parse_optional_bool_text,
32
+ parse_pixel_dimension,
32
33
  require_worksheet,
33
34
  run_post_command,
34
35
  )
@@ -71,6 +72,7 @@ _BEAUTIFY_COLUMN_POLICY: dict[str, object] = {
71
72
  }
72
73
  _COLUMN_BACKGROUND_STYLE_KEYS = frozenset({"bg_color", "backgroundColor", "fillColor", "background_color", "fill_color"})
73
74
  _COLUMN_FONT_COLOR_STYLE_KEYS = frozenset({"font_color", "color", "fontColor", "text_color", "textColor"})
75
+ _COLUMN_FORMAT_STYLE_KEYS = frozenset({"format_code", "formatCode"})
74
76
  _BEAUTIFY_CONFIG_KEYS = frozenset(
75
77
  {
76
78
  "defaults",
@@ -1510,12 +1512,15 @@ def _column_style_for_policy(style: dict[str, object], style_policy: dict[str, o
1510
1512
  column_policy = _column_policy_from_style_policy(style_policy)
1511
1513
  allow_font_color = bool(column_policy.get("allowFontColorBySemanticType"))
1512
1514
  allow_background = bool(column_policy.get("backgroundBySemanticType"))
1515
+ allow_format = bool(column_policy.get("allowFormatBySemanticType", True))
1513
1516
  cleaned: dict[str, object] = {}
1514
1517
  for key, value in style.items():
1515
1518
  if key in _COLUMN_BACKGROUND_STYLE_KEYS and not allow_background:
1516
1519
  continue
1517
1520
  if key in _COLUMN_FONT_COLOR_STYLE_KEYS and not allow_font_color:
1518
1521
  continue
1522
+ if key in _COLUMN_FORMAT_STYLE_KEYS and not allow_format:
1523
+ continue
1519
1524
  cleaned[key] = value
1520
1525
  return cleaned
1521
1526
 
@@ -2188,14 +2193,13 @@ def _base_beautify_field_update(
2188
2193
  style = _column_style_for_policy(classification.style, style_policy)
2189
2194
  logical_type = _field_logical_type(field, None)
2190
2195
  # Base validates number fields against numeric formatter patterns. A name
2191
- # such as "period" can suggest a date even when the stored field is numeric,
2192
- # so do not apply an incompatible date formatter from name-based inference.
2193
- if classification.category == "date" and logical_type and logical_type.lower() in {
2194
- "number",
2195
- "integer",
2196
- "float",
2197
- "double",
2198
- "decimal",
2196
+ # such as "period" can suggest a date even when its metadata omits the
2197
+ # type or it is stored as a number, so date formatting needs an explicit
2198
+ # date/time logical type rather than header-based inference alone.
2199
+ if classification.category == "date" and (logical_type or "").lower() not in {
2200
+ "date",
2201
+ "datetime",
2202
+ "timestamp",
2199
2203
  }:
2200
2204
  style.pop("format_code", None)
2201
2205
  existing_property = field.get("property")
@@ -2462,6 +2466,33 @@ def _base_field_update_payload(
2462
2466
  return payload
2463
2467
 
2464
2468
 
2469
+ def _base_native_field_width_payload(
2470
+ *,
2471
+ target,
2472
+ gid: int,
2473
+ metadata_response: dict[str, object],
2474
+ field_index: int,
2475
+ width: int,
2476
+ ) -> dict[str, object]:
2477
+ field_metadata, _ = _resolve_field(
2478
+ metadata_response,
2479
+ _resolved_base_target(target, gid),
2480
+ field=None,
2481
+ field_index=field_index,
2482
+ )
2483
+ field_id = field_metadata.get("field_id")
2484
+ if not isinstance(field_id, str) or not field_id.strip():
2485
+ raise UsageError(
2486
+ "Base field metadata did not include field_id; native width update cannot continue."
2487
+ )
2488
+ return {
2489
+ "document_id": target.document_id,
2490
+ "gid": gid,
2491
+ "field_id": field_id,
2492
+ "property": {"style": {"width": width}},
2493
+ }
2494
+
2495
+
2465
2496
  def _render_base_field_update_result(
2466
2497
  *,
2467
2498
  state,
@@ -2993,16 +3024,19 @@ def format_field(
2993
3024
  handle_cli_error(error)
2994
3025
 
2995
3026
 
2996
- @app.command("columns-width")
3027
+ @app.command(
3028
+ "columns-width",
3029
+ help="Set Sheet column widths, or persist Base widths on field.property.style.width.",
3030
+ )
2997
3031
  def columns_width(
2998
3032
  ctx: typer.Context,
2999
3033
  doc_id: Optional[str] = typer.Option(None, "--doc-id", help="MaybeAI document ID."),
3000
3034
  url: Optional[str] = typer.Option(None, "--url", help="MaybeAI workbook URL."),
3001
3035
  uri: Optional[str] = typer.Option(None, "--uri", help="Resolved workbook URI."),
3002
3036
  worksheet_name: Optional[str] = typer.Option(None, "--worksheet-name", help="Worksheet name."),
3003
- start_column: str = typer.Option(..., "--start-column", help="A1-style first column."),
3004
- end_column: str = typer.Option(..., "--end-column", help="A1-style last column."),
3005
- width: int = typer.Option(..., "--width", min=1, help="Column width."),
3037
+ start_column: str = typer.Option(..., "--start-column", help="A1-style first column (resolved to a Base field in Base mode)."),
3038
+ end_column: str = typer.Option(..., "--end-column", help="A1-style last column (resolved to a Base field in Base mode)."),
3039
+ width: str = typer.Option(..., "--width", help="Column width in pixels, for example 140px. Bare numbers remain legacy-compatible."),
3006
3040
  dry_run: bool = typer.Option(False, "--dry-run", help="Show the request without sending it."),
3007
3041
  output: Optional[str] = output_option(),
3008
3042
  verbose: Optional[bool] = verbose_option(),
@@ -3019,11 +3053,14 @@ def columns_width(
3019
3053
  verbose=verbose,
3020
3054
  timeout=timeout,
3021
3055
  )
3056
+ width_value, width_unit = parse_pixel_dimension(width, name="--width")
3022
3057
  payload = base_payload(target) | {
3023
3058
  "start_column": start_column,
3024
3059
  "end_column": end_column,
3025
- "width": width,
3060
+ "width": width_value,
3026
3061
  }
3062
+ if width_unit:
3063
+ payload["width_unit"] = width_unit
3027
3064
  if not dry_run:
3028
3065
  client = MaybeAIClient(state)
3029
3066
  engine, gid, _worksheet = _resolve_worksheet_engine(client=client, target=target)
@@ -3037,22 +3074,17 @@ def columns_width(
3037
3074
  last_index = max(start_index, end_index)
3038
3075
  updates: list[dict[str, object]] = []
3039
3076
  for field_index in range(first_index, last_index + 1):
3040
- update_payload = _base_field_update_payload(
3077
+ update_payload = _base_native_field_width_payload(
3041
3078
  target=target,
3042
3079
  gid=gid,
3043
3080
  metadata_response=metadata_response,
3044
- field=None,
3045
3081
  field_index=field_index,
3046
- logical_type=None,
3047
- formatter=None,
3048
- color=None,
3049
- background_color=None,
3050
- width=width,
3082
+ width=int(round(width_value)),
3051
3083
  )
3052
- updates.append(_normalize_base_mode_output(client.post_json("/api/v1/excel/db_table/field/update", update_payload)))
3084
+ updates.append(_normalize_base_mode_output(client.post_json("/api/v1/excel/table/field/update", update_payload)))
3053
3085
  render_output(
3054
3086
  build_command_output(
3055
- endpoint="/api/v1/excel/db_table/field/update",
3087
+ endpoint="/api/v1/excel/table/field/update",
3056
3088
  target=target,
3057
3089
  result={
3058
3090
  "success": all(bool(update.get("success", True)) for update in updates),
@@ -3093,7 +3125,7 @@ def rows_height(
3093
3125
  worksheet_name: Optional[str] = typer.Option(None, "--worksheet-name", help="Worksheet name."),
3094
3126
  start_row: int = typer.Option(..., "--start-row", min=1, help="1-based first row."),
3095
3127
  end_row: int = typer.Option(..., "--end-row", min=1, help="1-based last row."),
3096
- height: int = typer.Option(..., "--height", min=1, help="Row height."),
3128
+ height: str = typer.Option(..., "--height", help="Row height in pixels, for example 32px. Bare numbers remain legacy-compatible."),
3097
3129
  dry_run: bool = typer.Option(False, "--dry-run", help="Show the request without sending it."),
3098
3130
  output: Optional[str] = output_option(),
3099
3131
  verbose: Optional[bool] = verbose_option(),
@@ -3110,11 +3142,14 @@ def rows_height(
3110
3142
  verbose=verbose,
3111
3143
  timeout=timeout,
3112
3144
  )
3145
+ height_value, height_unit = parse_pixel_dimension(height, name="--height", example="32px")
3113
3146
  payload = base_payload(target) | {
3114
3147
  "start_row": start_row,
3115
3148
  "end_row": end_row,
3116
- "height": height,
3149
+ "height": height_value,
3117
3150
  }
3151
+ if height_unit:
3152
+ payload["height_unit"] = height_unit
3118
3153
  run_post_command(
3119
3154
  state=state,
3120
3155
  endpoint="/api/v1/excel/set_rows_height",
@@ -16,7 +16,7 @@ from ..formatters import build_command_output, handle_cli_error, load_json_file,
16
16
  from ..models.workbook import CreateWorkbookRequest
17
17
  from ..options import output_option, resolve_command_state, timeout_option, verbose_option
18
18
  from ..resolver import ResolvedTarget, build_document_url, parse_document_target, resolve_target
19
- from ._phase2 import bounded_limit
19
+ from ._phase2 import bounded_limit, require_yes_or_dry_run, run_post_command
20
20
  from .db_table import (
21
21
  _adopt_existing_create_response,
22
22
  _created_table_target,
@@ -42,6 +42,8 @@ _WORKSHEET_DIMENSIONS_ENDPOINT = "/api/v1/excel/worksheet/dimensions"
42
42
  _COPY_WORKSHEET_ENDPOINT = "/api/v1/excel/copy_worksheet"
43
43
  _GOOGLE_SHEET_HOST = "docs.google.com"
44
44
  _GOOGLE_SHEET_PATH_MARKER = "/spreadsheets/d/"
45
+ _WORKBOOK_ORDER_FIELDS = ("created_at", "file_size", "filename", "updated_at", "version")
46
+ _WORKBOOK_ORDER_DIRECTIONS = ("asc", "desc")
45
47
 
46
48
 
47
49
  class _EmptySourceWorksheetError(UsageError):
@@ -153,6 +155,23 @@ def _normalize_engine_token(value: str) -> str:
153
155
  )
154
156
 
155
157
 
158
+ def _normalize_workbook_order(value: str | None) -> tuple[str, str]:
159
+ if value is None or not value.strip():
160
+ return "updated_at", "desc"
161
+ parts = [part.strip().lower() for part in value.split(":")]
162
+ if len(parts) > 2 or not parts[0]:
163
+ raise UsageError("--order-by must use FIELD[:DIRECTION]")
164
+ field = parts[0]
165
+ if field not in _WORKBOOK_ORDER_FIELDS:
166
+ raise UsageError(
167
+ "--order-by field must be one of: " + ", ".join(_WORKBOOK_ORDER_FIELDS)
168
+ )
169
+ direction = parts[1] if len(parts) == 2 and parts[1] else "asc"
170
+ if direction not in _WORKBOOK_ORDER_DIRECTIONS:
171
+ raise UsageError("--order-by direction must be one of: asc, desc")
172
+ return field, direction
173
+
174
+
156
175
  def _normalize_import_source_engine(engine: str | None) -> str:
157
176
  if engine is None:
158
177
  return "sheet"
@@ -2251,6 +2270,11 @@ def calculate(
2251
2270
  def list_workbooks(
2252
2271
  ctx: typer.Context,
2253
2272
  limit: int = typer.Option(20, "--limit", min=1, help="Maximum workbooks to return."),
2273
+ order_by: Optional[str] = typer.Option(
2274
+ None,
2275
+ "--order-by",
2276
+ help="Sort workbooks by FIELD[:DIRECTION]. Default: updated_at:desc.",
2277
+ ),
2254
2278
  output: Optional[str] = output_option(),
2255
2279
  verbose: Optional[bool] = verbose_option(),
2256
2280
  timeout: Optional[float] = timeout_option(),
@@ -2263,7 +2287,12 @@ def list_workbooks(
2263
2287
  timeout=timeout,
2264
2288
  )
2265
2289
  endpoint = "/api/v1/excel/list_files"
2266
- payload = {"limit": bounded_limit(limit, option="--limit")}
2290
+ sort_field, sort_direction = _normalize_workbook_order(order_by)
2291
+ payload = {
2292
+ "limit": bounded_limit(limit, option="--limit"),
2293
+ "order_by": sort_field,
2294
+ "order_direction": sort_direction,
2295
+ }
2267
2296
  response = MaybeAIClient(state).post_json(endpoint, payload)
2268
2297
  render_output(
2269
2298
  build_command_output(
@@ -2277,10 +2306,53 @@ def list_workbooks(
2277
2306
  handle_cli_error(error)
2278
2307
 
2279
2308
 
2309
+ @app.command("delete")
2310
+ def delete_workbook(
2311
+ ctx: typer.Context,
2312
+ doc_id: Optional[str] = typer.Option(None, "--doc-id", help="MaybeAI document ID."),
2313
+ url: Optional[str] = typer.Option(None, "--url", help="MaybeAI workbook URL."),
2314
+ uri: Optional[str] = typer.Option(None, "--uri", help="Resolved workbook URI."),
2315
+ yes: bool = typer.Option(False, "--yes", help="Confirm destructive workbook deletion."),
2316
+ dry_run: bool = typer.Option(False, "--dry-run", help="Show the request without sending it."),
2317
+ output: Optional[str] = output_option(),
2318
+ verbose: Optional[bool] = verbose_option(),
2319
+ timeout: Optional[float] = timeout_option(),
2320
+ ) -> None:
2321
+ """Soft-delete a workbook."""
2322
+ try:
2323
+ require_yes_or_dry_run(yes=yes, dry_run=dry_run)
2324
+ state: CLIContext = resolve_command_state(
2325
+ ctx,
2326
+ doc_id=doc_id,
2327
+ url=url,
2328
+ uri=uri,
2329
+ output=output,
2330
+ verbose=verbose,
2331
+ timeout=timeout,
2332
+ )
2333
+ target = resolve_target(state)
2334
+ endpoint = "/api/v1/excel/delete_file"
2335
+ run_post_command(
2336
+ state=state,
2337
+ endpoint=endpoint,
2338
+ payload={"uri": target.uri},
2339
+ target=target,
2340
+ dry_run=dry_run,
2341
+ command_id="workbook.delete",
2342
+ )
2343
+ except Exception as error: # pragma: no cover - CLI boundary
2344
+ handle_cli_error(error)
2345
+
2346
+
2280
2347
  @app.command("list-user-workbooks")
2281
2348
  def list_user_workbooks(
2282
2349
  ctx: typer.Context,
2283
2350
  limit: int = typer.Option(20, "--limit", min=1, help="Maximum workbooks to return."),
2351
+ order_by: Optional[str] = typer.Option(
2352
+ None,
2353
+ "--order-by",
2354
+ help="Sort workbooks by FIELD[:DIRECTION]. Default: updated_at:desc.",
2355
+ ),
2284
2356
  output: Optional[str] = output_option(),
2285
2357
  verbose: Optional[bool] = verbose_option(),
2286
2358
  timeout: Optional[float] = timeout_option(),
@@ -2288,6 +2360,7 @@ def list_user_workbooks(
2288
2360
  list_workbooks(
2289
2361
  ctx,
2290
2362
  limit=limit,
2363
+ order_by=order_by,
2291
2364
  output=output,
2292
2365
  verbose=verbose,
2293
2366
  timeout=timeout,