@oxy-hq/cli 0.2.0 → 0.5.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.
@@ -1,4 +1,4 @@
1
- # Build and publish __NAME__'s custom apps with `oxy publish`.
1
+ # Build and publish __NAME__'s custom apps with `oxyc publish`.
2
2
  #
3
3
  # On day one this repo has no apps, and this workflow is a deliberate no-op:
4
4
  # discovery finds nothing, every later step is skipped, the publish JOB is
@@ -14,7 +14,7 @@
14
14
  # compiles every bundle, needs no credential to do it, and so still catches a
15
15
  # bundle that stopped building before anyone ships it by hand — and the publish
16
16
  # JOB is skipped, green, with a notice naming the self-serve command
17
- # (`oxy login --env <env>`, then `oxy publish --env <env>`). That is how the
17
+ # (`oxyc login --env <env>`, then `oxyc publish --env <env>`). That is how the
18
18
  # rest of this org already ships: `customer-apps`, Oxy's own monorepo of custom
19
19
  # apps, deleted its publish CI and its engineers publish from their laptops. A
20
20
  # scaffolded repo should behave like that on day one rather than arriving with a
@@ -32,7 +32,7 @@
32
32
  # Deliberately NOT modelled on pokehouse-oxy's `franchise-report-app.yml`
33
33
  # (Docker -> ECR -> ArgoCD). That is the only real, green CI on disk in this
34
34
  # org, which is exactly what makes it tempting; it is a legacy path for the
35
- # one app still on Next.js and has nothing to do with `oxy publish`.
35
+ # one app still on Next.js and has nothing to do with `oxyc publish`.
36
36
  #
37
37
  # Auth is a static `OXY_TOKEN` secret, not GitHub OIDC trusted publishing.
38
38
  # Both appear in this org's examples and a workflow that MIXES them 403s.
@@ -43,13 +43,14 @@
43
43
  # `build` runs the bundles' own postinstall and build scripts — arbitrary
44
44
  # third-party code out of node_modules — and holds no publish credential.
45
45
  # `publish` holds `OXY_TOKEN`, downloads what `build` produced, and runs no
46
- # package script at all: `oxy publish --dir` is documented as "skip the build
47
- # and publish this pre-built directory as-is", so the credential and the
48
- # untrusted code are never in the same job, on the same runner, at the same
46
+ # package script at all. That includes Oxy Functions: `build` bundles them with
47
+ # `oxyc publish --build-only`, and `publish` runs `oxyc publish --prebuilt`,
48
+ # which checks they arrived rather than running esbuild — so the credential and
49
+ # the untrusted code are never in the same job, on the same runner, at the same
49
50
  # time.
50
51
  #
51
52
  # TWO ENVIRONMENTS, and `--env` is passed EXPLICITLY on every publish. This is
52
- # not tidiness either: `oxy publish` defaults `--env` to **production**, so the
53
+ # not tidiness either: `oxyc publish` defaults `--env` to **production**, so the
53
54
  # earlier version of this workflow — which passed no `--env` at all — published
54
55
  # every push to main straight at the customer's live environment. Only ever as
55
56
  # a draft, so nothing went live, but into the wrong environment, and dev/prod
@@ -79,8 +80,8 @@ name: publish
79
80
  on:
80
81
  push:
81
82
  branches: [main]
82
- # Without this filter every README edit would download a ~657 MB CLI to
83
- # publish nothing. `workflow_dispatch` has no path filter, so the no-op
83
+ # Without this filter every README edit would install and build every app
84
+ # to publish nothing. `workflow_dispatch` has no path filter, so the no-op
84
85
  # path is still reachable by hand on a repo with no apps.
85
86
  paths:
86
87
  - "apps/**"
@@ -114,8 +115,7 @@ on:
114
115
  inputs:
115
116
  ref:
116
117
  # Explicit, and required, because of what Promote below actually
117
- # does. There is no promote-an-existing-build command in this CLI —
118
- # `oxy apps` offers show/create/list/delete and nothing else — so
118
+ # does. There is no promote-an-existing-build command in this CLI, so
119
119
  # "promote" can only mean "build this ref again and publish it live".
120
120
  # Leaving the ref implicit would let an operator who reviewed a draft
121
121
  # yesterday ship whatever landed on main since, believing they were
@@ -137,17 +137,11 @@ permissions:
137
137
  contents: read
138
138
 
139
139
  env:
140
- # The same pin as validate.yaml, and it must stay the same: two pins in one
141
- # repo drift, and the one nobody looks at is the one that breaks. Bump both
142
- # together, after re-running the sequence. customer-tooling's own suite has
143
- # a test that fails when they diverge, so this is enforced, not hoped for.
144
- #
145
- # This pin is also load-bearing for `--build-id`. Run-unique build ids
146
- # arrived in 0.5.121; an older CLI defaults to a bare `$GITHUB_SHA`, which
147
- # collides on a re-run or a promote and is rejected with HTTP 409. The
148
- # publish step passes `--build-id` explicitly, so an older CLI would still
149
- # work — the pin is the belt to that pair of braces.
150
- OXY_VERSION: "0.5.126"
140
+ # The `@oxy-hq/cli` (`oxyc`) both jobs install, exactly. Its own pin, not
141
+ # validate.yaml's `OXY_VERSION`: that one is the Rust `oxy` that validates the
142
+ # workspace, a different tool on a different release line. `--build-only` and
143
+ # `--prebuilt`, which the two jobs below depend on, arrived in 0.4.0.
144
+ OXYC_VERSION: "0.4.0"
151
145
 
152
146
  jobs:
153
147
  build:
@@ -205,7 +199,7 @@ jobs:
205
199
  # keeps printf's own exit status, so a failing script inside the
206
200
  # substitution would be swallowed and this step would write an EMPTY
207
201
  # environment name — which downstream would read as "no environment"
208
- # and, without the guard in the publish job, as `oxy publish`'s own
202
+ # and, without the guard in the publish job, as `oxyc publish`'s own
209
203
  # production default. An assignment carries the substitution's
210
204
  # status, so errexit catches it here.
211
205
  name="$(bash .github/scripts/publish-env.sh)"
@@ -248,7 +242,7 @@ jobs:
248
242
  # the dropdown alone builds the right code while GITHUB_SHA names
249
243
  # main's HEAD: the build id and the notice below would both be
250
244
  # confidently wrong, which is worse than vague. `git rev-parse HEAD`
251
- # is also exactly what `oxy publish` records as the build's source
245
+ # is also exactly what `oxyc publish` records as the build's source
252
246
  # commit, so the two agree by construction.
253
247
  sha="$(git rev-parse HEAD)"
