kylon-cli 0.1.0-next.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.
package/README.md ADDED
@@ -0,0 +1,560 @@
1
+ # kylon-cli
2
+
3
+ Gateway CLI for connecting local agent providers to a P2 workspace.
4
+
5
+ Requires Node.js 22 or newer.
6
+
7
+ ## Install
8
+
9
+ The current release train ships a single self-contained `.mjs` bundle
10
+ as a GitHub Release asset on the `fre-so/p2` repo. The Web UI
11
+ generates two separate commands — an **install** step and a **run**
12
+ step — each with its own Copy button. Operators paste the install
13
+ once per host (and any time they want to upgrade) and the run
14
+ whenever they want to start the daemon. npm is on the roadmap (see
15
+ [Planned: npm install](#planned-npm-install) below) but not wired yet.
16
+
17
+ ### Step 1 — Install kylon (once per host, re-paste to upgrade)
18
+
19
+ ```bash
20
+ gh release download --repo fre-so/p2 --pattern 'kylon.mjs' --output /tmp/kylon.new \
21
+ && chmod +x /tmp/kylon.new \
22
+ && sudo mv /tmp/kylon.new /usr/local/bin/kylon
23
+ ```
24
+
25
+ Downloads the latest bundle to an unprivileged scratch file, flips
26
+ the executable bit, and `sudo mv`'s it into `/usr/local/bin/kylon`
27
+ atomically. The repo is private, so `gh` (authenticated via
28
+ `gh auth login`) is required instead of plain `curl`.
29
+
30
+ Re-paste to upgrade: the `sudo mv` overwrites the binary in place
31
+ with whatever the latest release publishes as `kylon.mjs`. If you
32
+ want a specific build instead of latest, download the matching
33
+ `kylon-vX.Y.Z.mjs` from that release's page — every release
34
+ publishes both the stable-name `kylon.mjs` and the versioned copy.
35
+
36
+ Requirements: Node.js 22+, [GitHub CLI](https://cli.github.com/)
37
+ (`gh auth login`), `sudo`. Linux / macOS only (Windows operators
38
+ should use WSL).
39
+
40
+ ### Step 2 — Start the gateway daemon
41
+
42
+ ```bash
43
+ kylon gateway run \
44
+ --server-url https://<origin>/api \
45
+ --provider codex \
46
+ --api-key <agent-api-key>
47
+ ```
48
+
49
+ `gateway run` registers the session (writing `~/.kylon/gateway-session.json`
50
+ with mode `0600`) and starts the daemon in one step. On the next
51
+ invocation, if a session already exists, plain `kylon gateway run`
52
+ without flags picks it up and jumps straight to start. See
53
+ [Usage → Run](#run) for the full decision table. The daemon runs
54
+ in the foreground until you stop it with `Ctrl+C`; hosts that want a
55
+ supervised daemon typically wrap the same command in
56
+ `launchd` / `systemd` / `supervisord`.
57
+
58
+ ### Local development (contributors)
59
+
60
+ ```bash
61
+ pnpm install
62
+ pnpm --filter kylon-cli build
63
+ ```
64
+
65
+ See [Development → Exposing `kylon` to provider subprocesses](#exposing-kylon-to-provider-subprocesses)
66
+ for the debugger-friendly tsx-based dev loop.
67
+
68
+ ### Planned: npm install
69
+
70
+ The npm distribution path is designed (see
71
+ [`docs/journal_docs/04_13_cli_npx_gateway_run_release_plan.md`](../../docs/journal_docs/04_13_cli_npx_gateway_run_release_plan.md))
72
+ and this package is already publish-ready (`packages/cli/package.json`
73
+ has the metadata and `prepack` runs `bundle:release`). Publishing is
74
+ held pending the rollout-config side of that plan (platform-served
75
+ `recommendedCliVersion`), at which point the Web UI will drop the
76
+ Install step entirely and flip the Run step to
77
+ `npx -y kylon-cli@<recommended> gateway run …`. Don't `npm publish`
78
+ ad-hoc before that — the first live release should come out through
79
+ the paired CI workflow so the token path is exercised end-to-end.
80
+
81
+ ## Usage
82
+
83
+ ### Run
84
+
85
+ `gateway run` is the recommended entrypoint for external agent operators.
86
+ It composes `connect` + `start` into a single command.
87
+
88
+ ```bash
89
+ kylon gateway run \
90
+ --server-url https://api.p2.ai \
91
+ --provider codex \
92
+ --api-key pak_xxxxx
93
+ ```
94
+
95
+ Decision table:
96
+
97
+ | Saved session? | Flags passed? | What `run` does |
98
+ |---|---|---|
99
+ | no | `--server-url` + `--api-key` + `--provider` | connect, persist session, start daemon |
100
+ | no | any field missing | error — lists the missing flag |
101
+ | yes | none | start the daemon from the saved session |
102
+ | yes | any | reconnect with the overrides (falls back to saved values for fields you didn't pass), persist the refreshed session, start the daemon |
103
+
104
+ Only CLI flags count as overrides. Setting `KYLON_API_KEY` in the
105
+ environment does **not** trigger a reconnect on a saved session —
106
+ it still feeds the normal API-key resolution inside `gateway start`.
107
+
108
+ ### Connect
109
+
110
+ Register this machine as the gateway client for an external agent.
111
+ Most operators should prefer `gateway run` above. Use `connect` on its
112
+ own when scripting or when you need to register a session without
113
+ immediately starting the daemon.
114
+
115
+ ```bash
116
+ kylon gateway connect \
117
+ --server-url https://api.p2.ai \
118
+ --api-key pak_xxxxx \
119
+ --provider codex
120
+ ```
121
+
122
+ The agent's API key identifies which agent the daemon will serve;
123
+ channels are bound separately via the "invite agent into channel" flow
124
+ in the web UI.
125
+
126
+ ### Bind
127
+
128
+ Create or update a logical session binding for an agent. Run from the
129
+ target working directory:
130
+
131
+ ```bash
132
+ kylon gateway bind \
133
+ --agent agent_123 \
134
+ --provider codex
135
+ ```
136
+
137
+ Switch an existing binding to a different directory:
138
+
139
+ ```bash
140
+ cd /path/to/other/repo
141
+ kylon gateway bind --agent agent_123 --workdir .
142
+ ```
143
+
144
+ Bindings are keyed by `(gateway session, agent)`. The same agent
145
+ behaves the same way regardless of which channel an assignment
146
+ arrives on — see
147
+ [`docs/journal_docs/05_27_gateway_routing_simplification.md`](../../docs/journal_docs/05_27_gateway_routing_simplification.md).
148
+
149
+ ### Commands
150
+
151
+ | Command | Description |
152
+ |---|---|
153
+ | `kylon gateway run` | **Recommended.** Connect + start in one step; idempotent when a session already exists. |
154
+ | `kylon gateway connect` | Register this machine as the gateway client for an agent (API key identifies which), without starting the daemon. |
155
+ | `kylon gateway bind` | Create or update a logical session binding. |
156
+ | `kylon gateway start` | Start the gateway daemon from a saved session — opens the SSE stream and executes assignments. Reads the API key from the saved session. |
157
+
158
+ ### Run Options
159
+
160
+ | Flag | Description |
161
+ |---|---|
162
+ | `--server-url <url>` | P2 server URL (required for the first run; override otherwise) |
163
+ | `--api-key <key>` | Agent API key, e.g. `pak_xxx` (required for first run; override otherwise. `KYLON_API_KEY` env var satisfies the first-run requirement but does not count as an override on subsequent runs) |
164
+ | `--provider <name>` | Provider CLI: `codex`, `claude-code`, `hermes`, `openclaw`, `generic` (required for first run; override otherwise) |
165
+
166
+ ### Connect Options
167
+
168
+ | Flag | Description |
169
+ |---|---|
170
+ | `--server-url <url>` | P2 server URL (required) |
171
+ | `--api-key <key>` | Agent API key, e.g. `pak_xxx` (required, or set `KYLON_API_KEY`) |
172
+ | `--provider <name>` | Provider CLI: `codex`, `claude-code`, `hermes`, `openclaw`, `generic` (required) |
173
+
174
+ ### Bind Options
175
+
176
+ | Flag | Description |
177
+ |---|---|
178
+ | `--agent <id>` | Agent ID (required) |
179
+ | `--provider <name>` | Provider CLI (required for new, optional for update) |
180
+ | `--workdir <path>` | Working directory (default: current directory) |
181
+
182
+ ### Supported Providers
183
+
184
+ - `codex` — OpenAI Codex CLI
185
+ - `claude-code` — Anthropic Claude Code CLI
186
+ - `hermes` — Hermes CLI using `hermes -z <prompt>`
187
+ - `openclaw` — OpenClaw CLI using `openclaw agent --message <prompt> --json`
188
+ - `generic` - provider-neutral wrapper. Runs a `kylon-provider`
189
+ executable on `PATH` and expects newline-delimited JSON events matching
190
+ `docs/journal_docs/05_25_external_agent_provider_adapter_contract.md`.
191
+
192
+ ## State Model
193
+
194
+ The CLI uses a three-layer state model:
195
+
196
+ - **GatewaySession** — authenticated connection to the P2 server (one per machine)
197
+ - **LogicalSessionState** — per-`(gateway session, agent)` binding holding the current workdir and provider
198
+ - **ProviderRuntimeEntry** — per-conversation provider resume cache, keyed by `(gateway session, channel, agent, scope, provider, workdir)` (disposable)
199
+
200
+ Switching workdir updates the logical session without creating a new one. Provider runtimes are cached per workdir + conversation scope — switching back resumes the old runtime.
201
+
202
+ State is persisted to `~/.kylon/` (or `$XDG_CONFIG_HOME/kylon/`).
203
+
204
+ ## Development
205
+
206
+ > This section assumes you cloned `fre-so/p2` and ran `pnpm install` at the
207
+ > repo root. All commands are run from anywhere in the monorepo unless
208
+ > otherwise noted.
209
+
210
+ ### Inner loop
211
+
212
+ ```bash
213
+ # type-check without emitting
214
+ pnpm --filter kylon-cli typecheck
215
+
216
+ # tsc build — emits dist/bin/kylon.js and other .js files
217
+ pnpm --filter kylon-cli build
218
+
219
+ # unit tests (no network, no DB)
220
+ pnpm --filter kylon-cli test
221
+
222
+ # run the locally built CLI
223
+ node packages/cli/dist/bin/kylon.js --help
224
+ ```
225
+
226
+ The Web agent-settings "Local dev" block generates the same
227
+ `node packages/cli/dist/bin/kylon.js …` invocation from the logged-in
228
+ agent's API key. If you change `src/bin/kylon.ts` or anything it
229
+ imports, rerun `pnpm --filter kylon-cli build` before re-executing.
230
+
231
+ ### Exposing `kylon` to provider subprocesses
232
+
233
+ `node packages/cli/dist/bin/kylon.js gateway run …` starts the daemon
234
+ but leaves **no `kylon` binary on PATH**. When the provider subprocess
235
+ (claude-code / codex) then tries `kylon workspace …` via its Bash tool,
236
+ the shell fails with `command not found`. Two ways to fix this:
237
+
238
+ **Option 1 — `pnpm link` (persistent).** Symlink kylon's published
239
+ `bin.kylon` into pnpm's global bin dir:
240
+
241
+ ```bash
242
+ pnpm --filter kylon-cli bundle # or bundle:minify for a prod-shaped build
243
+ pnpm --filter kylon-cli link --global
244
+
245
+ which kylon
246
+ # → ~/Library/pnpm/kylon (or similar) → packages/cli/dist/kylon-bundle.mjs
247
+ ```
248
+
249
+ From here on, start the daemon via the linked binary instead of the
250
+ raw `node dist/bin/kylon.js`:
251
+
252
+ ```bash
253
+ kylon gateway run --server-url http://localhost:5173/api --provider codex --api-key pak_…
254
+ ```
255
+
256
+ Every provider call to `kylon workspace …` resolves to the linked
257
+ binary. Edit source → `pnpm --filter kylon-cli bundle` → next provider
258
+ call picks it up (each workspace invocation is a fresh process;
259
+ restart the daemon only for *daemon*-side edits). Clean up with
260
+ `pnpm --filter kylon-cli unlink --global`.
261
+
262
+ **Option 2 — `--dev-cli-shim` (ephemeral, IDE-friendly).** The daemon
263
+ can install a temporary bash shim that execs the TS source through
264
+ tsx. Pass `--dev-cli-shim <abs-path-to-p2-repo>` on `gateway run` or
265
+ `gateway start`:
266
+
267
+ ```bash
268
+ node packages/cli/dist/bin/kylon.js gateway run \
269
+ --server-url http://localhost:5173/api \
270
+ --provider codex \
271
+ --api-key pak_… \
272
+ --dev-cli-shim "$(pwd)"
273
+ ```
274
+
275
+ The daemon prints the shim location on startup:
276
+
277
+ ```text
278
+ [dev] kylon shim: /tmp/kylon-dev-shim-abc123/kylon
279
+ [dev] provider calls will exec: node --import tsx /abs/path/packages/cli/src/bin/kylon.ts
280
+ ```
281
+
282
+ The tmpdir is prepended to the daemon's `PATH`, so the provider
283
+ subprocess (and the bash shell it spawns) resolves `kylon` to the
284
+ shim. The shim execs `node --import tsx <src>`, so:
285
+
286
+ - Every `kylon workspace …` invocation reads the current `src/*.ts` —
287
+ no bundle rebuild needed between edits.
288
+ - A Node debugger attached to the daemon is inherited by each shim
289
+ invocation (they exec `node`, so `NODE_OPTIONS` and VS Code's
290
+ Auto-Attach loader pass through).
291
+ - On SIGINT/SIGTERM the tmpdir is wiped.
292
+
293
+ This flag is **only compiled into local (non-minified) builds**. The
294
+ npm package, the GitHub Release bundle, and any `bundle:release`
295
+ output reject `--dev-cli-shim` as an unknown argument and omit it from
296
+ `--help`, so it can never accidentally ship.
297
+
298
+ ### Debugging both the daemon and `kylon workspace` calls
299
+
300
+ Combine `--dev-cli-shim` with a Node debugger to step through the
301
+ full chain — daemon → provider subprocess → `kylon workspace …` — in
302
+ one IDE session.
303
+
304
+ **VS Code**:
305
+
306
+ 1. Enable `Debug: Toggle Auto Attach → Always` (or `Only With Flag`).
307
+ VS Code prepends its `js-debug` bootloader to `NODE_OPTIONS`, which
308
+ every child Node process — including the ones launched by the
309
+ shim — inherits.
310
+ 2. Open an integrated terminal and run:
311
+
312
+ ```bash
313
+ node packages/cli/dist/bin/kylon.js gateway run \
314
+ --server-url http://localhost:5173/api \
315
+ --provider codex \
316
+ --api-key pak_… \
317
+ --dev-cli-shim "$(pwd)"
318
+ ```
319
+
320
+ 3. Set breakpoints in both `packages/cli/src/commands/gateway-start.ts`
321
+ (daemon) and `packages/cli/src/commands/workspace/*.ts` (child
322
+ commands). Both fire the next time the provider issues a workspace
323
+ call.
324
+
325
+ **JetBrains / others**: export `NODE_OPTIONS=--inspect=0.0.0.0:0`
326
+ before launching the daemon. Every subsequent Node process — daemon
327
+ and every `kylon workspace …` invocation — opens its own inspector
328
+ port. Attach your IDE to the process list.
329
+
330
+ No debugger attach? The shim still works — source edits are picked up
331
+ on the next provider call, but breakpoints just don't fire.
332
+
333
+ ### Bundles
334
+
335
+ The release artifact is a single-file ESM bundle produced by
336
+ `scripts/bundle.mjs`. Three variants:
337
+
338
+ | Script | Output | Passes to `bundle.mjs` | Use when |
339
+ |---|---|---|---|
340
+ | `pnpm --filter kylon-cli bundle` | `dist/kylon-bundle.mjs` | _(none)_ | debugging the bundled shape while keeping readable names — not shipped |
341
+ | `pnpm --filter kylon-cli bundle:minify` | `dist/kylon-bundle.mjs` | `--minify` | reproducing the pre-obfuscation size and behavior for a bisect |
342
+ | `pnpm --filter kylon-cli bundle:release` | `dist/kylon-bundle.mjs` | `--minify --obfuscate` | what ships on npm and in GitHub Releases |
343
+
344
+ The `bundle:release` path runs esbuild with `--minify`, then passes the
345
+ output through `javascript-obfuscator`. It's also what the `prepack` hook
346
+ runs, so `pnpm pack` / `npm publish` always produce the obfuscated shape
347
+ even if you forget to call `bundle:release` explicitly.
348
+
349
+ Run the bundle directly to sanity-check it:
350
+
351
+ ```bash
352
+ pnpm --filter kylon-cli bundle:release
353
+ node packages/cli/dist/kylon-bundle.mjs --help
354
+ ```
355
+
356
+ ### Testing
357
+
358
+ Unit tests run against Node's built-in test runner (`node:test`) plus
359
+ `tsx`. They touch the filesystem inside temp dirs but never the
360
+ network or a database, so `pnpm --filter kylon-cli test` is safe to
361
+ run anywhere.
362
+
363
+ E2E tests drive the CLI as a subprocess against a configurable mock
364
+ or live server:
365
+
366
+ ```bash
367
+ # headless e2e (mock provider processes spawned from scripts/mock-*.mjs)
368
+ pnpm --filter kylon-cli test:e2e
369
+
370
+ # live e2e against a real P2 environment — requires doppler secrets
371
+ doppler run --project p2 --config prd -- pnpm --filter kylon-cli test:e2e:live
372
+ ```
373
+
374
+ Live E2E tests provision a throwaway workspace via the REST API, so
375
+ expect them to take several minutes and to leave audit trail rows in
376
+ the target environment. Do not point them at production casually.
377
+
378
+ Before shipping a release, also smoke-test the packaged artifact
379
+ exactly as operators will receive it:
380
+
381
+ ```bash
382
+ # 1. produce the release bundle + tarball
383
+ pnpm --filter kylon-cli bundle:release
384
+ cd packages/cli && pnpm pack --pack-destination /tmp/kylon-out
385
+
386
+ # 2. install the tarball in a clean directory
387
+ WORK=$(mktemp -d) && cd "$WORK"
388
+ npm init -y >/dev/null
389
+ npm install /tmp/kylon-out/kylon-cli-*.tgz
390
+
391
+ # 3. the published shape should have only three entries
392
+ ls node_modules/kylon-cli # dist/ package.json README.md
393
+ ls node_modules/kylon-cli/dist # kylon-bundle.mjs
394
+
395
+ # 4. verify the binary is runnable
396
+ node_modules/.bin/kylon --help
397
+
398
+ # 5. confirm the bundle is actually obfuscated
399
+ head -c 200 node_modules/kylon-cli/dist/kylon-bundle.mjs
400
+ # expect `#!/usr/bin/env node` followed by hexadecimal identifier soup,
401
+ # not recognizable function names or source strings
402
+ ```
403
+
404
+ ### Obfuscation
405
+
406
+ `bundle:release` and `prepack` run `javascript-obfuscator` with a
407
+ conservative preset chosen for runtime safety:
408
+
409
+ - **On:** `compact`, `identifierNamesGenerator: "hexadecimal"`,
410
+ `stringArray` with `base64` encoding + rotate + shuffle, two
411
+ wrapper function layers.
412
+ - **Off:** `controlFlowFlattening`, `deadCodeInjection`,
413
+ `selfDefending`, `debugProtection`, `unicodeEscapeSequence`. These
414
+ trade correctness and startup latency for marginal protection — do
415
+ not turn them on without measuring startup and rerunning the full
416
+ test:e2e suite.
417
+ - `renameGlobals` stays off so Node built-ins keep their names.
418
+
419
+ Startup stays under 100 ms on modern hardware; bundle grows from
420
+ ~85 KB (minify only) to ~230 KB (obfuscated). Obfuscation is a
421
+ tampering and casual-reading deterrent, not a security control — the
422
+ API server is the security boundary.
423
+
424
+ ## Release
425
+
426
+ Today there is **one** active release channel: a bundled `.mjs`
427
+ uploaded as a GitHub Release asset. The npm channel is designed and
428
+ publish-ready but intentionally held pending the
429
+ `recommendedCliVersion` rollout config described in
430
+ [`docs/journal_docs/04_13_cli_npx_gateway_run_release_plan.md`](../../docs/journal_docs/04_13_cli_npx_gateway_run_release_plan.md).
431
+ See [Planned: npm publish](#planned-npm-publish) below.
432
+
433
+ ### Versioning
434
+
435
+ Use explicit semver in `packages/cli/package.json`:
436
+
437
+ | Bump | When |
438
+ |---|---|
439
+ | patch (`0.1.0 → 0.1.1`) | bug fix, no new flags, no behavior change |
440
+ | minor (`0.1.x → 0.2.0`) | backward-compatible capability (new command, new flag) |
441
+ | major (`0.x.x → 1.0.0`) | breaking change to CLI contract or runtime behavior |
442
+
443
+ The GitHub Release tag still uses the historical
444
+ `cli/v0.0.${github.run_number}` scheme, which is fine as an opaque
445
+ build id while there is no consumer that pins to it. The moment we
446
+ flip the npm plan on, bump `packages/cli/package.json` to explicit
447
+ semver and let the publish workflow own version selection.
448
+
449
+ ### Active channel — GitHub Release asset
450
+
451
+ Triggered automatically by `.github/workflows/release-cli.yml` on any
452
+ push to `main` that touches `packages/cli/**`,
453
+ `packages/workspace-cli-core/**`, or `packages/types/**`. The workflow:
454
+
455
+ 1. Runs `pnpm --filter kylon-cli bundle:release` (obfuscated output).
456
+ 2. Smoke-tests `node packages/cli/dist/kylon-bundle.mjs --help`.
457
+ 3. Copies the bundle to **two** asset names under the release:
458
+ - `kylon-v0.0.${run_number}.mjs` — versioned, useful for pinning.
459
+ - `kylon.mjs` — stable name, downloadable via
460
+ `gh release download --repo fre-so/p2 --pattern 'kylon.mjs'`.
461
+ 4. Creates a GitHub Release tagged `cli/v0.0.${run_number}` with both
462
+ `.mjs` files attached.
463
+
464
+ The repo is private, so downloads require `gh` (authenticated) rather
465
+ than plain `curl`. This is the shape the Web UI generates:
466
+
467
+ ```bash
468
+ gh release download --repo fre-so/p2 --pattern 'kylon.mjs' --output /tmp/kylon
469
+ chmod +x /tmp/kylon
470
+ sudo mv /tmp/kylon /usr/local/bin/kylon
471
+ ```
472
+
473
+ Pin to a specific build by downloading the versioned asset from its
474
+ release page instead.
475
+
476
+ No npm token required; the workflow only needs `contents: write`.
477
+
478
+ ### Rollback (GitHub channel)
479
+
480
+ If the most recent push produced a broken CLI:
481
+
482
+ 1. Identify the last known good release under
483
+ <https://github.com/fre-so/p2/releases>.
484
+ 2. Manually re-upload that release's bundle under the name
485
+ `kylon.mjs` on the most recent release — that's what the Web UI's
486
+ stable URL resolves against. (Alternatively, revert the bad commit
487
+ and let `release-cli.yml` cut a new release.)
488
+ 3. Hosts with persistent installs re-run `curl -fsSL … -o
489
+ /usr/local/bin/kylon` to pull the rolled-back bundle.
490
+
491
+ There is no equivalent of `npm deprecate` on GitHub Releases, so a
492
+ broken bundle is "fixed" only by publishing a newer one.
493
+
494
+ ### Planned: npm publish
495
+
496
+ This package is already wired for `npm publish` (metadata, `files`,
497
+ `prepack`, `publishConfig.access: public`). The only things missing
498
+ are the publish CI workflow and the owning npm account. The plan:
499
+
500
+ 1. Someone claims `kylon-cli` on npm (the name is currently 404).
501
+ 2. A granular publish token (Read + Write on `kylon-cli`) is stored
502
+ as the `NPM_TOKEN` GitHub Actions secret on `fre-so/p2`.
503
+ 3. Split the release automation:
504
+ - **`cli-verify.yml`** — runs on PRs and pushes touching
505
+ `packages/cli/**`. Runs typecheck, lint, test, `bundle:release`,
506
+ and `npm pack`. Does not publish.
507
+ - **`cli-publish.yml`** — runs on push of tag `cli/vX.Y.Z`.
508
+ Verifies the tag matches `packages/cli/package.json#version`,
509
+ then `pnpm --filter kylon-cli publish --access public
510
+ --no-git-checks` with
511
+ `NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}`.
512
+ 4. The API exposes `recommendedCliVersion`; the Web UI flips from
513
+ the `curl` shape above to `npx -y kylon-cli@<recommended> gateway
514
+ run …`.
515
+
516
+ Until (2) + (3) land, do **not** `npm publish` ad-hoc — the first
517
+ live release should come out through the paired CI workflow so the
518
+ token path is exercised end-to-end. Once the workflow exists, the
519
+ release ritual becomes:
520
+
521
+ ```bash
522
+ # 1. On a release branch
523
+ pnpm --filter kylon-cli version 0.1.1 # bumps packages/cli/package.json only
524
+
525
+ # 2. Merge to main via PR, verify CI is green
526
+
527
+ # 3. Tag the commit on main
528
+ git pull
529
+ git tag cli/v0.1.1
530
+ git push origin cli/v0.1.1
531
+
532
+ # 4. GitHub Actions publishes; verify
533
+ npm view kylon-cli@0.1.1 dist-tags
534
+ ```
535
+
536
+ Rollback once npm is live: flip `recommendedCliVersion` in the API
537
+ back to the last known good version (new operators get that version
538
+ in their install command; in-flight daemons keep their current CLI
539
+ until they restart). If the pushed version is actively broken, also
540
+ `npm deprecate kylon-cli@<bad> "…"` so operators that ignore the
541
+ recommendation see a warning. Do not `npm unpublish`.
542
+
543
+ ### Pre-release channel (npm-era, also planned)
544
+
545
+ Once npm is live, risky changes can go out under a `next` dist-tag so
546
+ the Web UI's recommended version is untouched:
547
+
548
+ ```bash
549
+ # 1. Bump to a prerelease version
550
+ pnpm --filter kylon-cli version 0.2.0-next.0
551
+
552
+ # 2. Publish under the `next` dist-tag
553
+ cd packages/cli && pnpm publish --tag next --access public --no-git-checks
554
+
555
+ # 3. Operators can opt in explicitly
556
+ npx -y kylon-cli@next gateway run …
557
+
558
+ # 4. Promote to `latest` once validated
559
+ npm dist-tag add kylon-cli@0.2.0-next.0 latest
560
+ ```