@revos/cli 0.3.1 → 0.3.4

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 (87) hide show
  1. package/README.md +2 -2
  2. package/dist/adapters/oclif/commands/action-runs/get.mjs +1 -1
  3. package/dist/adapters/oclif/commands/action-runs/list.mjs +1 -1
  4. package/dist/adapters/oclif/commands/actions/get-input-schema.mjs +2 -2
  5. package/dist/adapters/oclif/commands/actions/get-params-schema.mjs +2 -2
  6. package/dist/adapters/oclif/commands/actions/get.mjs +1 -1
  7. package/dist/adapters/oclif/commands/actions/list.mjs +2 -2
  8. package/dist/adapters/oclif/commands/ai-instructions/create.mjs +1 -1
  9. package/dist/adapters/oclif/commands/ai-instructions/delete.mjs +1 -1
  10. package/dist/adapters/oclif/commands/ai-instructions/get.mjs +1 -1
  11. package/dist/adapters/oclif/commands/ai-instructions/list.mjs +1 -1
  12. package/dist/adapters/oclif/commands/ai-instructions/update.mjs +1 -1
  13. package/dist/adapters/oclif/commands/api.mjs +2 -2
  14. package/dist/adapters/oclif/commands/apply.mjs +2 -2
  15. package/dist/adapters/oclif/commands/auth/login.mjs +2 -2
  16. package/dist/adapters/oclif/commands/auth/logout.mjs +2 -2
  17. package/dist/adapters/oclif/commands/auth/status.mjs +2 -2
  18. package/dist/adapters/oclif/commands/connections/create.mjs +1 -1
  19. package/dist/adapters/oclif/commands/connections/delete.mjs +1 -1
  20. package/dist/adapters/oclif/commands/connections/get.mjs +1 -1
  21. package/dist/adapters/oclif/commands/connections/list.mjs +1 -1
  22. package/dist/adapters/oclif/commands/connections/update.mjs +1 -1
  23. package/dist/adapters/oclif/commands/cubes/create.mjs +1 -1
  24. package/dist/adapters/oclif/commands/cubes/delete.mjs +1 -1
  25. package/dist/adapters/oclif/commands/cubes/get.mjs +1 -1
  26. package/dist/adapters/oclif/commands/cubes/list.mjs +1 -1
  27. package/dist/adapters/oclif/commands/cubes/meta.mjs +2 -2
  28. package/dist/adapters/oclif/commands/cubes/query.mjs +2 -2
  29. package/dist/adapters/oclif/commands/cubes/update.mjs +1 -1
  30. package/dist/adapters/oclif/commands/diff.mjs +2 -2
  31. package/dist/adapters/oclif/commands/gservice-account-keys/get.mjs +1 -1
  32. package/dist/adapters/oclif/commands/gservice-account-keys/reveal.mjs +2 -2
  33. package/dist/adapters/oclif/commands/gservice-accounts/create.mjs +1 -1
  34. package/dist/adapters/oclif/commands/gservice-accounts/delete.mjs +1 -1
  35. package/dist/adapters/oclif/commands/gservice-accounts/get.mjs +1 -1
  36. package/dist/adapters/oclif/commands/gservice-accounts/list.mjs +1 -1
  37. package/dist/adapters/oclif/commands/init.mjs +2 -2
  38. package/dist/adapters/oclif/commands/org/create.mjs +1 -1
  39. package/dist/adapters/oclif/commands/org/current.mjs +2 -2
  40. package/dist/adapters/oclif/commands/org/get.mjs +1 -1
  41. package/dist/adapters/oclif/commands/org/list.mjs +2 -2
  42. package/dist/adapters/oclif/commands/org/switch.mjs +2 -2
  43. package/dist/adapters/oclif/commands/pull.mjs +2 -2
  44. package/dist/adapters/oclif/commands/score-groups/create.mjs +1 -1
  45. package/dist/adapters/oclif/commands/score-groups/delete.mjs +1 -1
  46. package/dist/adapters/oclif/commands/score-groups/get.mjs +1 -1
  47. package/dist/adapters/oclif/commands/score-groups/list.mjs +1 -1
  48. package/dist/adapters/oclif/commands/score-groups/update.mjs +1 -1
  49. package/dist/adapters/oclif/commands/scores/create.mjs +1 -1
  50. package/dist/adapters/oclif/commands/scores/delete.mjs +1 -1
  51. package/dist/adapters/oclif/commands/scores/list.mjs +1 -1
  52. package/dist/adapters/oclif/commands/scores/update.mjs +1 -1
  53. package/dist/adapters/oclif/commands/segments/create.mjs +1 -1
  54. package/dist/adapters/oclif/commands/segments/delete.mjs +1 -1
  55. package/dist/adapters/oclif/commands/segments/evaluate.mjs +2 -2
  56. package/dist/adapters/oclif/commands/segments/get-evaluation-history.mjs +2 -2
  57. package/dist/adapters/oclif/commands/segments/get-version.mjs +2 -2
  58. package/dist/adapters/oclif/commands/segments/get.mjs +1 -1
  59. package/dist/adapters/oclif/commands/segments/list-versions.mjs +2 -2
  60. package/dist/adapters/oclif/commands/segments/list.mjs +1 -1
  61. package/dist/adapters/oclif/commands/segments/restore-version.mjs +2 -2
  62. package/dist/adapters/oclif/commands/segments/update.mjs +1 -1
  63. package/dist/adapters/oclif/commands/sources/create.mjs +2 -2
  64. package/dist/adapters/oclif/commands/sources/delete.mjs +1 -1
  65. package/dist/adapters/oclif/commands/sources/get.mjs +1 -1
  66. package/dist/adapters/oclif/commands/sources/list-streams.mjs +2 -2
  67. package/dist/adapters/oclif/commands/sources/list.mjs +1 -1
  68. package/dist/adapters/oclif/commands/sources/update.mjs +2 -2
  69. package/dist/adapters/oclif/commands/status.mjs +3 -3
  70. package/dist/adapters/oclif/commands/table-views/create.mjs +1 -1
  71. package/dist/adapters/oclif/commands/table-views/delete.mjs +1 -1
  72. package/dist/adapters/oclif/commands/table-views/list.mjs +1 -1
  73. package/dist/adapters/oclif/commands/table-views/update.mjs +1 -1
  74. package/dist/adapters/oclif/commands/tables/create.mjs +1 -1
  75. package/dist/adapters/oclif/commands/tables/delete.mjs +1 -1
  76. package/dist/adapters/oclif/commands/tables/get.mjs +1 -1
  77. package/dist/adapters/oclif/commands/tables/list.mjs +1 -1
  78. package/dist/adapters/oclif/commands/tables/update.mjs +1 -1
  79. package/dist/{base.command-CnVb4RG6.mjs → base.command-CWGrSq73.mjs} +1 -1
  80. package/dist/{core-CY9pC37x.mjs → core-CChCxudp.mjs} +3 -0
  81. package/dist/{factory-DTqayaCF.mjs → factory-SazXfu26.mjs} +2 -2
  82. package/dist/index.mjs +1 -1
  83. package/dist/{presets-mJzFGMhG.mjs → presets-Blw7i6BW.mjs} +2 -2
  84. package/dist/templates/README.md +1 -0
  85. package/dist/templates/skills/create-connections/SKILL.md +4 -2
  86. package/dist/templates/skills/query-semantic-model/SKILL.md +347 -0
  87. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