254
248
  printf 'sha=%s\n' "$sha" >> "$GITHUB_OUTPUT"
@@ -309,6 +303,27 @@ jobs:
309
303
  # exists and is non-empty before anything is uploaded.
310
304
  run: pnpm -r build
311
305
 
306
+ - name: Bundle each app's Oxy Functions
307
+ if: steps.discover.outputs.count != '0'
308
+ # HERE, in the job with no credential, because bundling runs the app's
309
+ # own esbuild out of node_modules. `--build-only --dir` skips the build
310
+ # that just ran, checks `functions/` is not holding frontend output, and
311
+ # writes `functions/<name>.js` into the out dir for every function the
312
+ # manifest declares — a no-op for an app that declares none. The publish
313
+ # job then checks they arrived instead of bundling again.
314
+ run: |
315
+ set -euo pipefail
316
+ npm install --prefix "$RUNNER_TEMP/oxyc" --no-audit --no-fund "@oxy-hq/cli@$OXYC_VERSION"
317
+ oxyc="$RUNNER_TEMP/oxyc/node_modules/.bin/oxyc"
318
+ status=0
319
+ while IFS="$(printf '\t')" read -r app_dir out; do
320
+ [ -n "$app_dir" ] || continue
321
+ echo "::group::oxyc publish --build-only $app_dir"
322
+ ( cd "$app_dir" && "$oxyc" publish --build-only --dir "$out" ) || status=1
323
+ echo "::endgroup::"
324
+ done < "$RUNNER_TEMP/apps.tsv"
325
+ exit "$status"
326
+
312
327
  - name: Stage the built bundles for the publish job
313
328
  if: steps.discover.outputs.count != '0'
314
329
  run: |
@@ -365,13 +380,13 @@ jobs:
365
380
  retention-days: 1
366
381
 
367
382
  publish:
368
- name: oxy publish
383
+ name: oxyc publish
369
384
  runs-on: ubuntu-latest
370
385
  needs: build
371
386
  # Two gates, and they answer different questions. The first: does this repo
372
387
  # publish from CI at all? Absent `vars.OXY_CI_PUBLISH` it does not, and this
373
388
  # job is skipped with the notice the build job printed — the bundles were
374
- # still built, and shipping them is `oxy publish`, run by a person. The
389
+ # still built, and shipping them is `oxyc publish`, run by a person. The
375
390
  # second: day one, or any repo with no apps, where discovery found nothing.
376
391
  # Either way a SKIP, which is green, not red, and does not prompt for
377
392
  # whatever approval the environment below may require.
@@ -387,7 +402,7 @@ jobs:
387
402
  if: needs.build.outputs.ci-publish == 'on' && needs.build.outputs.count != '0'
388
403
 
389
404
  # The gate, and the credential boundary. `dev` on a push, `production` on
390
- # a manual dispatch — the SAME string this job passes to `oxy publish
405
+ # a manual dispatch — the SAME string this job passes to `oxyc publish
391
406
  # --env` below, resolved once in `build`, so the destination this job
392
407
  # publishes to and the environment it draws its secrets from are the same
393
408
  # thing by construction. Each environment holds its own `OXY_TOKEN`, and a
@@ -432,7 +447,7 @@ jobs:
432
447
  run: |
433
448
  set -euo pipefail
434
449
  # Put each bundle back exactly where the build left it, so
435
- # `oxy publish` sees the tree it would have seen in a single job:
450
+ # `oxyc publish` sees the tree it would have seen in a single job:
436
451
  # oxy-app.json beside its own build output, git provenance intact.
437
452
  # Nothing is executed to do this — it is a copy.
438
453
  staging="$RUNNER_TEMP/bundles"
@@ -446,72 +461,45 @@ jobs:
446
461
  cp -R "$staging/$app_dir/$out/." "$app_dir/$out/"
447
462
  done < "$staging/apps.tsv"
448
463
 
449
- - name: Install the pinned oxy CLI
464
+ - name: Install Node
465
+ uses: actions/setup-node@v4
466
+ with:
467
+ node-version-file: .nvmrc
468
+
469
+ - name: Install the pinned oxyc CLI
470
+ env:
471
+ # This job holds the publish credential (step-scoped, below), so the
472
+ # CLI's own dependencies run no install scripts here.
473
+ npm_config_ignore_scripts: "true"
450
474
  run: |
451
475
  set -euo pipefail
452
- # NOT `bash <(curl ...)`, the form the docs use: process
453
- # substitution hands bash an empty file when the download fails,
454
- # and bash then exits 0 — a step that reports "installed" having
455
- # installed nothing. Download to a file first so curl's own -f
456
- # failure is the step's.
457
- curl -sSfL https://get.oxy.tech -o "$RUNNER_TEMP/get-oxy.sh"
458
- OXY_VERSION="$OXY_VERSION" bash "$RUNNER_TEMP/get-oxy.sh"
459
-
460
- # And check the installer left something behind: it downloads the
461
- # binary with `curl -L` (no -f) and exits 0 regardless.
462
- if [ ! -x "$HOME/.local/bin/oxy" ]; then
463
- echo "::error::the installer exited 0 but left no executable at $HOME/.local/bin/oxy." >&2
464
- exit 1
465
- fi
476
+ # Into a prefix of its own rather than -g: nothing else on the
477
+ # runner's global npm is in play, and the path below is exact.
478
+ npm install --prefix "$RUNNER_TEMP/oxyc" --no-audit --no-fund "@oxy-hq/cli@$OXYC_VERSION"
479
+ echo "$RUNNER_TEMP/oxyc/node_modules/.bin" >> "$GITHUB_PATH"
466
480
 
467
- # The installer drops the binary in ~/.local/bin for a non-root
468
- # user and edits shell rc files, which a GitHub step never reads.
469
- # Put it on PATH explicitly instead of hoping.
470
- echo "$HOME/.local/bin" >> "$GITHUB_PATH"
471
-
472
- - name: Verify the oxy CLI
481
+ - name: Verify the oxyc CLI
473
482
  run: |
474
483
  set -euo pipefail
475
- # The same guard validate.yaml runs, on purpose: one guard with one
476
- # set of failure modes, not two that drift. Two silent failures
477
- # make it load-bearing:
478
- # 1. the installer runs `curl -L` WITHOUT -f, so a tag that does
479
- # not exist writes GitHub's 404 HTML page to the binary path,
480
- # chmod +x's it, and still exits 0;
481
- # 2. this CLI falls through to `--help` with exit 0 on a
482
- # subcommand it does not have, so an old binary "succeeds" at
483
- # doing nothing.
484
- if ! raw="$(oxy --version 2>&1)"; then
485
- echo "::error::'oxy --version' failed — the installed binary is not runnable." >&2
486
- printf '%s\n' "$raw" >&2
484
+ # Exact, not a floor: npm installed exactly the pin, so anything else
485
+ # means PATH found a different oxyc first.
486
+ have="$(oxyc --version)"
487
+ if [ "$have" != "$OXYC_VERSION" ]; then
488
+ echo "::error::oxyc $have is on PATH, not the pinned $OXYC_VERSION." >&2
487
489
  exit 1
