kylon-cli 0.1.0-next.12

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