- import { t as createCommand } from "../../../../presets-mJzFGMhG.mjs";
1
+ import { t as createCommand } from "../../../../presets-Blw7i6BW.mjs";
2
2
  //#region src/adapters/oclif/commands/table-views/create.ts
3
3
  var create_default = createCommand({
4
4
  resource: "tableViews",
@@ -1,4 +1,4 @@
1
- import { n as deleteCommand } from "../../../../presets-mJzFGMhG.mjs";
1
+ import { n as deleteCommand } from "../../../../presets-Blw7i6BW.mjs";
2
2
  //#region src/adapters/oclif/commands/table-views/delete.ts
3
3
  var delete_default = deleteCommand({
4
4
  resource: "tableViews",
@@ -1,4 +1,4 @@
1
- import { i as listCommand } from "../../../../presets-mJzFGMhG.mjs";
1
+ import { i as listCommand } from "../../../../presets-Blw7i6BW.mjs";
2
2
  //#region src/adapters/oclif/commands/table-views/list.ts
3
3
  var list_default = listCommand({
4
4
  resource: "tableViews",
@@ -1,4 +1,4 @@
1
- import { a as updateCommand } from "../../../../presets-mJzFGMhG.mjs";
1
+ import { a as updateCommand } from "../../../../presets-Blw7i6BW.mjs";
2
2
  //#region src/adapters/oclif/commands/table-views/update.ts
3
3
  var update_default = updateCommand({
4
4
  resource: "tableViews",
@@ -1,4 +1,4 @@
1
- import { t as createCommand } from "../../../../presets-mJzFGMhG.mjs";
1
+ import { t as createCommand } from "../../../../presets-Blw7i6BW.mjs";
2
2
  //#region src/adapters/oclif/commands/tables/create.ts
3
3
  var create_default = createCommand({
4
4
  resource: "tables",
@@ -1,4 +1,4 @@
1
- import { n as deleteCommand } from "../../../../presets-mJzFGMhG.mjs";
1
+ import { n as deleteCommand } from "../../../../presets-Blw7i6BW.mjs";
2
2
  //#region src/adapters/oclif/commands/tables/delete.ts
3
3
  var delete_default = deleteCommand({
4
4
  resource: "tables",
@@ -1,4 +1,4 @@
1
- import { r as getCommand } from "../../../../presets-mJzFGMhG.mjs";
1
+ import { r as getCommand } from "../../../../presets-Blw7i6BW.mjs";
2
2
  //#region src/adapters/oclif/commands/tables/get.ts
3
3
  var get_default = getCommand({
4
4
  resource: "tables",
@@ -1,4 +1,4 @@
1
- import { i as listCommand } from "../../../../presets-mJzFGMhG.mjs";
1
+ import { i as listCommand } from "../../../../presets-Blw7i6BW.mjs";
2
2
  //#region src/adapters/oclif/commands/tables/list.ts
3
3
  var list_default = listCommand({
4
4
  resource: "tables",
@@ -1,4 +1,4 @@
1
- import { a as updateCommand } from "../../../../presets-mJzFGMhG.mjs";
1
+ import { a as updateCommand } from "../../../../presets-Blw7i6BW.mjs";
2
2
  //#region src/adapters/oclif/commands/tables/update.ts
3
3
  var update_default = updateCommand({
4
4
  resource: "tables",
@@ -1,4 +1,4 @@
1
- import { G as ApiError, I as setAuthConfig, U as loadCredentials, i as formatEnvMismatchError, l as resolveProjectContext, o as formatProjectOrgFlagError, r as formatCredentialsMismatchWarning, u as validateEnvAgainstProject } from "./core-CY9pC37x.mjs";
1
+ import { G as ApiError, I as setAuthConfig, U as loadCredentials, i as formatEnvMismatchError, l as resolveProjectContext, o as formatProjectOrgFlagError, r as formatCredentialsMismatchWarning, u as validateEnvAgainstProject } from "./core-CChCxudp.mjs";
2
2
  import { Command, Flags } from "@oclif/core";
3
3
  import { makeTable } from "@oclif/table";
4
4
  //#region src/adapters/oclif/base.command.ts
@@ -2254,6 +2254,7 @@ var InitService = class InitService {
2254
2254
  ".claude/skills/create-dbt-transformations",
2255
2255
  ".claude/skills/create-dbt-transformations/references",
2256
2256
  ".claude/skills/load-sample-data",
2257
+ ".claude/skills/query-semantic-model",
2257
2258
  ".claude/skills/visualize-semantic-model",
2258
2259
  ".claude/skills/visualize-semantic-model/scripts",
2259
2260
  "dbt/models/bronze",
@@ -2292,6 +2293,7 @@ var InitService = class InitService {
2292
2293
  ".claude/skills/create-dbt-transformations/references/schema-conventions.md",
2293
2294
  ".claude/skills/create-dbt-transformations/references/edge-cases.md",
2294
2295
  ".claude/skills/load-sample-data/SKILL.md",
2296
+ ".claude/skills/query-semantic-model/SKILL.md",
2295
2297
  ".claude/skills/visualize-semantic-model/SKILL.md",
2296
2298
  ".claude/skills/visualize-semantic-model/scripts/render_graph.py",
2297
2299
  "dbt/models/bronze/.gitkeep",
@@ -2474,6 +2476,7 @@ var InitService = class InitService {
2474
2476
  ".claude/skills/create-dbt-transformations/references/schema-conventions.md": this.renderTemplate("skills/create-dbt-transformations/references/schema-conventions.md", {}),
2475
2477
  ".claude/skills/create-dbt-transformations/references/edge-cases.md": this.renderTemplate("skills/create-dbt-transformations/references/edge-cases.md", {}),
2476
2478
  ".claude/skills/load-sample-data/SKILL.md": this.renderTemplate("skills/load-sample-data/SKILL.md", {}),
2479
+ ".claude/skills/query-semantic-model/SKILL.md": this.renderTemplate("skills/query-semantic-model/SKILL.md", {}),
2477
2480
  ".claude/skills/visualize-semantic-model/SKILL.md": this.renderTemplate("skills/visualize-semantic-model/SKILL.md", {}),
2478
2481
  ".claude/skills/visualize-semantic-model/scripts/render_graph.py": this.renderTemplate("skills/visualize-semantic-model/scripts/render_graph.py", {}),
2479
2482
  "dbt/models/bronze/.gitkeep": "",
@@ -1,5 +1,5 @@
1
- import { E as getConfig, b as createApiClient } from "./core-CY9pC37x.mjs";
2
- import { t as BaseCommand } from "./base.command-CnVb4RG6.mjs";
1
+ import { E as getConfig, b as createApiClient } from "./core-CChCxudp.mjs";
2
+ import { t as BaseCommand } from "./base.command-CWGrSq73.mjs";
3
3
  import * as fs from "fs";
4
4
  import * as path from "path";
5
5
  import chalk from "chalk";
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { A as buildAuthorizationUrl, B as deleteCredentials, C as formatError, D as resolveApiUrl, E as getConfig, F as refreshAccessToken, G as ApiError, H as isTokenExpired, I as setAuthConfig, L as setAuthEnv, M as generatePKCEChallenge, N as getActiveAuthConfig, O as performOAuthLogin, P as getUserInfo, R as tokenResponseToCredentials, S as resolveAppUrl, T as DEFAULT_API_URL, U as loadCredentials, V as getCredentialsPath, W as saveCredentials, a as formatInProjectSwitchWarning, b as createApiClient, c as isInsideProject, d as iac_exports, i as formatEnvMismatchError, j as exchangeCodeForTokens, k as AUTH_ENVS, l as resolveProjectContext, o as formatProjectOrgFlagError, r as formatCredentialsMismatchWarning, s as renderProjectContextLine, t as InitService, u as validateEnvAgainstProject, w as sanitizeFileName, x as unwrap, z as startOAuthServer } from "./core-CY9pC37x.mjs";
1
+ import { A as buildAuthorizationUrl, B as deleteCredentials, C as formatError, D as resolveApiUrl, E as getConfig, F as refreshAccessToken, G as ApiError, H as isTokenExpired, I as setAuthConfig, L as setAuthEnv, M as generatePKCEChallenge, N as getActiveAuthConfig, O as performOAuthLogin, P as getUserInfo, R as tokenResponseToCredentials, S as resolveAppUrl, T as DEFAULT_API_URL, U as loadCredentials, V as getCredentialsPath, W as saveCredentials, a as formatInProjectSwitchWarning, b as createApiClient, c as isInsideProject, d as iac_exports, i as formatEnvMismatchError, j as exchangeCodeForTokens, k as AUTH_ENVS, l as resolveProjectContext, o as formatProjectOrgFlagError, r as formatCredentialsMismatchWarning, s as renderProjectContextLine, t as InitService, u as validateEnvAgainstProject, w as sanitizeFileName, x as unwrap, z as startOAuthServer } from "./core-CChCxudp.mjs";
2
2
  export { AUTH_ENVS, ApiError, DEFAULT_API_URL, InitService, buildAuthorizationUrl, createApiClient, deleteCredentials, exchangeCodeForTokens, formatCredentialsMismatchWarning, formatEnvMismatchError, formatError, formatInProjectSwitchWarning, formatProjectOrgFlagError, generatePKCEChallenge, getActiveAuthConfig, getConfig, getCredentialsPath, getUserInfo, iac_exports as iac, isInsideProject, isTokenExpired, loadCredentials, performOAuthLogin, refreshAccessToken, renderProjectContextLine, resolveApiUrl, resolveAppUrl, resolveProjectContext, sanitizeFileName, saveCredentials, setAuthConfig, setAuthEnv, startOAuthServer, tokenResponseToCredentials, unwrap, validateEnvAgainstProject };
@@ -1,5 +1,5 @@
1
- import { x as unwrap } from "./core-CY9pC37x.mjs";
2
- import { n as createListRender, r as defineApiCommand, t as bodyFlag } from "./factory-DTqayaCF.mjs";
1
+ import { x as unwrap } from "./core-CChCxudp.mjs";
2
+ import { n as createListRender, r as defineApiCommand, t as bodyFlag } from "./factory-SazXfu26.mjs";
3
3
  import { Args, Flags } from "@oclif/core";
4
4
  //#region src/adapters/oclif/presets.ts
5
5
  function camelToKebab(s) {
@@ -69,6 +69,7 @@ This project ships pre-installed Claude skills under `.claude/skills/` for the c
69
69
  - **`create-cubes`** — build Cube.dev semantic models from gold-layer tables.
70
70
  - **`create-dbt-transformations`** — write bronze→silver→gold dbt models.
71
71
  - **`load-sample-data`** — seed BigQuery with example data when the warehouse is empty.
72
+ - **`query-semantic-model`** — run a Cube.js query and render the result inline in chat as a table + ASCII bar chart.
72
73
  - **`visualize-semantic-model`** — render the cube graph as an image.
73
74
 
74
75
  Inside the container, ask Claude things like _"create a Connection for our Stripe source"_ or _"build a silver model for the orders table"_ and it will use the matching skill.
@@ -21,7 +21,7 @@ The hard part is **stream selection** and **sync-mode choice**, not the YAML sha
21
21
 
22
22
  ## Prerequisites
23
23
 
24
- - The Source already exists on the server (you'll get its `id` via `revos sources list`). If the user wants to ingest from a source that isn't created yet, stop and tell them to run `revos sources create` first — source configuration (connector picker, credentials, OAuth) lives in the RevOS UI.
24
+ - The Source already exists on the server (you'll get its `id` via `revos sources list`). If the user wants to ingest from a source that isn't created yet, stop and tell them to run `revos sources create` first — source configuration (connector picker, credentials, OAuth) lives in the RevOS UI. Once the user confirms they've returned from the UI, **re-run `revos connections list --json` and `revos sources list --json` before continuing** — the UI may have auto-created a connection for the new source. If a matching connection already exists, offer to pull it with `revos pull` rather than authoring a duplicate.
25
25
  - `revos auth status` shows authenticated. If not, ask the user to run `revos auth login`.
26
26
 
27
27
  ---
@@ -44,7 +44,9 @@ After deriving sync mode + cursor + primary key for each selected stream, presen
44
44
 
45
45
  ## Phase 1: Identify the Source
46
46
 
47
- Run `revos sources list --json` and match on the server-side `name` (or run `revos sources list` for a scannable table). Record the source's `id` that goes into the Connection YAML as `spec.source.id`.
47
+ First, run `revos connections list --json` to see what connections already exist. If the user just returned from the web UI, this re-fetch is essential the UI may have created a connection automatically. If you find a connection that matches the user's stated goal, surface it before proceeding so you don't create a duplicate.
48
+
49
+ Then run `revos sources list --json` and match on the server-side `name` (or run `revos sources list` for a scannable table). Record the source's `id` — that goes into the Connection YAML as `spec.source.id`.
48
50
 
49
51
  If no source was named, ask the user which one. Don't proceed without an id.
50
52
 
@@ -0,0 +1,347 @@
1
+ ---
2
+ name: query-semantic-model
3
+ description: >
4
+ Run a Cube.js query against the semantic model and show the result inline in chat as
5
+ an ASCII table and (where it makes sense) an ASCII bar chart. Use whenever the user
6
+ asks a business question that can be answered from the cubes — "how many orders…",
7
+ "top N…", "revenue by…", "trend over time", "compare X to Y" — or explicitly asks to
8
+ "query the semantic model", "run a cube query", "show me a chart", "plot…", "graph…".
9
+ Discovers measures and dimensions via `revos cubes meta`, executes the query with
10
+ `revos cubes query`, and renders the results without leaving the chat.
11
+ ---
12
+
13
+ # Query Semantic Model
14
+
15
+ Answer business questions by querying the org's semantic model and rendering the result
16
+ directly in the chat — no notebook, no UI hop. The CLI already exposes everything you
17
+ need: `revos cubes meta` lists the available cubes / measures / dimensions, and
18
+ `revos cubes query` runs a Cube.js query and returns the rows.
19
+
20
+ This skill is the "last mile" after Bronze → Silver → Gold → Cubes have been built and
21
+ applied. It turns the semantic model into something you can actually look at.
22
+
23
+ ---
24
+
25
+ ## Step 1: Discover what's queryable
26
+
27
+ Before guessing member names, list what the org's semantic model actually exposes:
28
+
29
+ ```bash
30
+ revos cubes meta --json
31
+ ```
32
+
33
+ The response carries an array of cubes, each with `measures`, `dimensions`, `segments`,
34
+ and a `name`. Cube members are addressed as `<CubeName>.<member>` — for example
35
+ `gold_order_items_enriched.count` or `gold_order_items_enriched.order_date`.
36
+
37
+ Pick the cube and members that match the user's question. If the question can't be
38
+ answered from the available members, say so — don't invent member names.
39
+
40
+ ### Map the join graph from the local cube files
41
+
42
+ `revos cubes meta` tells you which members exist; the local `cubes/` folder tells you
43
+ **how cubes are connected**. Read every `*.yml` file in `cubes/` and pull out each
44
+ cube's `joins:` block — that's how the user-defined part of the semantic model is
45
+ wired up. Example:
46
+
47
+ ```yaml
48
+ # cubes/gold_order_items_enriched.yml
49
+ spec:
50
+ definition:
51
+ name: gold_order_items_enriched
52
+ joins:
53
+ gold_users_with_order_stats:
54
+ relationship: many_to_one
55
+ sql: "${CUBE}.user_id = ${gold_users_with_order_stats}.user_id"
56
+ gold_product_performance:
57
+ relationship: many_to_one
58
+ sql: "${CUBE}.product_id = ${gold_product_performance}.product_id"
59
+ ```
60
+
61
+ From this you can compute, for any pair of cubes, how many distinct join paths exist
62
+ between them. That's what lets you pick the right path in Step 2 instead of letting
63
+ Cube.js guess one for you.
64
+
65
+ **Caveat — system cubes are not in `cubes/`.** RevOS generates a handful of cubes
66
+ server-side (scoring, segments, model overlays, pre-aggregation overlays). Those
67
+ never appear as YAML in the project. The full member list is only visible through
68
+ `revos cubes meta`. So:
69
+
70
+ - Use the `cubes/` folder to **see the user-defined join graph**.
71
+ - Use `revos cubes meta` to **see every member that can actually be queried**,
72
+ including system cubes.
73
+
74
+ When the user's question touches a system cube (e.g. "show me users by segment",
75
+ "top scoring entities"), the join graph from `cubes/` will be incomplete. Don't
76
+ assume there's no path — list the meta and look for it there.
77
+
78
+ ---
79
+
80
+ ## Step 2: Build the query — and pin the join path when it matters
81
+
82
+ A Cube.js query is a JSON object. The shapes you'll use most often:
83
+
84
+ ```json
85
+ { "measures": ["gold_order_items_enriched.count"] }
86
+ ```
87
+
88
+ ```json
89
+ {
90
+ "measures": ["gold_order_items_enriched.count"],
91
+ "dimensions": ["gold_order_items_enriched.traffic_source"],
92
+ "order": { "gold_order_items_enriched.count": "desc" },
93
+ "limit": 10
94
+ }
95
+ ```
96
+
97
+ Time series (the chart-friendliest shape):
98
+
99
+ ```json
100
+ {
101
+ "measures": ["gold_order_items_enriched.revenue"],
102
+ "timeDimensions": [
103
+ {
104
+ "dimension": "gold_order_items_enriched.order_date",
105
+ "granularity": "month",
106
+ "dateRange": "last 12 months"
107
+ }
108
+ ]
109
+ }
110
+ ```
111
+
112
+ Rules of thumb:
113
+
114
+ - One measure + one dimension → bar chart by category.
115
+ - One measure + one `timeDimensions` with `granularity` → bar chart over time.
116
+ - Multiple measures or multiple dimensions → render as an ASCII table only; don't try
117
+ to draw a chart.
118
+ - Always cap with `limit` (default 20) so a stray query can't return a million rows.
119
+
120
+ ### Join paths — and how to pin them with a hint
121
+
122
+ Cube.js infers the join path from the cubes that appear in `measures` /
123
+ `dimensions` / `filters`. **When more than one path connects them, Cube.js picks
124
+ one for you — and the answer changes depending on which.** This is the single
125
+ biggest source of "the number looks wrong" bugs in a semantic-model query.
126
+
127
+ You spell the path out by prefixing the member with the cubes you want Cube.js to
128
+ walk through, dot-separated. The last segment is the actual member; everything
129
+ before it is the join hint. Examples:
130
+
131
+ ```text
132
+ # no hint — Cube.js picks a path
133
+ gold_users_with_order_stats.lifetime_revenue
134
+
135
+ # hint: reach lifetime_revenue via the fact spine, then the users mart
136
+ gold_order_items_enriched.gold_users_with_order_stats.lifetime_revenue
137
+ ```
138
+
139
+ The hint syntax works the same on measures, dimensions, and filter members.
140
+
141
+ **When you need a hint (red flags):**
142
+
143
+ 1. Two cubes in the query are connected by **more than one path** in `cubes/`.
144
+ Common shape: a fact spine joins to two dimension cubes, and one of the dimension
145
+ cubes _also_ joins to the other (e.g. orders → users, orders → products,
146
+ products → users via `last_purchaser_id`). Without a hint, Cube.js may pick the
147
+ non-obvious path and quietly double- or under-count.
148
+ 2. The user's question implicitly names the path ("revenue **per order item** by
149
+ user country" vs "revenue **per user** by user country") — different grains,
150
+ different paths, different numbers.
151
+ 3. Counts come back smaller than the smallest cube would allow, or larger than the
152
+ largest fact would allow. That's a sign Cube.js fanned out through the wrong
153
+ join.
154
+ 4. The cube has a many-to-many relationship in the chain. Always pin it.
155
+ 5. You added a measure and the number changed dramatically (more than ~10%) without
156
+ the dimension set changing — Cube.js probably re-routed the join.
157
+
158
+ **Worked example — same question, two answers.**
159
+
160
+ Cubes (from `cubes/`):
161
+
162
+ ```text
163
+ gold_order_items_enriched ──many_to_one──▶ gold_users_with_order_stats
164
+ gold_order_items_enriched ──many_to_one──▶ gold_product_performance
165
+ gold_product_performance ──many_to_one──▶ gold_users_with_order_stats (last_purchaser_id)
166
+ ```
167
+
168
+ Question: "Total revenue by user country."
169
+
170
+ ```json
171
+ // Path A — order items → users directly (CORRECT for per-item revenue)
172
+ {
173
+ "measures": ["gold_order_items_enriched.revenue"],
174
+ "dimensions": [
175
+ "gold_order_items_enriched.gold_users_with_order_stats.country"
176
+ ]
177
+ }
178
+ ```
179
+
180
+ ```json
181
+ // Path B — order items → product → last_purchaser (WRONG: revenue gets
182
+ // attributed to the last buyer's country, not the actual buyer's)
183
+ {
184
+ "measures": ["gold_order_items_enriched.revenue"],
185
+ "dimensions": [
186
+ "gold_order_items_enriched.gold_product_performance.gold_users_with_order_stats.country"
187
+ ]
188
+ }
189
+ ```
190
+
191
+ The two queries return wildly different numbers. Always state which path you used
192
+ and **why** in the explanation underneath the chart so the user can spot a wrong
193
+ choice.
194
+
195
+ **Worked example — filter via a specific path.**
196
+
197
+ ```json
198
+ {
199
+ "measures": ["gold_order_items_enriched.revenue"],
200
+ "dimensions": ["gold_order_items_enriched.traffic_source"],
201
+ "filters": [
202
+ {
203
+ "member": "gold_order_items_enriched.gold_users_with_order_stats.country",
204
+ "operator": "equals",
205
+ "values": ["DE"]
206
+ }
207
+ ]
208
+ }
209
+ ```
210
+
211
+ **Rules for choosing a path:**
212
+
213
+ - Default to the **shortest** path that goes through the **fact spine** the
214
+ question is naturally grained on (revenue per order → spine =
215
+ `gold_order_items_enriched`; users with X → spine = `gold_users_with_order_stats`).
216
+ - Never silently take the alternate path. If two paths are plausible, ask the user
217
+ which grain they meant before running the query.
218
+ - If `revos cubes meta` returns a member name that already contains dots (e.g.
219
+ `cubeA.cubeB.foo`), that **is** the qualified form — paste it through verbatim,
220
+ don't strip the prefix.
221
+
222
+ ---
223
+
224
+ ## Step 3: Run the query
225
+
226
+ ```bash
227
+ revos cubes query --query '<INLINE_JSON>' --json
228
+ ```
229
+
230
+ For anything longer than a one-liner, write the JSON to a temp file and pass it with
231
+ `@`:
232
+
233
+ ```bash
234
+ cat > /tmp/q.json <<'EOF'
235
+ {
236
+ "measures": ["gold_order_items_enriched.revenue"],
237
+ "timeDimensions": [
238
+ {
239
+ "dimension": "gold_order_items_enriched.order_date",
240
+ "granularity": "month",
241
+ "dateRange": "last 12 months"
242
+ }
243
+ ]
244
+ }
245
+ EOF
246
+ revos cubes query --query @/tmp/q.json --json
247
+ ```
248
+
249
+ The response is the raw Cube.js payload — `data` is the array of rows, each row is a
250
+ flat object keyed by the same member names used in the query.
251
+
252
+ If the API returns an error, surface the message verbatim. Common causes: a member
253
+ name typo (re-check `revos cubes meta`), a cube that hasn't been applied yet
254
+ (`revos status` / `revos apply`), or a `dateRange` against a column that isn't a time
255
+ dimension.
256
+
257
+ ---
258
+
259
+ ## Step 4: Render the result in chat
260
+
261
+ Print **two** blocks in the assistant's reply, both as fenced code so they render
262
+ monospaced:
263
+
264
+ ### 4a. ASCII table — always
265
+
266
+ Show every returned row. Right-align numeric columns, left-align everything else.
267
+ Format numbers with thousands separators; round to 2 decimals when not integral.
268
+ Truncate long string values to 32 chars with a trailing `…`.
269
+
270
+ ```
271
+ traffic_source count
272
+ ───────────────── ───────
273
+ Search 142,318
274
+ Organic 88,204
275
+ Email 41,907
276
+ Facebook 22,015
277
+ Display 9,471
278
+ ```
279
+
280
+ ### 4b. ASCII bar chart — when the shape allows
281
+
282
+ If the query returned exactly one measure plus exactly one dimension (categorical or
283
+ time), draw a horizontal bar chart underneath the table. Scale bars to a width of
284
+ **40 characters** based on the largest value in the result set; use the `█` block
285
+ character for filled cells and a single trailing space.
286
+
287
+ ```
288
+ Search ████████████████████████████████████████ 142,318
289
+ Organic ████████████████████████▊ 88,204
290
+ Email ███████████▊ 41,907
291
+ Facebook ██████▏ 22,015
292
+ Display ██▋ 9,471
293
+ ```
294
+
295
+ Time-series example (oldest → newest, top-to-bottom):
296
+
297
+ ```
298
+ 2025-06 ███████████▊ € 41,200
299
+ 2025-07 █████████████████▏ € 59,800
300
+ 2025-08 ████████████████████▋ € 72,400
301
+
302
+ ```
303
+
304
+ Skip the chart (table only) when:
305
+
306
+ - The result set is empty.
307
+ - The query has 2+ measures, 2+ dimensions, or a single scalar value (no dimension).
308
+ - All measure values are zero or null.
309
+
310
+ For a single scalar answer (one row, one column), just say the number in prose
311
+ followed by the one-line table — no chart.
312
+
313
+ ---
314
+
315
+ ## Step 5: Explain the result briefly
316
+
317
+ After the rendered output, add 1–3 short sentences in plain English: what was
318
+ measured, the highest / lowest bucket, and any obvious anomaly (e.g. a missing month,
319
+ a single category dominating the total). Keep it to facts that are visible in the
320
+ table — don't speculate about causes.
321
+
322
+ If the user is likely to want to drill in, suggest **one** concrete follow-up query
323
+ they can ask for next ("Want the same broken down by country?"). Don't list more than
324
+ one — keep the chat tight.
325
+
326
+ ---
327
+
328
+ ## Rules
329
+
330
+ - Never hardcode cube or member names. Always confirm them via `revos cubes meta`
331
+ before composing the query.
332
+ - Before composing a multi-cube query, scan the `joins:` blocks in `cubes/*.yml`. If
333
+ the cubes you need are connected by more than one path, **pick one explicitly with
334
+ a dotted join hint** instead of letting Cube.js guess.
335
+ - Remember that system cubes (scoring, segments, model overlays) live only in
336
+ `revos cubes meta`, not in `cubes/`. Their joins aren't visible in the YAML —
337
+ only in the meta output.
338
+ - Always render the table; render the chart only when the data shape supports it.
339
+ - Keep `limit` set (default 20). For "top N" questions, set `limit` to N and add
340
+ `order` on the measure.
341
+ - Quote query JSON safely on the shell — prefer a temp file with `@/tmp/q.json` over
342
+ a long inline string with embedded quotes.
343
+ - Don't write the query result to disk or invoke a Python plotter — this skill is
344
+ in-chat only. For static PNG visualizations of the cube graph itself, use
345
+ `visualize-semantic-model`.
346
+ - If `revos cubes meta` returns no cubes, stop and tell the user to run `revos apply`
347
+ first.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revos/cli",
3
- "version": "0.3.1",
3
+ "version": "0.3.4",
4
4
  "description": "RevOS CLI for managing RevOS platform resources",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -89,7 +89,7 @@
89
89
  "yaml": "^2.8.3",
90
90
  "zod": "^4.3.5",
91
91
  "zod-validation-error": "^4.0.1",
92
- "@revos/api-client": "0.1.1"
92
+ "@revos/api-client": "0.1.2"
93
93
  },
94
94
  "devDependencies": {
95
95
  "@swc/core": "^1.7.26",