@oxy-hq/cli 0.7.0 → 0.9.0

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 (38) hide show
  1. package/README.md +131 -8
  2. package/dist/main.mjs +4909 -3551
  3. package/dist/main.mjs.map +1 -1
  4. package/dist/{mcp-xxWL3_dz.mjs → mcp-Bdp0rX0l.mjs} +2 -2
  5. package/dist/{mcp-xxWL3_dz.mjs.map → mcp-Bdp0rX0l.mjs.map} +1 -1
  6. package/package.json +5 -5
  7. package/skills/oxy-agents/SKILL.md +318 -0
  8. package/skills/oxy-agents/references/quick-reference.md +222 -0
  9. package/skills/oxy-agents/templates/agentic-template.yml +141 -0
  10. package/skills/oxy-airway-pipelines/SKILL.md +223 -316
  11. package/skills/oxy-automations/SKILL.md +61 -15
  12. package/skills/oxy-cli/SKILL.md +37 -4
  13. package/skills/oxy-custom-apps/SKILL.md +193 -291
  14. package/skills/oxy-custom-apps/references/minimal-starter.md +63 -0
  15. package/skills/oxy-custom-apps/references/performance-and-usage.md +105 -0
  16. package/skills/oxy-custom-apps/references/reading-data.md +106 -0
  17. package/skills/oxy-custom-apps/references/sandbox-agent-loop.md +267 -0
  18. package/skills/oxy-custom-apps/references/verifying-in-the-browser.md +65 -0
  19. package/skills/oxy-data-apps/SKILL.md +362 -0
  20. package/skills/oxy-data-apps/references/examples.md +263 -0
  21. package/skills/oxy-data-apps/references/quick-reference.md +171 -0
  22. package/skills/oxy-data-apps/templates/dashboard-with-controls.app.yml +128 -0
  23. package/skills/oxy-data-apps/templates/simple-dashboard.app.yml +102 -0
  24. package/skills/oxy-data-architecture/SKILL.md +137 -152
  25. package/skills/oxy-evals/SKILL.md +362 -0
  26. package/skills/oxy-evals/references/diagnosis.md +243 -0
  27. package/skills/oxy-evals/references/results-json.md +145 -0
  28. package/skills/oxy-evals/references/writing-expected.md +112 -0
  29. package/skills/oxy-semantic-modeling/SKILL.md +239 -59
  30. package/skills/oxy-semantic-modeling/references/warehouse-casts.md +57 -0
  31. package/skills/oxy-semantic-modeling/templates/topic-template.yml +38 -0
  32. package/skills/oxy-semantic-modeling/templates/view-template.yml +119 -0
  33. package/template/.github/workflows/publish.yaml +15 -13
  34. package/template/.nvmrc +1 -1
  35. package/template/.oxyc-managed +5 -5
  36. package/template/README.md +21 -10
  37. package/template/package.json +3 -2
  38. package/template/pnpm-workspace.yaml +28 -17