488
490
  fi
489
-
490
- have="$(printf '%s\n' "$raw" | sed -n 's/^oxy-app version: \([0-9][0-9.]*\).*/\1/p' | head -1)"
491
- if [ -z "$have" ]; then
492
- echo "::error::could not read a version out of 'oxy --version' — the install probably wrote a 404 page." >&2
493
- printf '%s\n' "$raw" >&2
494
- exit 1
495
- fi
496
-
497
- oldest="$(printf '%s\n%s\n' "$OXY_VERSION" "$have" | sort -t. -k1,1n -k2,2n -k3,3n | head -1)"
498
- if [ "$oldest" != "$OXY_VERSION" ]; then
499
- echo "::error::oxy $have is older than the pinned $OXY_VERSION." >&2
500
- exit 1
501
- fi
502
-
503
- echo "oxy $have (pin $OXY_VERSION)"
491
+ echo "oxyc $have (pin $OXYC_VERSION)"
504
492
 
505
493
  - name: Publish each bundle
506
494
  env:
507
495
  # Step-scoped as well as job-isolated. The job split is what keeps
508
496
  # this token away from the bundles' own build scripts — those ran
509
497
  # in `build`, on a different runner, and nothing in THIS job runs a
510
- # package script (`oxy publish --dir` publishes a pre-built
511
- # directory as-is rather than running the manifest's build). The
512
- # step scope is the cheap second layer: it keeps the token out of
513
- # the environment of the install and verify steps above, which
514
- # execute a downloaded shell script.
498
+ # package script (`oxyc publish --prebuilt --dir` publishes the
499
+ # directory as built, functions included, rather than running the
500
+ # manifest's build or esbuild). The step scope is the cheap second
501
+ # layer: it keeps the token out of the environment of the install
502
+ # and verify steps above, which run code fetched from npm.
515
503
  # Resolved against the ENVIRONMENT named above, where `dev` and
516
504
  # `production` each hold their own.
517
505
  #
@@ -531,7 +519,7 @@ jobs:
531
519
  # repository-level `OXY_TOKEN` after adding the environment ones.
532
520
  OXY_TOKEN: ${{ secrets.OXY_TOKEN }}
533
521
  # `dev` or `production`, resolved once in the build job. Passed to
534
- # `oxy publish --env` below; the identical string selected the
522
+ # `oxyc publish --env` below; the identical string selected the
535
523
  # environment this job is running in.
536
524
  PUBLISH_ENV: ${{ needs.build.outputs.publish-env }}
537
525
  # Optional, and it OVERRIDES `--env` rather than refining it (the
@@ -553,7 +541,7 @@ jobs:
553
541
  set -euo pipefail
554
542
 
555
543
  # Fail closed on the environment before anything else. An empty
556
- # PUBLISH_ENV would mean `oxy publish` fell back to its own default
544
+ # PUBLISH_ENV would mean `oxyc publish` fell back to its own default
557
545
  # — production — while every log line here said nothing at all, and
558
546
  # that silent fallback is the whole bug this workflow was fixed for.
559
547
  # It can only be empty if the build job's resolution step was
@@ -615,8 +603,12 @@ jobs:
615
603
  # publishes at the customer's live environment. It is the FIRST
616
604
  # argument for the same reason: anything appended below is a
617
605
  # refinement of a destination that has already been named.
618
- args=(--env "$PUBLISH_ENV" --dir "$out" --build-id "$build_id")
619
- # --org/--app/--project are deliberately absent: oxy publish
606
+ args=(--env "$PUBLISH_ENV" --dir "$out" --prebuilt --build-id "$build_id")
607
+ # `--prebuilt`: the build job bundled this app's functions into the
608
+ # out dir. The CLI refuses when a declared one did not arrive,
609
+ # rather than publishing a bundle whose function 404s at runtime.
610
+ #
611
+ # --org/--app/--project are deliberately absent: oxyc publish
620
612
  # reads the app's identity from the oxy-app.json in this
621
613
  # directory (discovery has already checked it is there and
622
614
  # complete) and resolves the project from the target's
@@ -629,8 +621,8 @@ jobs:
629
621
  # default — this workflow is not overriding platform behaviour.
630
622
  #
631
623
  # `--promote` publishes THIS build straight to live. It does not
632
- # promote a build that already exists: nothing in this CLI can
633
- # (`oxy apps` is show/create/list/delete). So the run rebuilt the
624
+ # promote a build that already exists: nothing in this CLI can.
625
+ # So the run rebuilt the
634
626
  # ref the operator named and is shipping that, under a new build
635
627
  # id — said out loud above, because "promote" invites the other
636
628
  # reading and the difference is a wrong version in front of
@@ -639,11 +631,11 @@ jobs:
639
631
  args+=(--promote)
640
632
  fi
641
633
 
642
- echo "::group::oxy publish $app_dir"
634
+ echo "::group::oxyc publish $app_dir"
643
635
  # A subshell so the cd cannot leak into the next iteration.
644
- # oxy publish reads oxy-app.json, resolves --dir, and captures
636
+ # oxyc publish reads oxy-app.json, resolves --dir, and captures
645
637
  # git provenance relative to its working directory.
646
- ( cd "$app_dir" && oxy publish "${args[@]}" ) || status=1
638
+ ( cd "$app_dir" && oxyc publish "${args[@]}" ) || status=1
647
639
  echo "::endgroup::"
648
640
  done < "$RUNNER_TEMP/bundles/apps.tsv"
649
641
 
@@ -17,7 +17,7 @@ Start a session scoped to this customer with:
17
17
  | `semantics/topics/` | `*.topic.yml` — question areas: a base view plus the views it may join to. Not one per table. |
18
18
  | `pipelines/` | `*.airway.yml` — Airway ELT pipelines. Credentials live in the secret manager, never in the YAML. |
19
19
  | `workflows/` | `*.automation.yml` — automations. `.automation.yml` is the canonical extension; `.procedure.yml` is legacy and still accepted, so do not start new files with it. |