@@ -0,0 +1,57 @@
1
+ # Casting a date column per warehouse
2
+
3
+ Use when a dimension whose meaning is a date or timestamp sits on a column that
4
+ is **not** stored as `DATE` / `DATETIME` / `TIMESTAMP`. Declare the dimension
5
+ `type: date` (or `type: datetime`) and wrap its `expr` with a cast, so filters
6
+ compile against a real date instead of failing with `TYPE_MISMATCH` on the
7
+ first query that filters on it.
8
+
9
+ **Sample first.** Stored formats differ between warehouses and between tables in
10
+ one warehouse — ISO strings (`"2024-01-31"`), compact strings (`"20240131"`),
11
+ integers (`20240131`), epoch seconds or milliseconds. One
12
+ `SELECT <col> FROM <table> LIMIT 1` tells you which you have; guessing is cheap
13
+ to get wrong and the failure only shows at query time.
14
+
15
+ | Warehouse | Cast functions |
16
+ | ---------- | -------------- |
17
+ | ClickHouse | `toDate(<col>)`, `toDateTime(<col>)`, `parseDateTimeBestEffort(<col>)`, `toDate(toString(<col>))` for integer-encoded dates |
18
+ | BigQuery | `CAST(<col> AS DATE)`, `PARSE_DATE('<format>', <col>)`, `PARSE_TIMESTAMP('<format>', <col>)`, `TIMESTAMP_SECONDS(<col>)` |
19
+ | Snowflake | `TO_DATE(<col>[, '<format>'])`, `TO_TIMESTAMP(<col>)`, `TRY_TO_DATE(<col>)` |
20
+ | Postgres | `(<col>)::date`, `(<col>)::timestamp`, `to_date(<col>, '<format>')`, `to_timestamp(<col>)` |
21
+ | DuckDB / Airhouse | `CAST(<col> AS DATE)`, `strptime(<col>, '<format>')`, `to_timestamp(<col>)`, `epoch_ms(<col>)` |
22
+
23
+ A column already typed as a date or timestamp needs no cast — `expr: <col>` is
24
+ enough.
25
+
26
+ ## Examples
27
+
28
+ ```yaml
29
+ # ClickHouse: business_date stored as Int32 like 20240131
30
+ - name: business_date
31
+ type: date
32
+ expr: "toDate(toString(business_date))"
33
+
34
+ # BigQuery: order_date stored as STRING like "2024-01-31"
35
+ - name: order_date
36
+ type: date
37
+ expr: "PARSE_DATE('%Y-%m-%d', order_date)"
38
+
39
+ # Snowflake: created_at already a TIMESTAMP — no cast
40
+ - name: created_at
41
+ type: datetime
42
+ expr: created_at
43
+ ```
44
+
45
+ ## Derived parts re-derive from the raw column
46
+
47
+ A derived dimension (`business_month`, `day_of_week`, …) repeats the cast
48
+ inside its own `expr` rather than referencing the base date dimension:
49
+
50
+ ```yaml
51
+ - name: business_quarter
52
+ type: number
53
+ expr: "toQuarter(toDate(toString(business_date)))"
54
+ ```
55
+
56
+ If the same cast chain appears in many dimensions, the column type is the real
57
+ problem — fix it once in the pipeline that loads the table, then drop the casts.
@@ -0,0 +1,38 @@
1
+ # Oxy topic template — copy, rename to <name>.topic.yml, and fill in.
2
+ # A topic is a question area: a base view plus the views it may join to.
3
+ # Name it after the questions people ask, not after a table; most topics
4
+ # span several views that share entity names.
5
+
6
+ name: my_topic_name
7
+ description: "Which questions this topic answers, and what it always excludes"
8
+ base_view: primary_view_name # the view queries start from
9
+ views:
10
+ - primary_view_name
11
+ # - related_view_name # joins resolve through matching entity names
12
+
13
+ # Optional: filters applied to every query in this topic, ANDed with the
14
+ # user's own. Write `field:` as view.dimension — a bare name is looked up in a
15
+ # view named like the TOPIC, not the base view, so it only works when the two
16
+ # share a name.
17
+ # default_filters:
18
+ # - field: primary_view_name.is_voided
19
+ # eq:
20
+ # value: false
21
+ #
22
+ # - field: primary_view_name.status
23
+ # not_in:
24
+ # values: ["cancelled", "test"]
25
+ #
26
+ # - field: primary_view_name.amount
27
+ # gte:
28
+ # value: 0
29
+ #
30
+ # - field: primary_view_name.created_date
31
+ # in_date_range:
32
+ # from: "2024-01-01" # a date, or a relative expression
33
+ # to: "now"
34
+
35
+ # Operators (the operator name is the key; its value sits under it):
36
+ # - eq / neq / gt / gte / lt / lte: value: <scalar>
37
+ # - in / not_in: values: [<scalar>, ...]
38
+ # - in_date_range / not_in_date_range: from: <date>, to: <date>
@@ -0,0 +1,119 @@
1
+ # Oxy view template — copy, rename to <name>.view.yml, and fill in.
2
+ # One view = one table (or `sql:`) plus the vocabulary an agent may use on it.
3
+ # No `# yaml-language-server: $schema=` header: no schema is published for views.
4
+
5
+ name: my_view_name
6
+ description: "What a row is, when the data is complete, and what it excludes"
7
+ datasource: "database_name" # a database name from config.yml
8
+ table: "schema_name.table_name"
9
+
10
+ entities:
11
+ # Primary entity: the grain of one row (one per view).
12
+ - name: primary_entity_name
13
+ type: primary
14
+ description: "What one row represents"
15
+ key: primary_key_dimension # a DIMENSION name below, never a raw column
16
+
17
+ # Foreign entity: one per foreign-key column. Use the SAME name as the other
18
+ # view's primary entity — matching names are what create the join.
19
+ # - name: related_entity_name
20
+ # type: foreign
21
+ # description: "The related row in another view"
22
+ # key: foreign_key_dimension
23
+
24
+ dimensions:
25
+ # Key dimensions (entity keys reference these by name)
26
+ - name: primary_key_dimension
27
+ type: string
28
+ description: "Primary key"
29
+ expr: primary_key_column
30
+
31
+ # Regular dimensions (filterable / groupable)
32
+ - name: dimension_name
33
+ type: string # string | number | date | datetime | boolean
34
+ description: "Business-friendly description"
35
+ expr: column_name_or_sql_expression
36
+ samples: ["VALUE_A", "VALUE_B"] # the FULL set of valid values for enum-like fields
37
+ synonyms: ["alternative name", "abbreviation"]
38
+
39
+ # Date dimensions: type date/datetime, and expr must yield a real date.
40
+ # If the column is a string, integer or epoch, wrap it in a cast (sample one
41
+ # row first) — otherwise date filters fail with TYPE_MISMATCH.
42
+ - name: created_date
43
+ type: date
44
+ description: "Date the record was created"
45
+ expr: created_at
46
+
47
+ # Derived date parts, so the agent never writes date arithmetic
48
+ - name: created_year
49
+ type: number
50
+ description: "Year the record was created"
51
+ expr: "EXTRACT(YEAR FROM created_at)"
52
+ synonyms: ["year"]
53
+
54
+ # Boolean dimensions
55
+ - name: is_active
56
+ type: boolean
57
+ description: "Whether the record is active"
58
+ expr: "status = 'active'"
59
+ samples: ["true", "false"] # samples are ALWAYS strings, even for boolean/number dimensions
60
+
61
+ measures:
62
+ - name: total_records
63
+ type: count
64
+ description: "Number of records"
65
+ synonyms: ["record count"]
66
+
67
+ - name: total_amount
68
+ type: sum
69
+ description: "Sum of amounts"
70
+ expr: amount_column
71
+ synonyms: ["total"]
72
+
73
+ - name: average_amount
74
+ type: average
75
+ description: "Average amount per record"
76
+ expr: amount_column
77
+
78
+ # Filtered measure: "aggregate X where Y". Each filter expr is a SQL
79
+ # condition over this view's columns, or a bare {{dimension}} naming a
80
+ # dimension of THIS view. Prefer this over a custom CASE WHEN: it can be
81
+ # answered from a pre-aggregation, a custom measure never can.
82
+ - name: completed_amount
83
+ type: sum
84
+ description: "Sum of amounts for completed records only"
85
+ expr: amount_column
86
+ filters:
87
+ - expr: "status = 'completed'"
88
+
89
+ # Custom measure: only for what a filtered measure cannot express (ratios,
90
+ # correlations, cross-period math). The expr is already an aggregate.
91
+ # Never served from a pre-aggregation (nor are median or number measures).
92
+ - name: completion_rate
93
+ type: custom
94
+ description: "Share of records that are completed"
95
+ expr: "SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) / NULLIF(COUNT(*), 0)"
96
+
97
+ # Freshness — OPTIONAL but cheap. Every value is a one-element LIST of strings.
98
+ # Without freshness_watermark_column the agent uses the first date dimension.
99
+ # meta:
100
+ # freshness_watermark_column: ["created_date"]
101
+ # freshness_expected_cadence: ["hourly"]
102
+ # freshness_complete_through: ["previous full hour (UTC)"]
103
+ # freshness_caveat: ["The current hour is partial."]
104
+
105
+ # Pre-aggregations (rollups) — OPTIONAL and opt-in: with no block, the view has
106
+ # no rollups at all. Members are names declared above, never raw columns.
107
+ #
108
+ # refresh_key: # view-level default; exactly ONE of every / sql
109
+ # every: "1h"
110
+ #
111
+ # pre_aggregations:
112
+ # - name: by_month
113
+ # dimensions: [dimension_name]
114
+ # measures: [total_records, total_amount] # count/count_distinct/sum/average/min/max
115
+ # time_dimension: created_date # a date/datetime dimension above
116
+ # granularity: day # second|minute|hour|day|week|month|quarter|year; serves this
117
+ # # grain or coarser — but a `week` rollup never serves month+
118
+ # refresh_key: # optional per-rollup override
119
+ # sql: "SELECT MAX(updated_at) FROM schema_name.table_name"
@@ -160,7 +160,7 @@ env:
160
160
  # service account are honoured from 0.7.0, which tries them first and falls