20
- | `apps/` | Custom app bundles (React + Vite, shipped with `oxy publish`). |
20
+ | `apps/` | Custom app bundles (React + Vite, shipped with `oxyc publish`). |
21
21
  | `config.yml` | Workspace root config — models, databases, defaults. |
22
22
  | `.github/workflows/` | GitHub Actions CI for this repo. Not the same thing as `workflows/` above, which holds oxy automations — the two are one letter of context apart and easy to confuse. |
23
23
  | `.github/scripts/` | Shell the workflows call. Kept out of the YAML because a `run:` block can only ever be executed on a runner, while a script can be run — and tested — anywhere. |
@@ -86,7 +86,7 @@ running it against **a real oxy in the cloud** — there is no local warehouse,
86
86
  no fixture data, and no local `oxy serve` to point at. From the repo root:
87
87
 
88
88
  ```bash
89
- oxy login --env dev # once per env; opens a browser, caches a token
89
+ oxyc login --env dev # once per env; opens a browser, caches a token
90
90
  pnpm dev --env dev # starts everything
91
91
  ```
92
92
 
@@ -97,7 +97,7 @@ stops both together when you press Ctrl-C.
97
97
  **`--env` is required, and that is deliberate.** It says which oxy the app
98
98
  reads from — `dev`, `staging`, `production`, a full URL
99
99
  (`--env https://acme.oxygen-hq.com`), or any name the app's own `oxy-app.json`
100
- declares under `environments`. The underlying `oxy proxy` defaults to
100
+ declares under `environments`. The underlying `oxyc proxy` defaults to
101
101
  **production** if nothing says otherwise, and a `pnpm dev` that quietly serves
102
102
  a customer's live data is not something this repo will do. `--env production`
103
103
  additionally needs `--yes`.
@@ -113,7 +113,7 @@ pnpm dev --env dev sales-dashboard
113
113
 
114
114
  | Process | Port | What it is |
115
115
  | --- | --- | --- |
116
- | `oxy proxy --env <env>` | `3000` | Background. Every `/api` call the app makes goes through here, signed with the token `oxy login` cached, and out to the cloud. It stands in for a local `oxy serve`, which is why the port is not configurable — the Vite plugin already proxies `/api` to `3000`. |
116
+ | `oxyc proxy --env <env>` | `3000` | Background. Every `/api` call the app makes goes through here, signed with the token `oxyc login` cached, and out to the cloud. It stands in for a local `oxy serve`, which is why the port is not configurable — the Vite plugin already proxies `/api` to `3000`. Each Oxy Function call prints a `↳ <status> fn <name> request_id=… trace_id=…` line — the ids to look up when one fails. |
117
117
  | `pnpm dev` in the app | `5173` | Foreground. Vite, with hot reload. **This is the URL you open**: <http://localhost:5173>. |
118
118
 
119
119
  Ctrl-C stops both, and it always returns: a proxy that ignores the polite
@@ -139,9 +139,11 @@ that an afternoon of local clicking cannot change what the customer sees:
139
139
  ### If something is wrong
140
140
 
141
141
  - **Requests come back 401** — the cached token for that env has expired.
142
- `oxy login --env <env>` again.
143
- - **`the 'oxy' CLI is not on your PATH`** — install it with
144
- `curl -sSfL https://get.oxy.tech | bash`; it lands in `~/.local/bin`.
142
+ `oxyc login --env <env>` again.
143
+ - **`the 'oxyc' CLI is not on your PATH`** — install it with
144
+ `npm install -g @oxy-hq/cli`. A global install, not `npx`: Ctrl-C stops the
145
+ proxy by pid, and a wrapper process in between keeps the signal from reaching
146
+ it.
145
147
  - **`this repo has no custom apps yet`** — expected on a fresh repo, and it is
146
148
  not an error. The message says how to add one.
147
149
  - **A change to `apps/` does not show up** — Vite serves the app, but the data
@@ -160,8 +162,8 @@ a long-lived credential sitting in it.
160
162
  From the app's own directory under `apps/`:
161
163
 
162
164
  ```bash
163
- oxy login --env dev # once per env; opens a browser, caches a token
164
- oxy publish --env dev # a draft, to the dev environment
165
+ oxyc login --env dev # once per env; opens a browser, caches a token
166
+ oxyc publish --env dev # a draft, to the dev environment
165
167
  ```
166
168
 
167
169
  `--env` is not optional in practice. The CLI defaults it to **production**, so
@@ -212,14 +214,14 @@ opted in.
212
214
  ### Where an opted-in CI publishes
213
215
 
214
216
  `.github/workflows/publish.yaml` builds every bundle under `apps/` and ships
215
- it with `oxy publish`. It runs on a push to `main` that touches `apps/`, the
217
+ it with `oxyc publish`. It runs on a push to `main` that touches `apps/`, the
216
218
  lockfile, or the workflow itself, and it can be run by hand from the Actions
217
219
  tab.
218
220
 
219
221
  **Where it publishes is decided by how it was triggered, and it is always
220
222
  said out loud:**
221
223
 
222
- | Trigger | Event | `oxy publish --env` | Channel | Reviewers |
224
+ | Trigger | Event | `oxyc publish --env` | Channel | Reviewers |
223
225
  | --- | --- | --- | --- | --- |
224
226
  | push to `main` | `push` | `dev` | draft | none |
225
227
  | Actions tab (**Run workflow**) | `workflow_dispatch` | `production` | draft, or live with **Promote** | whatever `production` requires |
@@ -231,7 +233,7 @@ by `customer-tooling`'s own suite.
231
233
 
232
234
  That mapping lives in `.github/scripts/publish-env.sh`, and a trigger it has
233
235
  no entry for **fails the run** rather than picking one. The reason is worth
234
- knowing, because it was a live bug in this workflow: `oxy publish` defaults
236
+ knowing, because it was a live bug in this workflow: `oxyc publish` defaults
235
237
  `--env` to **production**, so an invocation that leaves the flag off does not
236
238
  publish "nowhere in particular" — it publishes at the customer's live
237
239
  environment. Every publish here names its environment explicitly, and there
@@ -250,7 +252,7 @@ tidiness one. `build` runs `pnpm install` and `pnpm -r build` — which is to
250
252
  say it executes the postinstall and build scripts of every dependency your
251
253
  apps pull in — and holds no publish credential at all. It hands the built
252
254
  output to `publish` as a workflow artifact. `publish` holds `OXY_TOKEN`, and
253
- runs no package script: `oxy publish --dir` uploads a pre-built directory
255
+ runs no package script: `oxyc publish --dir` uploads a pre-built directory
254
256
  as-is instead of building anything. So a compromised dependency in an app's
255
257
  tree never runs on a runner where the token exists.
256
258
 
@@ -325,8 +327,8 @@ worth knowing exactly what the two ways of doing it are:
325
327
  reviewed a draft and want that exact bundle live, this is the one you want.
326
328
  - **The Actions tab**, running this workflow with a **Ref** and **Promote**
327
329
  ticked, **rebuilds that ref and publishes the result live**. It does not
328
- promote an existing draft — no command in the `oxy` CLI can, so the
329
- workflow cannot either. If `main` has moved since the draft you reviewed,
330
+ promote an existing draft — `oxyc publish` cannot, so the workflow cannot
331
+ either. If `main` has moved since the draft you reviewed,
330
332
  this ships the newer commit, under a new build id.
331
333
 
332
334
  That is why the ref is an explicit, required input rather than something the
@@ -15,7 +15,7 @@
15
15
  # committed.
16
16
  node_modules/
17
17
 
18
- # Build output. `out/` is oxy publish's default outDir; `dist/` and `build/`
18
+ # Build output. `out/` is oxyc publish's default outDir; `dist/` and `build/`
19
19
  # are what a Vite or Next app writes when its oxy-app.json names one instead.
20
20
  out/
21
21
  dist/
@@ -6,9 +6,9 @@
6
6
  # why this file exists at all. Before it, running an app locally needed three
7
7
  # commands in two terminals and knowledge that was written down nowhere:
8
8
  #
9
- # oxy proxy --env <env> background, listens on :3000. Forwards the app's
9
+ # oxyc proxy --env <env> background, listens on :3000. Forwards the app's
10
10
  # /api calls to the cloud, signed with the token
11
- # `oxy login --env <env>` cached.
11
+ # `oxyc login --env <env>` cached.
12
12
  # pnpm dev (in the app) foreground, Vite on :5173, which proxies /api to
13
13
  # :3000. That target is the Vite plugin's default,
14
14
  # so the proxy's port is not really negotiable —
@@ -36,7 +36,7 @@ root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
36
36
  # that also uses it looks for it.
37
37
  DISCOVER="$root/.github/scripts/discover-apps.sh"
38
38
 
39
- # `oxy proxy`'s own default, and what @oxy-hq/vite-plugin proxies /api to.
39
+ # `oxyc proxy`'s own default, and what @oxy-hq/vite-plugin proxies /api to.
40
40
  PROXY_PORT=3000
41
41
  # Vite's default. Printed, never bound by this script.
42
42
  DEV_PORT=5173
@@ -60,7 +60,7 @@ Run one of this repo's custom apps locally, against a cloud oxy's real data.
60
60
  pnpm dev --env <env> [app] [--allow-writes] [--allow-events] [--yes]
61
61
 
62
62
  --env <env> REQUIRED. Which oxy to proxy to. There is deliberately no