161
161
  # back to the publisher. A pin older than that still publishes, through the
162
162
  # publisher alone.
163
- OXYC_VERSION: "0.7.0"
163
+ OXYC_VERSION: "0.9.0"
164
164
 
165
165
  jobs:
166
166
  build:
@@ -281,25 +281,27 @@ jobs:
281
281
 
282
282
  # pnpm BEFORE setup-node: setup-node's `cache: pnpm` shells out to
283
283
  # `pnpm store path`, so it fails if pnpm is not already installed.
284
+ #
285
+ # v6, the first major that supports pnpm 11; v4 does not. And NO
286
+ # `version:` input, on purpose: the action installs the pnpm that
287
+ # package.json's `packageManager` pins, and it fails this step
288
+ # ("Multiple versions of pnpm specified") when a `version:` differs from
289
+ # that pin — `11` against `11.22.0` included. A package.json with no pin
290
+ # fails here too ("No pnpm version is specified"); the fix is the
291
+ # template's `packageManager` line, which `oxyc update` shows in its
292
+ # package.json diff, not a `version:` in this managed file.
284
293
  - name: Install pnpm
285
294
  if: steps.discover.outputs.count != '0'
286
- uses: pnpm/action-setup@v4
287
- with:
288
- # package.json floors pnpm at >=10 and there is no `packageManager`
289
- # field for the action to read, so the major goes here. 10 is not
290
- # cosmetic: pnpm 9 reads no settings at all from
291
- # pnpm-workspace.yaml, which silently makes its
292
- # `onlyBuiltDependencies` block inert and blocks an app's
293
- # esbuild/sharp postinstall with no diagnostic.
294
- version: 10
295
+ uses: pnpm/action-setup@v6
295
296
 
296
297
  - name: Install Node
297
298
  if: steps.discover.outputs.count != '0'
298
299
  uses: actions/setup-node@v4
299
300
  with:
300
- # .nvmrc is the single source of truth (20). This org's CI examples
301
- # disagree — one says 26 — and 20 is what the real working monorepo
302
- # pins, so read the file rather than restating a number here.
301
+ # .nvmrc is the single source of truth (26, what the customer-apps
302
+ # monorepo pins), so read the file rather than restating a number
303
+ # here. Whatever it says must be >= 22.13: that is the Node pnpm 11
304
+ # requires, and package.json's `engines.node` says so too.
303
305
  node-version-file: .nvmrc
304
306
  cache: pnpm
305
307
  cache-dependency-path: pnpm-lock.yaml
package/template/.nvmrc CHANGED
@@ -1 +1 @@
1
- 20
1
+ 26
@@ -239,11 +239,11 @@ mixed package.json
239
239
  # which is `mixed`, not a judgement call between the two.
240
240
  mixed .gitignore
241
241
 
242
- # Same shape, and the file says so itself: the `onlyBuiltDependencies` block
243
- # carries the three entries the shipped app bundles need, with an instruction
244
- # to ADD to the list rather than replace it. The `packages:` globs are the
245
- # tool's; a fourth dependency that needs its install script run is the
246
- # customer's.
242
+ # Same shape, and the file says so itself: the `allowBuilds` map carries the
243
+ # three entries the shipped app bundles need, with an instruction to ADD to
244
+ # the map rather than replace it. The `packages:` globs and
245
+ # `confirmModulesPurge` are the tool's; a fourth dependency whose install
246
+ # script somebody decided on is the customer's.
247
247
  mixed pnpm-workspace.yaml
248
248
 
249
249
  # --- scope: which tree each file describes ----------------------------------
@@ -468,13 +468,24 @@ should.
468
468
 
469
469
  ## Node and pnpm
470
470
 
471
- Node **20** (`.nvmrc`), matching the working app monorepo this build config is
472
- derived from. Use **pnpm**, never npm or yarn — there is no `package-lock.json`
473
- here and there should not be one.
474
-
475
- pnpm is floored at **>= 10**, one major above that monorepo's `>= 9`, and the
476
- difference is deliberate: pnpm 9 reads no settings at all from
477
- `pnpm-workspace.yaml`, so on pnpm 9 the `onlyBuiltDependencies` block there is
478
- silently inert and an app's `esbuild` / `sharp` postinstall is blocked with no
479
- diagnostic. A floor that admits a version where the config does nothing is
480
- worse than no floor.
471
+ Node **26** (`.nvmrc`), matching the customer-apps monorepo this build config
472
+ is derived from. Anything from **22.13** up works (`engines.node`): that is the
473
+ floor pnpm 11 itself requires, so a Node below it cannot run pnpm here at all.
474
+ Use **pnpm**, never npm or yarn — there is no `package-lock.json` here and
475
+ there should not be one.
476
+
477
+ pnpm is **pinned**, not just floored: `packageManager` in `package.json` names
478
+ pnpm 11.22.0, and that one line decides the pnpm everywhere. `corepack enable
479
+ pnpm` installs exactly it; a pnpm 10 already on your PATH switches itself to
480
+ it; CI's `pnpm/action-setup` installs it — which is why the workflow carries
481
+ no `version:` of its own: the action refuses to run with two. To move to
482
+ another pnpm, change that line (`corepack use pnpm@<version>`).
483
+
484
+ Dependencies' install scripts are approved in `pnpm-workspace.yaml`, in the
485
+ `allowBuilds` map. pnpm 11 **fails the install** (`ERR_PNPM_IGNORED_BUILDS`)
486
+ on a dependency with a build script the map does not decide, so the first
487
+ install after adding such a dependency stops until somebody sets it to `true`
488
+ (run the script) or `false` (skip it) there and commits that. A repo that
489
+ still carries the older `onlyBuiltDependencies` list gets that failure even
490
+ for the dependencies it lists: pnpm 11 removed the key and ignores it without
491
+ a word.
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "__SLUG__",
3
3
  "private": true,