63
- default: \`oxy proxy\`'s own default is production, and a
63
+ default: \`oxyc proxy\`'s own default is production, and a
64
64
  \`pnpm dev\` that silently reads production is not something
65
65
  this repo will do. One of:
66
66
  dev staging production
@@ -73,7 +73,7 @@ Run one of this repo's custom apps locally, against a cloud oxy's real data.
73
73
  --yes Confirm that --env production is really what you meant.
74
74
  -h, --help This text.
75
75
 
76
- First run, once per env: oxy login --env <env>
76
+ First run, once per env: oxyc login --env <env>
77
77
  EOF
78
78
  }
79
79
 
@@ -315,13 +315,13 @@ if [ -z "$env_name" ]; then
315
315
  "a full URL (--env https://acme.oxygen-hq.com), or a name the app's" \
316
316
  "oxy-app.json declares under \"environments\"." \
317
317
  "" \
318
- "There is no default on purpose. 'oxy proxy' defaults to PRODUCTION, and" \
318
+ "There is no default on purpose. 'oxyc proxy' defaults to PRODUCTION, and" \
319
319
  "a 'pnpm dev' that silently serves production data is not a thing this" \
320
320
  "repo will do quietly."
321
321
  fi
322
322
 
323
323
  if [ "$env_name" = "production" ] && [ "$confirm_yes" -ne 1 ]; then
324
- # `oxy proxy` refuses a production target without --yes on its own. Catching
324
+ # `oxyc proxy` refuses a production target without --yes on its own. Catching
325
325
  # it here matters because the proxy runs in the BACKGROUND: its refusal (or
326
326
  # its prompt) would scroll past under Vite's banner, and the app would just
327
327
  # fail every request with nothing on screen explaining why.
@@ -335,12 +335,13 @@ fi
335
335
 
336
336
  # --- preconditions ----------------------------------------------------------
337
337
 
338
- if ! command -v oxy >/dev/null 2>&1; then
339
- die "the 'oxy' CLI is not on your PATH, and the proxy is oxy." \
338
+ if ! command -v oxyc >/dev/null 2>&1; then
339
+ die "the 'oxyc' CLI is not on your PATH, and the proxy is oxyc." \
340
340
  "" \
341
- " curl -sSfL https://get.oxy.tech | bash" \
341
+ " npm install -g @oxy-hq/cli" \
342
342
  "" \
343
- "It installs to ~/.local/bin; add that to PATH if it is not already there."
343
+ "A global install, not npx: the proxy is killed by pid on Ctrl-C, and a" \
344
+ "wrapper process in between would keep the signal from reaching it."
344
345
  fi
345
346
 
346
347
  if ! command -v pnpm >/dev/null 2>&1; then
@@ -371,7 +372,7 @@ read_port_holder "$PROXY_PORT"
371
372
  if [ -n "$port_holder" ]; then
372
373
  die "port $PROXY_PORT is already taken by: $port_holder" \
373
374
  "" \
374
- "'oxy proxy' has to listen there — the app's dev server proxies /api to" \
375
+ "'oxyc proxy' has to listen there — the app's dev server proxies /api to" \
375
376
  "that exact port — so this cannot just move out of the way. Usually it is" \
376
377
  "a proxy from an earlier run that outlived its terminal, or a local" \
377
378
  "'oxy serve'. Stop it and try again:" \
@@ -389,7 +390,7 @@ if [ "$confirm_yes" -eq 1 ]; then proxy_args+=(--yes); fi
389
390
  say "" \
390
391
  " app $app_dir" \
391
392
  " env $env_name" \
392
- " proxy oxy ${proxy_args[*]} -> http://localhost:$PROXY_PORT" \
393
+ " proxy oxyc ${proxy_args[*]} -> http://localhost:$PROXY_PORT" \
393
394
  " dev pnpm dev in $app_dir -> http://localhost:$DEV_PORT" \
394
395
  ""
395
396
 
@@ -410,23 +411,26 @@ else
410
411
  say " Tracking events are DROPPED, so local clicking never shows up in the" \
411
412
  " customer's analytics. Pass --allow-events to forward them."
412
413
  fi
413
- say "" " Ctrl-C stops both. If requests come back 401, run: oxy login --env $env_name" ""
414
+ say "" " Ctrl-C stops both. If requests come back 401, run: oxyc login --env $env_name" ""
414
415
 
415
- oxy "${proxy_args[@]}" &
416
+ oxyc "${proxy_args[@]}" &
416
417
  proxy_pid=$!
417
418
 
418
- # A proxy that dies on the first second — no cached token, a bad --env — would
419
+ # A proxy that dies on the first second — a bad --env, a production host
420
+ # without --yes — would
419
421
  # otherwise leave Vite up and every request failing, with the reason scrolled
420
422
  # off the top. One second is enough to catch the immediate exits and cannot
421
423
  # produce a false alarm: a proxy still running after it is simply left alone.
422
424
  sleep 1
423
425
  if process_finished "$proxy_pid"; then
424
426
  proxy_pid=""
425
- die "'oxy proxy --env $env_name' exited immediately — see its output above." \
427
+ die "'oxyc proxy --env $env_name' exited immediately — see its output above." \
426
428
  "" \
427
- "Most often there is no cached token for that env yet:" \
429
+ "Most often --env did not resolve to a deployment, or it names a" \
430
+ "production host (an org URL under oxygen-hq.com) and needs --yes." \
431
+ "If it did start but every request is a 401, cache a token for it:" \
428
432
  "" \
429
- " oxy login --env $env_name"
433
+ " oxyc login --env $env_name"
430
434
  fi
431
435
 
432
436
  # The dev server, in the foreground, and the LAST statement in this file.
@@ -1 +0,0 @@
1
- {"version":3,"file":"mcp-VdontkUr.mjs","names":[],"sources":["../src/commands/mcp.ts"],"sourcesContent":["/**\n * `oxyc mcp` — the Oxy API as MCP tools, over stdio.\n *\n * NOT THE SAME THING AS `oxy mcp`. That one is workspace tooling: it takes a\n * local checkout and exposes the semantic model, automations and `.sql` files\n * in it. This one takes a TOKEN and exposes the deployment's HTTP API — the\n * same surface `oxyc api` reaches, for an agent that would rather call a tool\n * than shell out. Different input, different audience, no overlap.\n *\n * FOUR TOOLS, NOT SIX HUNDRED — the design decision that makes this affordable.\n *\n * The obvious shape is one tool per endpoint, and it is the wrong one: the API\n * has ~670 routes, an agent runtime ships every tool's JSON schema in every\n * request, and that is tens of kilobytes of context spent on each turn before\n * a single question is asked. It would also go stale on every deploy, since\n * the tool list would be baked into this package rather than read from the\n * deployment.\n *\n * So discovery stays a QUESTION the agent asks (`routes`, `schema`) rather\n * than a payload it carries, exactly as the CLI does it. Four tools cost a few\n * hundred bytes per turn and reach every endpoint, including ones added after\n * this package was published.\n *\n * Auth, target resolution and placeholder substitution are the CLI's — the\n * same `Context`, so a token cached by `oxyc login` (or `oxy login`) works\n * here with no separate setup, and `{org}` / `{workspace}` resolve the same\n * way.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { CallToolRequestSchema, ListToolsRequestSchema } from \"@modelcontextprotocol/sdk/types.js\";\nimport { loadCatalog, loadOpenApi, searchRoutes } from \"../api/catalog.js\";\nimport { paramsToQuery, parseFields } from \"../api/fields.js\";\nimport { runJq } from \"../api/output.js\";\nimport { isExternalSurface, normalizePath, substitutePlaceholders } from \"../api/paths.js\";\nimport { parseJson, request } from \"../api/request.js\";\nimport type { Context } from \"../context/resolve.js\";\nimport { CliError, ExitCode } from \"../util/errors.js\";\nimport { comparablePath } from \"./discover.js\";\n\n/**\n * Package version, reported in the MCP handshake.\n *\n * Read from `package.json` rather than restated, so a release bump cannot\n * leave the server announcing a version nobody shipped.\n */\nconst VERSION: string = (() => {\n try {\n const here = dirname(fileURLToPath(import.meta.url));\n for (const candidate of [\n resolve(here, \"..\", \"package.json\"),\n resolve(here, \"..\", \"..\", \"package.json\")\n ]) {\n if (existsSync(candidate)) {\n return (\n (JSON.parse(readFileSync(candidate, \"utf8\")) as { version?: string }).version ?? \"0.0.0\"\n );\n }\n }\n } catch {\n // A packaging shape we did not anticipate. The handshake needs a string,\n // not an accurate one.\n }\n return \"0.0.0\";\n})();\n\n/**\n * A tool result. MCP wants content blocks; everything here is text, because\n * the caller is a language model and the payloads are JSON or markdown.\n */\nfunction text(body: string, isError = false) {\n return { content: [{ type: \"text\" as const, text: body }], isError };\n}\n\nconst TOOLS = [\n {\n name: \"oxy_routes\",\n description:\n \"List the API endpoints this Oxy deployment mounts, with what each one does. \" +\n \"ALWAYS call this before oxy_request rather than guessing a path. \" +\n \"Pass a filter (matched against method, path, surface and description) to narrow it — \" +\n \"unfiltered is ~670 routes. Above 60 matches the descriptions are dropped and only \" +\n \"method, path and credential come back, so narrow enough to stay under that when you \" +\n \"need to know what an endpoint does; past ~400 it is refused outright.\",\n inputSchema: {\n type: \"object\",\n properties: {\n filter: {\n type: \"string\",\n description: \"Substring to narrow by, e.g. 'threads', 'sql', 'admin', 'semantic'.\"\n },\n all: {\n type: \"boolean\",\n description:\n \"Include ide-only and worker-only routes. Off by default: those are mounted on one \" +\n \"instance of the fleet, so a caller hitting the load balancer cannot reach them directly.\"\n }\n }\n }\n },\n {\n name: \"oxy_schema\",\n description:\n \"The request and response schema for ONE endpoint, so a body can be built correctly. \" +\n \"Covers the data plane (SQL, semantic query, and the org/workspace lookups); a blank \" +\n \"result means undocumented, not nonexistent — use oxy_routes to confirm the endpoint exists.\",\n inputSchema: {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"Endpoint path, e.g. '{workspace}/sql/query'.\" },\n method: { type: \"string\", description: \"Narrow to one HTTP method.\" }\n },\n required: [\"path\"]\n }\n },\n {\n name: \"oxy_request\",\n description:\n \"Make an authenticated request to the Oxy API. The credential is picked from the path \" +\n \"(/api/** takes a bearer, /external/api/** an API key). Placeholders {org}, {workspace}, \" +\n \"{project}, {customer} and {me} are substituted from context. \" +\n \"Read freely; ask the human before any mutating request against production.\",\n inputSchema: {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"Path relative to /api, e.g. '{org}/workspaces'.\" },\n method: {\n type: \"string\",\n description: \"HTTP method. Defaults to GET, or POST with fields.\"\n },\n fields: {\n type: \"object\",\n description:\n \"Request parameters. Sent as a JSON body on POST/PUT/PATCH, or as query parameters \" +\n \"on GET/HEAD/DELETE. Values keep their JSON type.\"\n },\n jq: {\n type: \"string\",\n description:\n \"A jq program to reduce the response before it is returned. Use it — a full list \" +\n \"response is far more context than the two fields you need.\"\n }\n },\n required: [\"path\"]\n }\n },\n {\n name: \"oxy_whoami\",\n description:\n \"Which deployment this is pointed at and who the token belongs to. \" +\n \"Use it when a call returns 401/403, or a 200 with a null body — on this API an expired \" +\n \"session answers 200 null rather than 401, and this tells the two apart.\",\n inputSchema: { type: \"object\", properties: {} }\n }\n];\n\n/**\n * Above this many matches, `oxy_routes` drops the descriptions.\n *\n * DEGRADES, IT DOES NOT REFUSE — the same shape as `oxyc routes`, which picks\n * `renderDetailed` under 40 matches and the compact method+path table above it\n * (`discover.ts`). The expensive field is `description`, not the row: a trimmed\n * row is a few tens of bytes, so several hundred still fit in a result an agent\n * can read, while the descriptions are what turn a broad match into hundreds of\n * KB of context spent before a question is asked.\n *\n * The threshold is on the COUNT, not on whether a `filter` was supplied — a\n * filter is not evidence of narrowing. `searchRoutes` matches a substring of\n * method, path, surface or description, so `\"admin\"`, `\"workspace\"` and\n * `\"query\"` are all filters a model reaches for in good faith that clear this.\n */\nconst MAX_DESCRIBED_ROUTES = 60;\n\n/**\n * And above THIS many, there is nothing useful to return at all.\n *\n * Deliberately far above the threshold that drops descriptions: refusing is the\n * answer only when even the trimmed listing would be a context dump, which for\n * a deployment of ~670 routes means \"you asked for effectively all of them\".\n */\nconst MAX_ROUTES_PER_RESULT = 400;\n\n/** `{ a: 1 }` → the `-f`/`-F` pairs `parseFields` understands. */\nfunction fieldPairs(fields: Record<string, unknown> | undefined): string[] {\n if (!fields) return [];\n return Object.entries(fields).map(([k, v]) => `${k}=${JSON.stringify(v)}`);\n}\n\n/**\n * Check the arguments the schema says are required.\n *\n * `inputSchema.required` is ADVISORY on the low-level `Server` — it is handed\n * to the model, not enforced by the transport. Without this, `oxy_schema` with\n * no `path` matched every path and returned the whole OpenAPI document, and\n * `oxy_request` with none built a request to `/api/`.\n */\nfunction requireArgs(name: string, args: Record<string, unknown>, required: string[]): void {\n const missing = required.filter((k) => {\n const v = args[k];\n return v === undefined || v === null || (typeof v === \"string\" && v.trim() === \"\");\n });\n if (missing.length > 0) {\n throw new CliError(`${name} needs ${missing.join(\", \")}`, { code: ExitCode.USAGE });\n }\n}\n\nasync function callTool(ctx: Context, name: string, args: Record<string, unknown>) {\n switch (name) {\n case \"oxy_routes\": {\n const catalog = await loadCatalog({ target: ctx.target(), bearer: ctx.maybeBearer() });\n const filter = (args.filter as string | undefined)?.trim();\n let matches = searchRoutes(catalog, filter || undefined);\n if (!args.all) matches = matches.filter((r) => r.role === \"fleet-ok\");\n\n // A cache stale enough to be wrong is worth saying INSIDE the result:\n // `loadCatalog` warns on stderr, which an MCP client never shows anyone.\n const staleNote = catalog.stale\n ? \"NOTE: this route table came from a stale local cache; the deployment was unreachable.\\n\\n\"\n : \"\";\n\n const asked = filter ? ` ${JSON.stringify(filter)}` : \"\";\n\n // Only a match set too large even to LIST is refused. Below that the\n // result degrades instead, because a filter like \"admin\" or \"query\" can\n // clear the description threshold in good faith and \"guess something\n // narrower\" is a dead end for a model with no way to see what it hit.\n if (matches.length > MAX_ROUTES_PER_RESULT) {\n return text(\n `${staleNote}${matches.length} routes match${asked}, which is effectively the whole ` +\n `deployment. Call oxy_routes again with a narrower \\`filter\\` (matched against ` +\n `method, path, surface and description), e.g. \"threads\", \"sql\", \"semantic\", \"admin\".`,\n true\n );\n }\n\n if (matches.length === 0) {\n // An empty array reads as \"this deployment has no such endpoint\",\n // which is usually false — the filter was wrong, or the route is\n // ide-only and hidden by default.\n return text(\n `${staleNote}No route matches ${JSON.stringify(filter ?? \"\")}. Try a broader filter, ` +\n `or pass all=true to include ide-only and worker-only mounts.`,\n true\n );\n }\n // Trimmed to the fields that help a caller build a request. The full\n // record carries the handler path and mount notes, which are for a\n // maintainer reading source, not for an agent composing a call.\n const described = matches.length <= MAX_DESCRIBED_ROUTES;\n // `credential` SURVIVES THE TRIM. It is what separates the bearer mount\n // from the `/external/api` API-key one, so a caller composing a request\n // needs it in either form — and `renderCompact`, the CLI shape this\n // mirrors, keeps it as a per-surface heading. It is also not the\n // expensive field: ~25 bytes a row, ~10 KB across the whole ceiling,\n // against descriptions that run to hundreds of KB.\n const trimmed = matches.map((r) =>\n described\n ? { method: r.method, path: r.path, credential: r.credential, description: r.description }\n : { method: r.method, path: r.path, credential: r.credential }\n );\n // The note says the listing is trimmed AND how to get the descriptions\n // back. Without it a model reads a description-less row as an endpoint\n // that has no documentation, rather than one it did not ask narrowly\n // enough to see.\n const trimNote = described\n ? \"\"\n : `NOTE: ${matches.length} routes match${asked} — too many to describe, so each row is ` +\n `method, path and credential WITHOUT its description. Narrow the \\`filter\\` to ` +\n `${MAX_DESCRIBED_ROUTES} or fewer matches to get the descriptions back.\\n\\n`;\n return text(`${staleNote}${trimNote}${JSON.stringify(trimmed)}`);\n }\n\n case \"oxy_schema\": {\n requireArgs(\"oxy_schema\", args, [\"path\"]);\n const doc = (await loadOpenApi({ target: ctx.target(), bearer: ctx.maybeBearer() })) as {\n paths?: Record<string, Record<string, unknown>>;\n components?: unknown;\n };\n const wanted = String(args.path ?? \"\");\n const method = args.method as string | undefined;\n // `comparablePath` is imported from `discover.ts` rather than copied:\n // a second reduction of the same two spellings is a second thing to keep\n // in step, and the copy here was untested. Exact first, then substring —\n // the order `runSchema` uses, so the two commands answer alike.\n const paths = Object.entries(doc.paths ?? {});\n const exact = paths.filter(([p]) => comparablePath(p) === comparablePath(wanted));\n const hit =\n exact.length > 0\n ? exact\n : paths.filter(([p]) => comparablePath(p).includes(comparablePath(wanted)));\n if (hit.length === 0) {\n return text(\n `No documented schema for ${wanted}. The OpenAPI document covers the data plane only — ` +\n `call oxy_routes with a filter to confirm the endpoint exists.`,\n true\n );\n }\n const selected = Object.fromEntries(\n hit.map(([p, ops]) => [\n p,\n method\n ? Object.fromEntries(\n Object.entries(ops).filter(([verb]) => verb.toLowerCase() === method.toLowerCase())\n )\n : ops\n ])\n );\n return text(JSON.stringify({ paths: selected, components: doc.components }, null, 2));\n }\n\n case \"oxy_request\": {\n requireArgs(\"oxy_request\", args, [\"path\"]);\n const raw = String(args.path ?? \"\");\n const method = String(args.method ?? \"\").toUpperCase() || undefined;\n const fields = parseFields([], fieldPairs(args.fields as Record<string, unknown>));\n const verb = method ?? (fields.present ? \"POST\" : \"GET\");\n const carriesBody = ![\"GET\", \"HEAD\", \"DELETE\"].includes(verb);\n\n let path = substitutePlaceholders(normalizePath(raw), ctx.placeholders());\n let body: string | undefined;\n if (fields.present) {\n if (carriesBody) body = JSON.stringify(fields.params);\n else {\n const query = paramsToQuery(fields.params);\n if (query) path += (path.includes(\"?\") ? \"&\" : \"?\") + query;\n }\n }\n\n const apiKey = ctx.apiKey();\n const external = isExternalSurface(path);\n const response = await request({\n target: ctx.target(),\n path,\n method: verb,\n body,\n bearer: external && apiKey ? ctx.maybeBearer() : ctx.bearer(),\n apiKey\n });\n\n if (response.status < 200 || response.status >= 300) {\n // The STATUS and the body, not a thrown error: the model can act on\n // \"403, here is what the server said\" and cannot act on a transport\n // exception. It is marked `isError` so the runtime shows it as one.\n return text(`HTTP ${response.status} ${response.statusText}\\n${response.body}`, true);\n }\n\n if (response.body.trim() === \"null\") {\n return text(\n \"null\\n\\n(NOTE: a 200 with a null body can mean an expired session on this API, \" +\n \"not 'no such thing'. Call oxy_whoami to tell them apart.)\"\n );\n }\n\n const jq = args.jq as string | undefined;\n if (jq) return text(runJq(response.body, jq));\n return text(response.body);\n }\n\n case \"oxy_whoami\": {\n const target = ctx.target();\n const response = await request({\n target,\n path: \"/api/user\",\n method: \"GET\",\n bearer: ctx.bearer()\n });\n const payload = parseJson(response.body);\n if (payload === null || payload === undefined) {\n return text(\n `target: ${target}\\nThe token no longer resolves to a user — the session has expired. ` +\n `Ask the human to run \\`oxyc login\\`.`,\n true\n );\n }\n return text(JSON.stringify({ target, user: payload }, null, 2));\n }\n\n default:\n return text(`unknown tool: ${name}`, true);\n }\n}\n\n/**\n * Serve MCP on stdio until the client disconnects.\n *\n * STDIO IS THE WHOLE TRANSPORT, deliberately. An HTTP/SSE server would need a\n * port, a lifetime and an auth story of its own; stdio is what every agent\n * runtime already launches, and the process inherits the caller's environment\n * — which is where the token and the target come from.\n *\n * NOTHING MAY BE WRITTEN TO STDOUT except protocol frames. `log.info` and\n * friends already go to stderr, which is why this can share the CLI's own\n * plumbing without corrupting the stream.\n */\nexport async function runMcp(ctx: Context): Promise<void> {\n const server = new Server({ name: \"oxyc\", version: VERSION }, { capabilities: { tools: {} } });\n\n server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));\n\n server.setRequestHandler(CallToolRequestSchema, async (req) => {\n try {\n return await callTool(\n ctx,\n req.params.name,\n (req.params.arguments ?? {}) as Record<string, unknown>\n );\n } catch (cause) {\n // A throw here would kill the session. The model can act on a message;\n // it cannot act on a dead transport.\n const message = (cause as Error).message ?? String(cause);\n // BOTH CHANNELS. A model gets one string, so there is no marker to\n // distinguish them — but reading `hint` alone drops the one line that\n // says what to DO, which is the half a model can act on.\n const { hint, remedy } = cause as { hint?: string; remedy?: string };\n // Blank line between them, the way every other renderer of these two\n // separates them — a model given two consecutive imperatives with no\n // boundary loses the distinction the second field exists to make.\n // `!== undefined` for the narrowing TS gives it, and a length check\n // because an empty string would open the block with a blank line —\n // `filter(Boolean)` covered that and cost the narrowing, so both.\n const extra = [hint, remedy].filter((line) => line !== undefined && line !== \"\").join(\"\\n\\n\");\n return text(extra ? `${message}\\n\\n${extra}` : message, true);\n }\n });\n\n await server.connect(new StdioServerTransport());\n\n // `connect` returns once the transport is wired; the process must stay up\n // until the client closes stdin.\n await new Promise<void>((resolve) => {\n process.stdin.on(\"close\", resolve);\n server.onclose = resolve;\n });\n}\n"],"mappings":"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"}