4
+ "packageManager": "pnpm@11.22.0+sha512.1ff870c4c6133dfd88fb2afc46dd13d47f09c9794b438c6fdb47ca98caf3bc16381ee0be93a091b8e3824cf01f889f46d7d9e20910fb0be1ab0fb5baa80dd621",
4
5
  "engines": {
5
- "node": ">=20",
6
- "pnpm": ">=10"
6
+ "node": ">=22.13",
7
+ "pnpm": ">=11"
7
8
  },
8
9
  "scripts": {
9
10
  "dev": "./scripts/dev.sh",
@@ -12,21 +12,32 @@ packages:
12
12
  - "apps/*"
13
13
  - "apps/*/*"
14
14
 
15
- # pnpm 10 does not run a dependency's install/postinstall script unless the
16
- # package is named here. These three are what the real app bundles need
17
- # (esbuild and unrs-resolver fetch a platform binary, sharp builds one), so
18
- # without this block the first app added here fails to build. Add to the list
19
- # rather than replacing it.
15
+ # Which dependencies may run their install/postinstall scripts. pnpm 11 runs
16
+ # none it has not been told about, and FAILS the install
17
+ # (`ERR_PNPM_IGNORED_BUILDS`) on any dependency with a build script this map
18
+ # does not decide. These three are what the real app bundles need: esbuild and
19
+ # unrs-resolver check for, or fetch, a platform binary, sharp builds one when
20
+ # no prebuilt fits. esbuild is in every app that has Oxy Functions — `oxyc
21
+ # publish` bundles them with `pnpm exec esbuild`. Add to the map rather than
22
+ # replacing it: a new dependency with a build script stops the install until
23
+ # somebody writes `true` (run it) or `false` (skip it) for it here.
20
24
  #
21
- # The key is `onlyBuiltDependencies`, NOT the `allowBuilds` that customer-apps
22
- # carries. Verified against pnpm 10.8.0: a wrong-typed `onlyBuiltDependencies`
23
- # crashes inside `createAllowBuildFunction`, proving pnpm reads it, while a
24
- # wrong-typed `allowBuilds` installs cleanly — pnpm never looks at that key at
25
- # all, with no warning. customer-apps' block is therefore inert; ours is not.
26
- #
27
- # pnpm 9 ignores this file's settings entirely (same probe, pnpm 9.15.9:
28
- # no crash), which is why package.json floors pnpm at >=10 rather than >=9.
29
- onlyBuiltDependencies:
30
- - esbuild
31
- - sharp
32
- - unrs-resolver
25
+ # The key is `allowBuilds`. pnpm 11 removed `onlyBuiltDependencies`, the list
26
+ # this file used to carry, and ignores it without a word: with esbuild listed
27
+ # there, pnpm 11.22.0 still fails an app's install on `Ignored build scripts:
28
+ # esbuild@0.28.2`. pnpm 10.33 reads `allowBuilds` too, though it rarely gets
29
+ # the chance: package.json pins pnpm 11.22.0 (`packageManager`), and a pnpm 10
30
+ # switches itself to that version before installing anything.
31
+ allowBuilds:
32
+ esbuild: true
33
+ sharp: true
34
+ unrs-resolver: true
35
+
36
+ # pnpm 11 lays `node_modules` out for a new store version, so its first install
37
+ # over a tree pnpm 10 wrote has to delete the tree and recreate it — and it
38
+ # asks first. With no terminal to ask on — an agent or a script running
39
+ # `pnpm install`, or `oxyc publish`, which runs one — it aborts instead, with
40
+ # `ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY`. Nothing is lost by not asking:
41
+ # the tree is rebuilt from the lockfile. CI never hits it — a runner starts
42
+ # with no `node_modules`.
43
+ confirmModulesPurge: false