@kontextmind/kxm 0.7.173 → 0.7.175

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.
@@ -11,7 +11,7 @@
11
11
  "name": "kxm",
12
12
  "source": "./plugins/kxm",
13
13
  "description": "Durable workflows, peer agents, and kxm tui",
14
- "version": "0.7.173",
14
+ "version": "0.7.175",
15
15
  "category": "development",
16
16
  "tags": ["kxm", "multi-agent", "workflows", "mcp"]
17
17
  }
package/CHANGELOG.md CHANGED
@@ -181,6 +181,19 @@ All notable user-facing changes are documented here. The project follows [Semant
181
181
 
182
182
  ### Changed
183
183
 
184
+ - **`kxm update --kxm` defaults to npm.** Missing or source-less `update.yaml` now
185
+ uses the public `@kontextmind/kxm` package instead of the GitHub release tarball.
186
+ `source: github` still selects the digest-checked tarball. When the working
187
+ directory is this repository, `--kxm` installs the local files and does not fetch.
188
+ - **`kxm update` is a full update by default.** Bare `kxm update` and `--all` run
189
+ detected harness `--self` first (default harness leading), then the kxm package,
190
+ then extensions and models. `--kxm` remains kxm-only. Detected harness updater
191
+ failures on `--all` degrade the receipt and do not fail the command; an unknown
192
+ harness id still exits 2.
193
+ - **`kxm land` runs `npm` on Windows.** The verify stage launches `npm.cmd` through `cmd.exe` when `npm` is not an `.exe`.
194
+ - **npm 12 `pack --json`.** The install smoke and pack tests accept the package-keyed object as well as the older one-element array.
195
+ - **Roadmap links on Windows.** `kxm land` keeps the git symlink text when this process cannot create the `docs/roadmap` links.
196
+
184
197
  - **Tool policy is enforced at load and before dispatch.**
185
198
  `coordinator` and `read-only` select the read-only one-shot profile.
186
199
  `workspace-writer` and `tests-writer` select the edit profile. Dispatch
@@ -2,7 +2,7 @@
2
2
  title: "Decisions"
3
3
  description: "Decisions of medium significance or higher made by automated KXM work in this repository. One entry per decision: the options evaluated with their weights, and why the selected option won. Recorded the turn the decision is taken."
4
4
  audience: "agents and maintainers"
5
- updated: "2026-09-28"
5
+ updated: "2026-10-02"
6
6
  ---
7
7
 
8
8
  # Decisions
@@ -151,3 +151,24 @@ Evidence: Evidence: test/core/oneshot-producer.test.ts (real reviewer shapes), r
151
151
  - Selected: port through the engine lane `vision-assert-port`; the PR is
152
152
  closed by its replacement landing, not by hand.
153
153
  Evidence: Evidence: merge-base 77beda4 predates the CLI restructure; merge-tree showed cli/vnext.ts deleted on main; vision-gate.ts already on main.
154
+
155
+ ## 2026-10-02 — Porting the stale update-default branch onto main (landing)
156
+
157
+ - Context: `feat/update-npm-default-and-all` was based on 2026-09-24 main
158
+ and still uncommitted. `origin/main` had moved about 66 commits and
159
+ already contained the claude-bridge `apiKey` registration fix and the
160
+ `gh`/`curl` release downloader. `kxm land`'s rebase stage only resolves
161
+ union conflicts, which cannot merge the `planKxmPackageUpdate` signature
162
+ clash (`downloader` vs `localRoot`).
163
+ - Options:
164
+ - Rebase the 2026-09-24 branch through `kxm land`'s rebase stage — weight
165
+ 0.2. Its union-only resolver cannot handle the `planKxmPackageUpdate`
166
+ signature clash.
167
+ - Stash-apply onto a fresh lane, take upstream for claude-bridge, `dist`,
168
+ and `package.json`, and merge the rest — weight 0.9. Keeps the reviewed
169
+ update-default work and the upstream fixes that must not be duplicated.
170
+ - Discard and re-dispatch to the writer — weight 0.3. Re-authors reviewed
171
+ work that already had a specified merge.
172
+ - Selected: stash-apply onto a fresh lane off `origin/main`, take upstream
173
+ for claude-bridge, `dist`, and `package.json`, and merge the rest.
174
+ Evidence: Evidence: `git diff HEAD origin/main` on the stale branch showed upstream's claude-bridge fix and curl downloader already landed; the lane port keeps both those and the npm/`--all` behavior.
@@ -145,7 +145,7 @@ The context suites in detail:
145
145
  | Permission-diff trust: authority lattice, prose neutrality, Git base shadowing, CLI diff and check | `permission.test.ts`, `cli.test.ts`, `contracts.test.ts`, `package-install.test.ts` |
146
146
  | KXM schemas, restricted YAML fixtures, cross-resource semantics and sync-safe rejection | `contracts.test.ts`, `restricted-yaml.test.ts` |
147
147
  | Harness detection, auth and dispatch for the built-in catalog, including Windows launch rules | `harness.test.ts` |
148
- | `kxm update`: `update.yaml` validation, GitHub or npm version checks, install-kind detection, `kxm-<v>.tgz` asset selection | `kxm-update.test.ts`, `kxm-update-cli.test.ts`, `kxm-install-kind.test.ts` |
148
+ | `kxm update`: `update.yaml` validation, npm default / GitHub override, local checkout install, install-kind detection, `kxm-<v>.tgz` asset selection | `kxm-update.test.ts`, `kxm-update-cli.test.ts`, `kxm-install-kind.test.ts` |
149
149
 
150
150
  The round trip seeds its Runtime stores in a project-local `.kxm/runtime/`
151
151
  layout that the Runtime never writes. Separate tests seed two projects' event
@@ -6,7 +6,7 @@ Move the `kxm` CLI, the hub, the Runtime, and the Claude Code or Pi integrations
6
6
 
7
7
  - Protected storage for a full backup; see [Back up and restore KXM](backup-and-restore.md).
8
8
  - A window in which the hub, the Runtime supervisor and long-lived workers may stop.
9
- - The GitHub CLI (`gh`, signed in) if you apply updates with `kxm update --kxm` from the default `github` source. When `gh` is not installed, that command downloads the same `kxm-<version>.tgz` with curl and still checks the release sha256 before installing. Without a working `kxm` either, use the curl commands in [Update the CLI without gh](#update-the-cli-without-gh).
9
+ - The GitHub CLI (`gh`, signed in), only when `update.yaml` sets `source: github`. When `gh` is not installed, that command downloads the same `kxm-<version>.tgz` with curl and still checks the release sha256 before installing. Without a working `kxm` either, use the curl commands in [Update the CLI without gh](#update-the-cli-without-gh).
10
10
 
11
11
  ## Check for an update
12
12
 
@@ -17,7 +17,7 @@ kxm update --check
17
17
  Expected output when a newer release exists:
18
18
 
19
19
  ```text
20
- kxm 0.7.1 → 0.7.2 available · kxm update --kxm
20
+ kxm 0.7.1 → 0.7.2 available · kxm update
21
21
  ```
22
22
 
23
23
  From a source checkout, the check makes no network call and reports the checkout instead:
@@ -26,7 +26,7 @@ From a source checkout, the check makes no network call and reports the checkout
26
26
  kxm 0.7.1 (running from source at /work/kxm)
27
27
  ```
28
28
 
29
- The check reads `update.yaml` under the user state root. Its `source` is `github` (release tarballs) by default; `npm` is also accepted. See [Updater settings](../reference/config-reference.md#updater-settings-kxmupdatev1). Add `--json` to see the install kind KXM detected.
29
+ The check reads `update.yaml` under the user state root. Its `source` is `npm` by default; `github` (release tarballs) is also accepted. When the working directory is this repository, `kxm update --kxm` installs the local files instead of fetching. See [Updater settings](../reference/config-reference.md#updater-settings-kxmupdatev1). Add `--json` to see the install kind KXM detected.
30
30
 
31
31
  ## Stop the services and back up
32
32
 
@@ -48,7 +48,7 @@ Use the path that matches how KXM was installed. `kxm update --kxm` applies an u
48
48
 
49
49
  | Install kind | Upgrade with |
50
50
  |---|---|
51
- | Global npm install | `kxm update --kxm` (release tarball, or npm with `source: npm`), or `npm install --global --omit=peer @kontextmind/kxm@latest` |
51
+ | Global npm install | `kxm update --kxm` (npm registry, or GitHub tarball with `source: github`), or `npm install --global --omit=peer @kontextmind/kxm@latest` |
52
52
  | Pi package | `pi update` |
53
53
  | Claude Code marketplace plugin | Reinstall the plugin; see the plugin note below |
54
54
  | Project dependency | `npm install @kontextmind/kxm@latest` in that project |
@@ -77,7 +77,7 @@ Refuse a tarball whose sha256 does not match the release asset digest.
77
77
 
78
78
  ### Update harnesses and plugins
79
79
 
80
- Without `--check` or a lone `--kxm`, `kxm update` also runs the native updaters of detected harnesses. Narrow it with a harness id and one scope:
80
+ Bare `kxm update` (same as `--all`) runs detected harness `--self` updaters first, then the kxm package, then extensions and models; `--kxm` alone updates only kxm. Narrow it with a harness id and one scope:
81
81
 
82
82
  ```bash
83
83
  kxm update --dry-run # plan every detected harness
@@ -971,7 +971,7 @@ Not run: stores a credential.
971
971
  ## `kxm update`
972
972
 
973
973
  ```text
974
- kxm update [harness] [--check | --kxm] [--self | --extensions | --models]
974
+ kxm update [harness] [--check | --kxm] [--all | --self | --extensions | --models]
975
975
  ```
976
976
 
977
977
  Checks for or applies a KXM operator package update, and runs the native updaters of detected harnesses.
@@ -979,14 +979,15 @@ Checks for or applies a KXM operator package update, and runs the native updater
979
979
  | Option | Argument | Default | Description |
980
980
  |---|---|---|---|
981
981
  | `--check` | none | off | Check for a kxm package update without applying |
982
- | `--kxm` | none | off | Apply the kxm operator package update (GitHub release tarball or npm) |
983
- | `--self` | none | off | Update only the harness CLI |
982
+ | `--all` | none | off | Update harness CLIs first (`--self`), then kxm, extensions, and models (default for a bare `kxm update`) |
983
+ | `--kxm` | none | off | Apply only the kxm operator package update (npm by default, GitHub tarball when configured, or local files in this repo) |
984
+ | `--self` | none | off | Update only harness CLIs |
984
985
  | `--extensions` | none | off | Update only extensions/plugins (Pi packages, Claude kxm) |
985
986
  | `--models` | none | off | Refresh model catalogs where the harness supports it |
986
987
 
987
988
  - Arguments: `[harness]`, Harness id (default: every detected harness).
988
- - `--check` cannot be combined with any other update flag or a harness (exit 2, `scope_conflict`), and at most one of `--self`, `--extensions`, `--models` is allowed (exit 2).
989
- - From a source checkout, `--check` reports the running version without network access, and `--kxm` is refused (exit 2, `install_kind_source`) with an instruction to `git pull`. Only npm-global installs can apply `--kxm`; other install kinds exit 2 with `install_kind_<kind>`. A GitHub release must publish a sha256 digest for `kxm-<version>.tgz` or the install fails closed (`release_digest_missing`, `release_digest_mismatch`). When `gh` is missing, the github source downloads that same asset with `curl --fail --silent --show-error --location` and still verifies the digest before `npm install -g`.
989
+ - `--check` cannot be combined with any other update flag or a harness (exit 2, `scope_conflict`), and at most one of `--all`, `--self`, `--extensions`, `--models` is allowed (exit 2).
990
+ - Default `source` is npm. Set `update.yaml` `source: github` for the release tarball. When the working directory is this repository (`@kontextmind/kxm` plus `.git`), `--check` reports `source: local` without a network fetch and `--kxm` runs `npm install --global --omit=peer <cwd>`. From a source-built binary used outside this repo, `--check` reports the running version without network access, and `--kxm` is refused (exit 2, `install_kind_source`) with an instruction to `git pull`. Only npm-global installs can apply a registry or GitHub `--kxm`; other install kinds exit 2 with `install_kind_<kind>`. A GitHub release must publish a sha256 digest for `kxm-<version>.tgz` or the install fails closed (`release_digest_missing`, `release_digest_mismatch`). When `gh` is missing, the github source downloads that same asset with `curl --fail --silent --show-error --location` and still verifies the digest before `npm install -g`.
990
991
 
991
992
  On a host with neither `gh` nor a working `kxm`, download and check it yourself:
992
993
 
@@ -1001,7 +1002,7 @@ npm install --global --omit=peer "./kxm-${version}.tgz"
1001
1002
  ```
1002
1003
 
1003
1004
  - Settings come only from `update.yaml` under the user state root (`auto: true` enables auto-apply); a project `.kxm/update.yaml` is ignored with a warning.
1004
- - Without `--check` or a lone `--kxm`, KXM probes harnesses (as `harness list` does) and runs each updater for the selected scope. An unknown harness id exits 2 (`unknown_harness` step); a failed step exits 1.
1005
+ - Without `--check` or a lone `--kxm`, KXM probes harnesses (as `harness list` does) and runs each updater for the selected scope. An unknown harness id exits 2 (`unknown_harness` step); a failed step exits 1, except under `--all` (the default), where a failed harness updater sets `degraded: true` and exits 0.
1005
1006
  - Honors `--dry-run`: steps are planned, not run, and the cached update notice is not refreshed.
1006
1007
  - JSON keys: `--check` gives `current`, `available`, `auto`, `source`, `latest`, `message`, `installKind`, `root`; otherwise `dryRun`, `scope`, `notice`, `kxm` (when applying), `steps` (`harness`, `scope`, `command`, `args`, `outcome`, `detail`), `installKind`, `root`.
1007
1008
 
@@ -1010,7 +1011,7 @@ kxm update --check --dry-run --json
1010
1011
  ```
1011
1012
 
1012
1013
  ```text
1013
- {"schema":"kxm.cli-result.v1","ok":true,"command":"update check","current":"0.7.1","available":false,"auto":false,"source":"github","installKind":"source","root":"/work/kxm","message":"kxm 0.7.1 (running from source at /work/kxm)"}
1014
+ {"schema":"kxm.cli-result.v1","ok":true,"command":"update check","current":"0.7.1","available":false,"auto":false,"source":"npm","installKind":"source","root":"/work/kxm","message":"kxm 0.7.1 (running from source at /work/kxm)"}
1014
1015
  ```
1015
1016
 
1016
1017
  ```bash
@@ -1656,14 +1656,14 @@ never mints a token or writes a file, and always exits 0. See the
1656
1656
  |---|---|---|
1657
1657
  | `schema` | `kxm.update.v1` | Required |
1658
1658
  | `auto` | Boolean | Required |
1659
- | `source` | `github` or `npm` | Optional, `github` |
1659
+ | `source` | `github` or `npm` | Optional, `npm` |
1660
1660
 
1661
1661
  Unknown fields are refused (`update.yaml unknown field <name>`).
1662
1662
 
1663
1663
  ```yaml
1664
1664
  schema: kxm.update.v1
1665
1665
  auto: false # required boolean
1666
- source: github # github (default) | npm
1666
+ source: npm # npm (default) | github
1667
1667
  ```
1668
1668
 
1669
1669
  ## Workspace layout: tracked, ignored, and state
@@ -8,7 +8,7 @@ KXM has three parts you can install: the `kxm` command-line tool, the Claude Cod
8
8
  - Git.
9
9
  - Claude Code, if you want the plugin.
10
10
  - Pi, if you want Pi agents. Install it with `npm install --global @earendil-works/pi-coding-agent` and sign in to a model provider as the [Pi documentation](https://pi.dev/docs/latest) describes.
11
- - The GitHub CLI (`gh`), only if you plan to update with `kxm update --kxm` from the GitHub source. When `gh` is missing, that command falls back to curl and still checks the release checksum. The manual curl commands are in [Upgrade KXM](../operations/upgrade.md#update-the-cli-without-gh).
11
+ - The GitHub CLI (`gh`), only if `update.yaml` sets `source: github` so `kxm update --kxm` downloads the release tarball. When `gh` is missing, that command falls back to curl and still checks the release checksum. The manual curl commands are in [Upgrade KXM](../operations/upgrade.md#update-the-cli-without-gh).
12
12
 
13
13
  ## Choose what to install
14
14
 
@@ -107,13 +107,13 @@ npm ci
107
107
  node scripts/kxm.mjs --version
108
108
  ```
109
109
 
110
- Use `node scripts/kxm.mjs` wherever the docs show `kxm`. It runs the CLI bundle committed in `plugins/kxm/dist/`. Update a checkout with `git pull` and `npm ci`; `kxm update --kxm` refuses to run there. [Develop KXM](../contributing/development.md) covers building and testing.
110
+ Use `node scripts/kxm.mjs` wherever the docs show `kxm`. It runs the CLI bundle committed in `plugins/kxm/dist/`. Update a checkout with `git pull` and `npm ci`. From this repository, `kxm update --kxm` installs those local files globally. [Develop KXM](../contributing/development.md) covers building and testing.
111
111
 
112
112
  ## Update
113
113
 
114
114
  | Part | Command |
115
115
  |---|---|
116
- | `kxm` CLI | `npm install --global --omit=peer @kontextmind/kxm@latest` (check first with `kxm update --check`) |
116
+ | `kxm` CLI | `kxm update --kxm` or `npm install --global --omit=peer @kontextmind/kxm@latest` (check first with `kxm update --check`) |
117
117
  | Claude Code plugin | `claude plugin marketplace update kxm`, then uninstall and reinstall the plugin; `claude plugin update` never refreshes it |
118
118
  | Pi package | `pi update --extensions` |
119
119
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontextmind/kxm",
3
- "version": "0.7.173",
3
+ "version": "0.7.175",
4
4
  "description": "KXM local-first multi-agent orchestration and operator dashboard",
5
5
  "type": "module",
6
6
  "author": "KontextMind",
@@ -37,7 +37,7 @@
37
37
  "test:core": "npm run build && node scripts/run-bounded.mjs 1200000 node --disable-warning=ExperimentalWarning --experimental-strip-types --test --test-force-exit --test-concurrency=4 test/core/*.test.ts",
38
38
  "test:simulations": "npm run build && node scripts/run-bounded.mjs 1200000 node --disable-warning=ExperimentalWarning --experimental-strip-types --test --test-force-exit --test-concurrency=4 test/simulations/*.test.ts",
39
39
  "test:complete": "npm run build && node scripts/run-bounded.mjs 2400000 node --disable-warning=ExperimentalWarning --experimental-strip-types --test --test-force-exit --test-concurrency=4 test/core/*.test.ts test/simulations/*.test.ts",
40
- "test": "npm run build && node scripts/run-bounded.mjs 1200000 node --disable-warning=ExperimentalWarning --experimental-strip-types --test --test-force-exit --test-concurrency=4 test/core/*.test.ts packages/core/*/tests/unit/*.test.ts",
40
+ "test": "npm run build && node scripts/run-bounded.mjs 1800000 node --disable-warning=ExperimentalWarning --experimental-strip-types --test --test-force-exit --test-concurrency=4 test/core/*.test.ts packages/core/*/tests/unit/*.test.ts",
41
41
  "test:ci-shard": "node scripts/ci-unit-shard.mjs",
42
42
  "e2e": "node scripts/obscura.mjs --ensure && playwright test",
43
43
  "test:coverage:core": "npm run build && node scripts/run-bounded.mjs 2400000 node --disable-warning=ExperimentalWarning --experimental-strip-types --test --test-force-exit --test-concurrency=4 --experimental-test-coverage --test-coverage-lines=91 --test-coverage-branches=80 --test-coverage-functions=92 --test-coverage-include=plugins/kxm/src/**/*.ts --test-coverage-exclude=plugins/kxm/src/server.ts --test-coverage-exclude=plugins/kxm/src/mcp-server.ts --test-coverage-exclude=plugins/kxm/src/runtime-supervisor.ts --test-coverage-include=packages/core/*/src/**/*.ts test/core/*.test.ts packages/core/*/tests/unit/*.test.ts",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "kxm",
4
4
  "displayName": "KXM",
5
- "version": "0.7.173",
5
+ "version": "0.7.175",
6
6
  "description": "Headless multi-agent orchestration, durable workflows, and a live operator dashboard for Pi and Claude Code",
7
7
  "author": {
8
8
  "name": "KontextMind",
@@ -19780,8 +19780,17 @@ function argsFor(entry, scope) {
19780
19780
  if (scope === "extensions") return entry.update.extensions;
19781
19781
  return entry.update.models;
19782
19782
  }
19783
+ function detectedHarnesses(inventory, requestedHarness) {
19784
+ if (requestedHarness) return inventory.harnesses.filter((entry) => entry.id === requestedHarness);
19785
+ const detected = inventory.harnesses.filter((entry) => entry.detected);
19786
+ const preferred = inventory.defaultHarness;
19787
+ return [
19788
+ ...detected.filter((entry) => entry.id === preferred),
19789
+ ...detected.filter((entry) => entry.id !== preferred)
19790
+ ];
19791
+ }
19783
19792
  function planHarnessUpdate(inventory, requested) {
19784
- const selected = requested.harness ? inventory.harnesses.filter((entry) => entry.id === requested.harness) : inventory.harnesses.filter((entry) => entry.detected);
19793
+ const selected = detectedHarnesses(inventory, requested.harness);
19785
19794
  if (requested.harness && selected.length === 0) {
19786
19795
  return [{
19787
19796
  harness: requested.harness,
@@ -19792,36 +19801,44 @@ function planHarnessUpdate(inventory, requested) {
19792
19801
  detail: isKnownHarnessId(requested.harness) ? "not_detected" : "unknown_harness"
19793
19802
  }];
19794
19803
  }
19804
+ const waves = requested.scope === "all" ? ["self", "extensions", "models"] : [requested.scope];
19795
19805
  const steps = [];
19796
- for (const status of selected) {
19797
- const entry = BUILTIN_HARNESSES.find((candidate) => candidate.id === status.id);
19798
- if (!entry) continue;
19799
- const scopes = scopesFor(requested.scope, entry);
19800
- if (scopes.length === 0) {
19801
- steps.push({
19802
- harness: status.id,
19803
- scope: requested.scope === "all" ? "self" : requested.scope,
19804
- command: status.command ?? entry.commands[0],
19805
- args: [],
19806
- outcome: "skipped",
19807
- detail: "no_updater"
19808
- });
19809
- continue;
19810
- }
19811
- if (!status.detected || !status.command) {
19806
+ for (const scope of waves) {
19807
+ for (const status of selected) {
19808
+ const entry = BUILTIN_HARNESSES.find((candidate) => candidate.id === status.id);
19809
+ if (!entry) continue;
19810
+ const supported = scopesFor(requested.scope === "all" ? "all" : scope, entry);
19811
+ if (!supported.includes(scope)) {
19812
+ if (requested.scope !== "all") {
19813
+ steps.push({
19814
+ harness: status.id,
19815
+ scope,
19816
+ command: status.command ?? entry.commands[0],
19817
+ args: [],
19818
+ outcome: "skipped",
19819
+ detail: "no_updater"
19820
+ });
19821
+ }
19822
+ continue;
19823
+ }
19824
+ if (!status.detected || !status.command) {
19825
+ steps.push({
19826
+ harness: status.id,
19827
+ scope,
19828
+ command: entry.commands[0],
19829
+ args: argsFor(entry, scope) ?? [],
19830
+ outcome: "skipped",
19831
+ detail: "not_detected"
19832
+ });
19833
+ continue;
19834
+ }
19812
19835
  steps.push({
19813
19836
  harness: status.id,
19814
- scope: scopes[0],
19815
- command: entry.commands[0],
19816
- args: argsFor(entry, scopes[0]) ?? [],
19817
- outcome: "skipped",
19818
- detail: "not_detected"
19837
+ scope,
19838
+ command: status.command,
19839
+ args: argsFor(entry, scope) ?? [],
19840
+ outcome: "would"
19819
19841
  });
19820
- continue;
19821
- }
19822
- for (const scope of scopes) {
19823
- const args = argsFor(entry, scope) ?? [];
19824
- steps.push({ harness: status.id, scope, command: status.command, args, outcome: "would" });
19825
19842
  }
19826
19843
  }
19827
19844
  return steps;
@@ -35526,7 +35543,7 @@ function compareSemver(left, right) {
35526
35543
  }
35527
35544
  function formatKxmUpdateNotice(notice) {
35528
35545
  if (!notice.available) return notice.message;
35529
- const apply = notice.auto ? "kxm update --kxm (auto)" : "kxm update --kxm";
35546
+ const apply = notice.auto ? "kxm update (auto)" : "kxm update";
35530
35547
  return `kxm ${notice.current} \u2192 ${notice.latest} available \xB7 ${apply}`;
35531
35548
  }
35532
35549
  function withAsset(notice, asset) {
@@ -35644,7 +35661,11 @@ function verifyReleaseAssetDigest(path4, sha256) {
35644
35661
  return false;
35645
35662
  }
35646
35663
  }
35647
- function planKxmPackageUpdate(source, latest, releaseDir, asset, downloader = "gh") {
35664
+ function planKxmPackageUpdate(source, latest, releaseDir, asset, downloader = "gh", localRoot) {
35665
+ if (source === "local") {
35666
+ if (!localRoot) return [];
35667
+ return [{ kind: "install", command: "npm", args: ["install", "--global", "--omit=peer", localRoot] }];
35668
+ }
35648
35669
  if (source === "npm") {
35649
35670
  return [{ kind: "install", command: "npm", args: ["install", "--global", "--omit=peer", `@kontextmind/kxm@${latest}`] }];
35650
35671
  }
@@ -51787,6 +51808,11 @@ init_hub_binding();
51787
51808
  init_bindings();
51788
51809
  import { existsSync as existsSync38, readFileSync as readFileSync34 } from "node:fs";
51789
51810
  import { dirname as dirname21, join as join48 } from "node:path";
51811
+ function kxmSourceCheckoutRoot(dir) {
51812
+ if (!existsSync38(join48(dir, ".git"))) return void 0;
51813
+ if (!packageIsKxm(dir)) return void 0;
51814
+ return dir;
51815
+ }
51790
51816
  function packageIsKxm(root) {
51791
51817
  try {
51792
51818
  const pkg = JSON.parse(readFileSync34(join48(root, "package.json"), "utf8"));
@@ -51872,7 +51898,7 @@ import { join as join49 } from "node:path";
51872
51898
  init_bindings();
51873
51899
  function loadKxmUpdateConfig(env = process.env) {
51874
51900
  const path4 = join49(kxmUserStateRoot({ env }), "update.yaml");
51875
- if (!existsSync39(path4)) return { schema: KXM_UPDATE_SCHEMA, auto: false, source: "github" };
51901
+ if (!existsSync39(path4)) return { schema: KXM_UPDATE_SCHEMA, auto: false, source: "npm" };
51876
51902
  let parsed;
51877
51903
  try {
51878
51904
  parsed = (0, import_yaml19.parse)(readFileSync35(path4, "utf8"));
@@ -51893,7 +51919,7 @@ function loadKxmUpdateConfig(env = process.env) {
51893
51919
  if (typeof row.auto !== "boolean") {
51894
51920
  throw new KxmUpdateConfigError("update.yaml auto must be a boolean");
51895
51921
  }
51896
- const source = row.source === void 0 ? "github" : row.source;
51922
+ const source = row.source === void 0 ? "npm" : row.source;
51897
51923
  if (source !== "npm" && source !== "github") {
51898
51924
  throw new KxmUpdateConfigError("update.yaml source must be npm or github");
51899
51925
  }
@@ -55090,7 +55116,7 @@ function applyKxmPackageUpdate(runtime, notice) {
55090
55116
  const releaseDir = mkdtempSync3(join57(tmpdir3(), "kxm-pkg-update-"));
55091
55117
  try {
55092
55118
  const downloader = notice.source === "github" && !ghReleaseDownloadAvailable(runtime.env) ? "curl" : "gh";
55093
- const planned = planKxmPackageUpdate(notice.source, notice.latest, releaseDir, notice.asset, downloader);
55119
+ const planned = planKxmPackageUpdate(notice.source, notice.latest, releaseDir, notice.asset, downloader, notice.localRoot);
55094
55120
  if (runtime.dryRun) {
55095
55121
  return { ok: true, detail: planned.map(formatPackageUpdateStep).join(" && ") };
55096
55122
  }
@@ -55216,22 +55242,34 @@ async function cmdSshClose(runtime, host) {
55216
55242
  return 0;
55217
55243
  }
55218
55244
  async function cmdUpdate(runtime, harness, options) {
55219
- if (options.check && (options.kxm || options.self || options.extensions || options.models || harness)) {
55245
+ if (options.check && (options.kxm || options.self || options.extensions || options.models || options.all || harness)) {
55220
55246
  print(runtime.io, runtime.json, { ok: false, command: "update", error: "scope_conflict" }, "--check cannot be combined with other update flags");
55221
55247
  return 2;
55222
55248
  }
55223
- const selected = [options.self, options.extensions, options.models].filter(Boolean).length;
55249
+ const selected = [options.self, options.extensions, options.models, options.all].filter(Boolean).length;
55224
55250
  if (selected > 1) {
55225
- print(runtime.io, runtime.json, { ok: false, command: "update", error: "scope_conflict" }, "specify at most one of --self, --extensions, or --models");
55251
+ print(runtime.io, runtime.json, { ok: false, command: "update", error: "scope_conflict" }, "specify at most one of --all, --self, --extensions, or --models");
55226
55252
  return 2;
55227
55253
  }
55228
55254
  warnIgnoredProjectUpdateYaml(runtime);
55229
55255
  const probe = installProbeFrom(runtime);
55230
55256
  const classified = classifyInstallRoot(probe);
55231
55257
  const current = installedKxmVersion(classified.root) ?? installedKxmVersion(findKxmRepoRoot(import.meta.url));
55258
+ const localRoot = kxmSourceCheckoutRoot(runtime.dirs.workdir) ?? kxmSourceCheckoutRoot(runtime.cwd);
55232
55259
  let notice;
55233
55260
  let kindReport = classified;
55234
- if (classified.kind === "source") {
55261
+ if (localRoot) {
55262
+ const localVersion = installedKxmVersion(localRoot) ?? current ?? "unknown";
55263
+ notice = {
55264
+ current: current ?? localVersion,
55265
+ latest: localVersion,
55266
+ available: true,
55267
+ auto: false,
55268
+ source: "local",
55269
+ message: `kxm ${current ?? localVersion} (local ${localRoot})`,
55270
+ localRoot
55271
+ };
55272
+ } else if (classified.kind === "source") {
55235
55273
  if (options.check) {
55236
55274
  const message = `kxm ${current ?? "unknown"} (running from source at ${classified.root})`;
55237
55275
  print(runtime.io, runtime.json, {
@@ -55240,7 +55278,7 @@ async function cmdUpdate(runtime, harness, options) {
55240
55278
  current: current ?? "unknown",
55241
55279
  available: false,
55242
55280
  auto: false,
55243
- source: "github",
55281
+ source: "npm",
55244
55282
  installKind: "source",
55245
55283
  root: classified.root,
55246
55284
  message
@@ -55262,7 +55300,7 @@ async function cmdUpdate(runtime, harness, options) {
55262
55300
  current: current ?? "unknown",
55263
55301
  available: false,
55264
55302
  auto: false,
55265
- source: "github",
55303
+ source: "npm",
55266
55304
  message: `kxm ${current ?? "unknown"} (running from source)`
55267
55305
  };
55268
55306
  } else {
@@ -55285,13 +55323,30 @@ async function cmdUpdate(runtime, harness, options) {
55285
55323
  }, notice.message);
55286
55324
  return 0;
55287
55325
  }
55288
- const applyKxm = Boolean(options.kxm || notice.auto);
55326
+ const fullUpdate = Boolean(options.all) || selected === 0 && !options.kxm && !harness;
55327
+ const applyKxm = Boolean(options.kxm || fullUpdate || notice.auto);
55289
55328
  let kxmApply;
55290
- if (applyKxm) {
55329
+ const applyKxmPackage = (requireSuccess) => {
55330
+ if (!applyKxm) return void 0;
55331
+ if (notice.source === "local" && notice.localRoot) {
55332
+ kxmApply = applyKxmPackageUpdate(runtime, notice);
55333
+ if (!kxmApply.ok && requireSuccess) {
55334
+ print(runtime.io, runtime.json, {
55335
+ ok: false,
55336
+ command: "update",
55337
+ error: kxmApply.error,
55338
+ kxm: kxmApply,
55339
+ notice,
55340
+ ...installKindPayload(classified)
55341
+ }, kxmApply.detail);
55342
+ return 1;
55343
+ }
55344
+ return void 0;
55345
+ }
55291
55346
  const resolved = resolveInstallKind(probe, npmGlobalRootFn(runtime));
55292
55347
  kindReport = resolved;
55293
55348
  if (resolved.kind !== "npm-global") {
55294
- if (options.kxm) {
55349
+ if (requireSuccess) {
55295
55350
  print(runtime.io, runtime.json, {
55296
55351
  ok: false,
55297
55352
  command: "update",
@@ -55305,9 +55360,11 @@ async function cmdUpdate(runtime, harness, options) {
55305
55360
  }
55306
55361
  if (notice.available) runtime.io.stderr(`kxm: ${resolved.instruction}
55307
55362
  `);
55308
- } else if (notice.available) {
55363
+ return void 0;
55364
+ }
55365
+ if (notice.available) {
55309
55366
  kxmApply = applyKxmPackageUpdate(runtime, notice);
55310
- if (!kxmApply.ok && options.kxm) {
55367
+ if (!kxmApply.ok && requireSuccess) {
55311
55368
  print(runtime.io, runtime.json, {
55312
55369
  ok: false,
55313
55370
  command: "update",
@@ -55319,9 +55376,12 @@ async function cmdUpdate(runtime, harness, options) {
55319
55376
  return 1;
55320
55377
  }
55321
55378
  }
55322
- }
55323
- const skipHarness = Boolean(options.kxm && selected === 0 && !harness && !notice.auto);
55379
+ return void 0;
55380
+ };
55381
+ const skipHarness = Boolean(options.kxm && !fullUpdate && selected === 0 && !harness && !notice.auto);
55324
55382
  if (skipHarness) {
55383
+ const aborted = applyKxmPackage(true);
55384
+ if (aborted !== void 0) return aborted;
55325
55385
  print(
55326
55386
  runtime.io,
55327
55387
  runtime.json,
@@ -55332,23 +55392,39 @@ async function cmdUpdate(runtime, harness, options) {
55332
55392
  }
55333
55393
  const scope = options.self ? "self" : options.extensions ? "extensions" : options.models ? "models" : "all";
55334
55394
  const inventory = await probeHarnessesAsync({ env: runtime.env });
55335
- const planned = planHarnessUpdate(inventory, { ...harness ? { harness } : {}, scope });
55336
- const steps = runHarnessUpdate(planned, { env: runtime.env, dryRun: runtime.dryRun });
55337
- const failed2 = steps.some((step) => step.outcome === "failed") || kxmApply?.ok === false;
55395
+ const planned = planHarnessUpdate(inventory, { ...harness ? { harness } : {}, scope: fullUpdate ? "all" : scope });
55396
+ const runOpts = { env: runtime.env, dryRun: runtime.dryRun };
55397
+ let steps;
55398
+ if (fullUpdate) {
55399
+ const selfSteps = runHarnessUpdate(planned.filter((step) => step.scope === "self"), runOpts);
55400
+ applyKxmPackage(false);
55401
+ const restSteps = runHarnessUpdate(planned.filter((step) => step.scope !== "self"), runOpts);
55402
+ steps = [...selfSteps, ...restSteps];
55403
+ } else {
55404
+ const aborted = applyKxmPackage(Boolean(options.kxm));
55405
+ if (aborted !== void 0) return aborted;
55406
+ steps = runHarnessUpdate(planned, runOpts);
55407
+ }
55408
+ const kxmFailed = kxmApply?.ok === false;
55338
55409
  const skippedUnknown = steps.some((step) => step.detail === "unknown_harness");
55410
+ const harnessFailed = steps.some((step) => step.outcome === "failed");
55339
55411
  const text = [notice.available ? notice.message : void 0, kxmApply?.detail, formatHarnessUpdate(steps)].filter(Boolean).join("\n");
55340
55412
  print(runtime.io, runtime.json, {
55341
- ok: !failed2 && !skippedUnknown,
55413
+ ok: !kxmFailed && !skippedUnknown,
55342
55414
  command: "update",
55343
55415
  dryRun: runtime.dryRun,
55344
- scope,
55416
+ scope: fullUpdate ? "all" : scope,
55417
+ defaultHarness: inventory.defaultHarness,
55345
55418
  notice,
55346
55419
  ...kxmApply ? { kxm: kxmApply } : {},
55347
55420
  steps,
55421
+ ...harnessFailed ? { degraded: true } : {},
55348
55422
  ...installKindPayload(kindReport)
55349
55423
  }, text);
55350
55424
  if (skippedUnknown) return 2;
55351
- return failed2 ? 1 : 0;
55425
+ if (kxmFailed) return 1;
55426
+ if (harnessFailed && !fullUpdate) return 1;
55427
+ return 0;
55352
55428
  }
55353
55429
  async function cmdValidate(runtime, fileFlag) {
55354
55430
  const worker = gateOf(runtime, "validate");
@@ -56420,7 +56496,7 @@ function createProgram(ctx, result, argv) {
56420
56496
  addGlobalOptions(authCmd.command("token").description("Inspect, issue, or clear local disk session tokens")).option("--status", "Check status of the active session token").option("--clear", "Clear persisted disk session token").option("--issue", "Force issuing a fresh session token").action(async function authTokenAction(options) {
56421
56497
  result.code = await cmdAuthToken(runtimeFrom(ctx, this), options);
56422
56498
  });
56423
- addGlobalOptions(program2.command("update").description("Update kxm, harness CLIs, extensions, plugins, and model catalogs").argument("[harness]", "Harness id (default: every detected harness)").option("--check", "Check for a kxm package update without applying").option("--kxm", "Apply the kxm operator package update (GitHub release tarball or npm)").option("--self", "Update only the harness CLI").option("--extensions", "Update only extensions/plugins (Pi packages, Claude kxm)").option("--models", "Refresh model catalogs where the harness supports it")).action(async function updateAction(harness, options) {
56499
+ addGlobalOptions(program2.command("update").description("Update kxm, harness CLIs, extensions, plugins, and model catalogs").argument("[harness]", "Harness id (default: every detected harness)").option("--check", "Check for a kxm package update without applying").option("--all", "Update harness CLIs first (--self), then kxm, extensions, and models (default)").option("--kxm", "Apply only the kxm operator package update").option("--self", "Update only harness CLIs").option("--extensions", "Update only extensions/plugins (Pi packages, Claude kxm)").option("--models", "Refresh model catalogs where the harness supports it")).action(async function updateAction(harness, options) {
56424
56500
  result.code = await cmdUpdate(runtimeFrom(ctx, this), harness, options);
56425
56501
  });
56426
56502
  const runtimeCmd = addGlobalOptions(program2.command("runtime").description("Manage the KXM Runtime supervisor"));
@@ -25438,7 +25438,7 @@ function sessionTokenFixHint(policy) {
25438
25438
  }
25439
25439
 
25440
25440
  // plugins/kxm/src/mcp-server.ts
25441
- var VERSION = "0.7.173";
25441
+ var VERSION = "0.7.175";
25442
25442
  var CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
25443
25443
  var inbox = /* @__PURE__ */ new Map();
25444
25444
  var notifiedInbox = /* @__PURE__ */ new Set();
@@ -17298,8 +17298,17 @@ function argsFor(entry, scope) {
17298
17298
  if (scope === "extensions") return entry.update.extensions;
17299
17299
  return entry.update.models;
17300
17300
  }
17301
+ function detectedHarnesses(inventory, requestedHarness) {
17302
+ if (requestedHarness) return inventory.harnesses.filter((entry) => entry.id === requestedHarness);
17303
+ const detected = inventory.harnesses.filter((entry) => entry.detected);
17304
+ const preferred = inventory.defaultHarness;
17305
+ return [
17306
+ ...detected.filter((entry) => entry.id === preferred),
17307
+ ...detected.filter((entry) => entry.id !== preferred)
17308
+ ];
17309
+ }
17301
17310
  function planHarnessUpdate(inventory, requested) {
17302
- const selected = requested.harness ? inventory.harnesses.filter((entry) => entry.id === requested.harness) : inventory.harnesses.filter((entry) => entry.detected);
17311
+ const selected = detectedHarnesses(inventory, requested.harness);
17303
17312
  if (requested.harness && selected.length === 0) {
17304
17313
  return [{
17305
17314
  harness: requested.harness,
@@ -17310,36 +17319,44 @@ function planHarnessUpdate(inventory, requested) {
17310
17319
  detail: isKnownHarnessId(requested.harness) ? "not_detected" : "unknown_harness"
17311
17320
  }];
17312
17321
  }
17322
+ const waves = requested.scope === "all" ? ["self", "extensions", "models"] : [requested.scope];
17313
17323
  const steps = [];
17314
- for (const status of selected) {
17315
- const entry = BUILTIN_HARNESSES.find((candidate) => candidate.id === status.id);
17316
- if (!entry) continue;
17317
- const scopes = scopesFor(requested.scope, entry);
17318
- if (scopes.length === 0) {
17319
- steps.push({
17320
- harness: status.id,
17321
- scope: requested.scope === "all" ? "self" : requested.scope,
17322
- command: status.command ?? entry.commands[0],
17323
- args: [],
17324
- outcome: "skipped",
17325
- detail: "no_updater"
17326
- });
17327
- continue;
17328
- }
17329
- if (!status.detected || !status.command) {
17324
+ for (const scope of waves) {
17325
+ for (const status of selected) {
17326
+ const entry = BUILTIN_HARNESSES.find((candidate) => candidate.id === status.id);
17327
+ if (!entry) continue;
17328
+ const supported = scopesFor(requested.scope === "all" ? "all" : scope, entry);
17329
+ if (!supported.includes(scope)) {
17330
+ if (requested.scope !== "all") {
17331
+ steps.push({
17332
+ harness: status.id,
17333
+ scope,
17334
+ command: status.command ?? entry.commands[0],
17335
+ args: [],
17336
+ outcome: "skipped",
17337
+ detail: "no_updater"
17338
+ });
17339
+ }
17340
+ continue;
17341
+ }
17342
+ if (!status.detected || !status.command) {
17343
+ steps.push({
17344
+ harness: status.id,
17345
+ scope,
17346
+ command: entry.commands[0],
17347
+ args: argsFor(entry, scope) ?? [],
17348
+ outcome: "skipped",
17349
+ detail: "not_detected"
17350
+ });
17351
+ continue;
17352
+ }
17330
17353
  steps.push({
17331
17354
  harness: status.id,
17332
- scope: scopes[0],
17333
- command: entry.commands[0],
17334
- args: argsFor(entry, scopes[0]) ?? [],
17335
- outcome: "skipped",
17336
- detail: "not_detected"
17355
+ scope,
17356
+ command: status.command,
17357
+ args: argsFor(entry, scope) ?? [],
17358
+ outcome: "would"
17337
17359
  });
17338
- continue;
17339
- }
17340
- for (const scope of scopes) {
17341
- const args = argsFor(entry, scope) ?? [];
17342
- steps.push({ harness: status.id, scope, command: status.command, args, outcome: "would" });
17343
17360
  }
17344
17361
  }
17345
17362
  return steps;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin",
3
- "version": "0.7.173",
3
+ "version": "0.7.175",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -34,15 +34,20 @@ still blocks the tools.
34
34
  | Command | Purpose |
35
35
  |---|---|
36
36
  | `kxm update --check` | Check for a kxm package update without applying it |
37
- | `kxm update --kxm` | Apply the kxm package update |
37
+ | `kxm update` / `--all` | Harness `--self` first, then kxm, extensions, and models |
38
+ | `kxm update --kxm` | Apply only the kxm package update |
38
39
  | `kxm update [harness] --self` | Update only the harness CLI |
39
40
  | `kxm update [harness] --extensions` | Update only extensions and plugins (Pi packages, the Claude kxm plugin) |
40
41
  | `kxm update [harness] --models` | Refresh model catalogs where the harness supports it |
41
42
 
42
- - `kxm update --kxm` downloads the GitHub release tarball by default. It uses
43
- `gh` when that CLI answers `gh --version`, and otherwise curl with the same
43
+ - Bare `kxm update` (and `--all`) updates detected harness CLIs first, then kxm,
44
+ then extensions and models. `--kxm` is kxm-only. kxm installs from npm by default;
45
+ it needs an authenticated `gh` only when `update.yaml` sets `source: github`.
46
+ It uses `gh` when that CLI answers `gh --version`, and otherwise curl with the same
44
47
  sha256 check. A host with neither can follow the curl commands in
45
48
  docs/operations/upgrade.md.
49
+ Inside the kxm repository it installs the local files. Detected harness updater
50
+ failures on `--all` are degraded, not fatal.
46
51
  - `kxm update claude --extensions` updates only a user-scope plugin install;
47
52
  its dry run prints `claude extensions: would claude plugin update kxm -y`.
48
53
  A project-scope install is the operator's `claude plugin update kxm@kxm --scope project`.
@@ -68,6 +68,7 @@ import {
68
68
  } from "../init-guide-setup.ts";
69
69
  import {
70
70
  classifyInstallRoot,
71
+ kxmSourceCheckoutRoot,
71
72
  resolveInstallKind,
72
73
  type InstallKindReport,
73
74
  type InstallProbe,
@@ -161,7 +162,7 @@ export function applyKxmPackageUpdate(runtime: Runtime, notice: KxmUpdateNotice)
161
162
  const releaseDir = mkdtempSync(join(tmpdir(), "kxm-pkg-update-"));
162
163
  try {
163
164
  const downloader = notice.source === "github" && !ghReleaseDownloadAvailable(runtime.env) ? "curl" : "gh";
164
- const planned = planKxmPackageUpdate(notice.source, notice.latest, releaseDir, notice.asset, downloader);
165
+ const planned = planKxmPackageUpdate(notice.source, notice.latest, releaseDir, notice.asset, downloader, notice.localRoot);
165
166
  if (runtime.dryRun) {
166
167
  return { ok: true, detail: planned.map(formatPackageUpdateStep).join(" && ") };
167
168
  }
@@ -309,14 +310,15 @@ export async function cmdUpdate(runtime: Runtime, harness: string | undefined, o
309
310
  models?: boolean | undefined;
310
311
  check?: boolean | undefined;
311
312
  kxm?: boolean | undefined;
313
+ all?: boolean | undefined;
312
314
  }): Promise<number> {
313
- if (options.check && (options.kxm || options.self || options.extensions || options.models || harness)) {
315
+ if (options.check && (options.kxm || options.self || options.extensions || options.models || options.all || harness)) {
314
316
  print(runtime.io, runtime.json, { ok: false, command: "update", error: "scope_conflict" }, "--check cannot be combined with other update flags");
315
317
  return 2;
316
318
  }
317
- const selected = [options.self, options.extensions, options.models].filter(Boolean).length;
319
+ const selected = [options.self, options.extensions, options.models, options.all].filter(Boolean).length;
318
320
  if (selected > 1) {
319
- print(runtime.io, runtime.json, { ok: false, command: "update", error: "scope_conflict" }, "specify at most one of --self, --extensions, or --models");
321
+ print(runtime.io, runtime.json, { ok: false, command: "update", error: "scope_conflict" }, "specify at most one of --all, --self, --extensions, or --models");
320
322
  return 2;
321
323
  }
322
324
  warnIgnoredProjectUpdateYaml(runtime);
@@ -328,10 +330,24 @@ export async function cmdUpdate(runtime: Runtime, harness: string | undefined, o
328
330
  // `cd ~ && kxm update --kxm` into an uncaught ENOENT on `$HOME/package.json`,
329
331
  // and would happily compare a stranger project's version wherever one did
330
332
  // exist. Unreadable is now a stated refusal.
333
+ // Exception: a cwd that *is* this repository (`@kontextmind/kxm` plus `.git`)
334
+ // is a local-files apply, not a stranger version compare.
331
335
  const current = installedKxmVersion(classified.root) ?? installedKxmVersion(findKxmRepoRoot(import.meta.url));
336
+ const localRoot = kxmSourceCheckoutRoot(runtime.dirs.workdir) ?? kxmSourceCheckoutRoot(runtime.cwd);
332
337
  let notice: KxmUpdateNotice;
333
338
  let kindReport = classified;
334
- if (classified.kind === "source") {
339
+ if (localRoot) {
340
+ const localVersion = installedKxmVersion(localRoot) ?? current ?? "unknown";
341
+ notice = {
342
+ current: current ?? localVersion,
343
+ latest: localVersion,
344
+ available: true,
345
+ auto: false,
346
+ source: "local",
347
+ message: `kxm ${current ?? localVersion} (local ${localRoot})`,
348
+ localRoot,
349
+ };
350
+ } else if (classified.kind === "source") {
335
351
  if (options.check) {
336
352
  const message = `kxm ${current ?? "unknown"} (running from source at ${classified.root})`;
337
353
  print(runtime.io, runtime.json, {
@@ -340,7 +356,7 @@ export async function cmdUpdate(runtime: Runtime, harness: string | undefined, o
340
356
  current: current ?? "unknown",
341
357
  available: false,
342
358
  auto: false,
343
- source: "github",
359
+ source: "npm",
344
360
  installKind: "source",
345
361
  root: classified.root,
346
362
  message,
@@ -362,7 +378,7 @@ export async function cmdUpdate(runtime: Runtime, harness: string | undefined, o
362
378
  current: current ?? "unknown",
363
379
  available: false,
364
380
  auto: false,
365
- source: "github",
381
+ source: "npm",
366
382
  message: `kxm ${current ?? "unknown"} (running from source)`,
367
383
  };
368
384
  } else {
@@ -385,13 +401,30 @@ export async function cmdUpdate(runtime: Runtime, harness: string | undefined, o
385
401
  }, notice.message);
386
402
  return 0;
387
403
  }
388
- const applyKxm = Boolean(options.kxm || notice.auto);
404
+ const fullUpdate = Boolean(options.all) || (selected === 0 && !options.kxm && !harness);
405
+ const applyKxm = Boolean(options.kxm || fullUpdate || notice.auto);
389
406
  let kxmApply: { ok: boolean; detail: string; error?: string } | undefined;
390
- if (applyKxm) {
407
+ const applyKxmPackage = (requireSuccess: boolean): number | undefined => {
408
+ if (!applyKxm) return undefined;
409
+ if (notice.source === "local" && notice.localRoot) {
410
+ kxmApply = applyKxmPackageUpdate(runtime, notice);
411
+ if (!kxmApply.ok && requireSuccess) {
412
+ print(runtime.io, runtime.json, {
413
+ ok: false,
414
+ command: "update",
415
+ error: kxmApply.error,
416
+ kxm: kxmApply,
417
+ notice,
418
+ ...installKindPayload(classified),
419
+ }, kxmApply.detail);
420
+ return 1;
421
+ }
422
+ return undefined;
423
+ }
391
424
  const resolved = resolveInstallKind(probe, npmGlobalRootFn(runtime));
392
425
  kindReport = resolved;
393
426
  if (resolved.kind !== "npm-global") {
394
- if (options.kxm) {
427
+ if (requireSuccess) {
395
428
  print(runtime.io, runtime.json, {
396
429
  ok: false,
397
430
  command: "update",
@@ -404,9 +437,11 @@ export async function cmdUpdate(runtime: Runtime, harness: string | undefined, o
404
437
  return 2;
405
438
  }
406
439
  if (notice.available) runtime.io.stderr(`kxm: ${resolved.instruction}\n`);
407
- } else if (notice.available) {
440
+ return undefined;
441
+ }
442
+ if (notice.available) {
408
443
  kxmApply = applyKxmPackageUpdate(runtime, notice);
409
- if (!kxmApply.ok && options.kxm) {
444
+ if (!kxmApply.ok && requireSuccess) {
410
445
  print(runtime.io, runtime.json, {
411
446
  ok: false,
412
447
  command: "update",
@@ -418,9 +453,12 @@ export async function cmdUpdate(runtime: Runtime, harness: string | undefined, o
418
453
  return 1;
419
454
  }
420
455
  }
421
- }
422
- const skipHarness = Boolean(options.kxm && selected === 0 && !harness && !notice.auto);
456
+ return undefined;
457
+ };
458
+ const skipHarness = Boolean(options.kxm && !fullUpdate && selected === 0 && !harness && !notice.auto);
423
459
  if (skipHarness) {
460
+ const aborted = applyKxmPackage(true);
461
+ if (aborted !== undefined) return aborted;
424
462
  print(
425
463
  runtime.io,
426
464
  runtime.json,
@@ -431,23 +469,39 @@ export async function cmdUpdate(runtime: Runtime, harness: string | undefined, o
431
469
  }
432
470
  const scope: HarnessUpdateScope = options.self ? "self" : options.extensions ? "extensions" : options.models ? "models" : "all";
433
471
  const inventory = await probeHarnessesAsync({ env: runtime.env });
434
- const planned = planHarnessUpdate(inventory, { ...(harness ? { harness } : {}), scope });
435
- const steps = runHarnessUpdate(planned, { env: runtime.env, dryRun: runtime.dryRun });
436
- const failed = steps.some((step) => step.outcome === "failed") || kxmApply?.ok === false;
472
+ const planned = planHarnessUpdate(inventory, { ...(harness ? { harness } : {}), scope: fullUpdate ? "all" : scope });
473
+ const runOpts = { env: runtime.env, dryRun: runtime.dryRun };
474
+ let steps;
475
+ if (fullUpdate) {
476
+ const selfSteps = runHarnessUpdate(planned.filter((step) => step.scope === "self"), runOpts);
477
+ applyKxmPackage(false);
478
+ const restSteps = runHarnessUpdate(planned.filter((step) => step.scope !== "self"), runOpts);
479
+ steps = [...selfSteps, ...restSteps];
480
+ } else {
481
+ const aborted = applyKxmPackage(Boolean(options.kxm));
482
+ if (aborted !== undefined) return aborted;
483
+ steps = runHarnessUpdate(planned, runOpts);
484
+ }
485
+ const kxmFailed = kxmApply?.ok === false;
437
486
  const skippedUnknown = steps.some((step) => step.detail === "unknown_harness");
487
+ const harnessFailed = steps.some((step) => step.outcome === "failed");
438
488
  const text = [notice.available ? notice.message : undefined, kxmApply?.detail, formatHarnessUpdate(steps)].filter(Boolean).join("\n");
439
489
  print(runtime.io, runtime.json, {
440
- ok: !failed && !skippedUnknown,
490
+ ok: !kxmFailed && !skippedUnknown,
441
491
  command: "update",
442
492
  dryRun: runtime.dryRun,
443
- scope,
493
+ scope: fullUpdate ? "all" : scope,
494
+ defaultHarness: inventory.defaultHarness,
444
495
  notice,
445
496
  ...(kxmApply ? { kxm: kxmApply } : {}),
446
497
  steps,
498
+ ...(harnessFailed ? { degraded: true } : {}),
447
499
  ...installKindPayload(kindReport),
448
500
  }, text);
449
501
  if (skippedUnknown) return 2;
450
- return failed ? 1 : 0;
502
+ if (kxmFailed) return 1;
503
+ if (harnessFailed && !fullUpdate) return 1;
504
+ return 0;
451
505
  }
452
506
 
453
507
  export async function cmdValidate(runtime: Runtime, fileFlag?: string | undefined): Promise<number> {
@@ -788,8 +788,9 @@ function createProgram(ctx: CliContext, result: { code: number }, argv: readonly
788
788
  addGlobalOptions(program.command("update").description("Update kxm, harness CLIs, extensions, plugins, and model catalogs")
789
789
  .argument("[harness]", "Harness id (default: every detected harness)")
790
790
  .option("--check", "Check for a kxm package update without applying")
791
- .option("--kxm", "Apply the kxm operator package update (GitHub release tarball or npm)")
792
- .option("--self", "Update only the harness CLI")
791
+ .option("--all", "Update harness CLIs first (--self), then kxm, extensions, and models (default)")
792
+ .option("--kxm", "Apply only the kxm operator package update")
793
+ .option("--self", "Update only harness CLIs")
793
794
  .option("--extensions", "Update only extensions/plugins (Pi packages, Claude kxm)")
794
795
  .option("--models", "Refresh model catalogs where the harness supports it"))
795
796
  .action(async function updateAction(this: Command, harness: string | undefined, options: {
@@ -798,6 +799,7 @@ function createProgram(ctx: CliContext, result: { code: number }, argv: readonly
798
799
  models?: boolean;
799
800
  check?: boolean;
800
801
  kxm?: boolean;
802
+ all?: boolean;
801
803
  }) {
802
804
  result.code = await cmdUpdate(runtimeFrom(ctx, this), harness, options);
803
805
  });
@@ -1449,13 +1449,24 @@ function argsFor(entry: HarnessCatalogEntry, scope: Exclude<HarnessUpdateScope,
1449
1449
  return entry.update.models;
1450
1450
  }
1451
1451
 
1452
+ function detectedHarnesses(
1453
+ inventory: HarnessInventory,
1454
+ requestedHarness?: string,
1455
+ ): HarnessStatus[] {
1456
+ if (requestedHarness) return inventory.harnesses.filter((entry) => entry.id === requestedHarness);
1457
+ const detected = inventory.harnesses.filter((entry) => entry.detected);
1458
+ const preferred = inventory.defaultHarness;
1459
+ return [
1460
+ ...detected.filter((entry) => entry.id === preferred),
1461
+ ...detected.filter((entry) => entry.id !== preferred),
1462
+ ];
1463
+ }
1464
+
1452
1465
  export function planHarnessUpdate(
1453
1466
  inventory: HarnessInventory,
1454
1467
  requested: { harness?: string; scope: HarnessUpdateScope },
1455
1468
  ): HarnessUpdateStep[] {
1456
- const selected = requested.harness
1457
- ? inventory.harnesses.filter((entry) => entry.id === requested.harness)
1458
- : inventory.harnesses.filter((entry) => entry.detected);
1469
+ const selected = detectedHarnesses(inventory, requested.harness);
1459
1470
  if (requested.harness && selected.length === 0) {
1460
1471
  return [{
1461
1472
  harness: requested.harness,
@@ -1466,36 +1477,46 @@ export function planHarnessUpdate(
1466
1477
  detail: isKnownHarnessId(requested.harness) ? "not_detected" : "unknown_harness",
1467
1478
  }];
1468
1479
  }
1480
+ const waves: Exclude<HarnessUpdateScope, "all">[] = requested.scope === "all"
1481
+ ? ["self", "extensions", "models"]
1482
+ : [requested.scope];
1469
1483
  const steps: HarnessUpdateStep[] = [];
1470
- for (const status of selected) {
1471
- const entry = BUILTIN_HARNESSES.find((candidate) => candidate.id === status.id);
1472
- if (!entry) continue;
1473
- const scopes = scopesFor(requested.scope, entry);
1474
- if (scopes.length === 0) {
1475
- steps.push({
1476
- harness: status.id,
1477
- scope: requested.scope === "all" ? "self" : requested.scope,
1478
- command: status.command ?? entry.commands[0]!,
1479
- args: [],
1480
- outcome: "skipped",
1481
- detail: "no_updater",
1482
- });
1483
- continue;
1484
- }
1485
- if (!status.detected || !status.command) {
1484
+ for (const scope of waves) {
1485
+ for (const status of selected) {
1486
+ const entry = BUILTIN_HARNESSES.find((candidate) => candidate.id === status.id);
1487
+ if (!entry) continue;
1488
+ const supported = scopesFor(requested.scope === "all" ? "all" : scope, entry);
1489
+ if (!supported.includes(scope)) {
1490
+ if (requested.scope !== "all") {
1491
+ steps.push({
1492
+ harness: status.id,
1493
+ scope,
1494
+ command: status.command ?? entry.commands[0]!,
1495
+ args: [],
1496
+ outcome: "skipped",
1497
+ detail: "no_updater",
1498
+ });
1499
+ }
1500
+ continue;
1501
+ }
1502
+ if (!status.detected || !status.command) {
1503
+ steps.push({
1504
+ harness: status.id,
1505
+ scope,
1506
+ command: entry.commands[0]!,
1507
+ args: argsFor(entry, scope) ?? [],
1508
+ outcome: "skipped",
1509
+ detail: "not_detected",
1510
+ });
1511
+ continue;
1512
+ }
1486
1513
  steps.push({
1487
1514
  harness: status.id,
1488
- scope: scopes[0]!,
1489
- command: entry.commands[0]!,
1490
- args: argsFor(entry, scopes[0]!) ?? [],
1491
- outcome: "skipped",
1492
- detail: "not_detected",
1515
+ scope,
1516
+ command: status.command,
1517
+ args: argsFor(entry, scope) ?? [],
1518
+ outcome: "would",
1493
1519
  });
1494
- continue;
1495
- }
1496
- for (const scope of scopes) {
1497
- const args = argsFor(entry, scope) ?? [];
1498
- steps.push({ harness: status.id, scope, command: status.command, args, outcome: "would" });
1499
1520
  }
1500
1521
  }
1501
1522
  return steps;
@@ -19,6 +19,12 @@ export interface InstallKindReport {
19
19
  instruction: string;
20
20
  }
21
21
 
22
+ export function kxmSourceCheckoutRoot(dir: string): string | undefined {
23
+ if (!existsSync(join(dir, ".git"))) return undefined;
24
+ if (!packageIsKxm(dir)) return undefined;
25
+ return dir;
26
+ }
27
+
22
28
  function packageIsKxm(root: string): boolean {
23
29
  try {
24
30
  const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { name?: unknown };
@@ -10,7 +10,7 @@ import { kxmUserStateRoot } from "./bindings.ts";
10
10
 
11
11
  export function loadKxmUpdateConfig(env: NodeJS.ProcessEnv = process.env): KxmUpdateConfig {
12
12
  const path = join(kxmUserStateRoot({ env }), "update.yaml");
13
- if (!existsSync(path)) return { schema: KXM_UPDATE_SCHEMA, auto: false, source: "github" };
13
+ if (!existsSync(path)) return { schema: KXM_UPDATE_SCHEMA, auto: false, source: "npm" };
14
14
  let parsed: unknown;
15
15
  try {
16
16
  parsed = parseYaml(readFileSync(path, "utf8"));
@@ -31,7 +31,7 @@ export function loadKxmUpdateConfig(env: NodeJS.ProcessEnv = process.env): KxmUp
31
31
  if (typeof row.auto !== "boolean") {
32
32
  throw new KxmUpdateConfigError("update.yaml auto must be a boolean");
33
33
  }
34
- const source = row.source === undefined ? "github" : row.source;
34
+ const source = row.source === undefined ? "npm" : row.source;
35
35
  if (source !== "npm" && source !== "github") {
36
36
  throw new KxmUpdateConfigError("update.yaml source must be npm or github");
37
37
  }
@@ -10,6 +10,7 @@ const CACHE_TTL_MS = 6 * 60 * 60 * 1000;
10
10
  const SHA256_DIGEST = /^sha256:([0-9a-fA-F]{64})$/;
11
11
 
12
12
  export type KxmUpdateSource = "npm" | "github";
13
+ export type KxmApplySource = KxmUpdateSource | "local";
13
14
 
14
15
  export interface KxmReleaseAsset {
15
16
  name: string;
@@ -27,9 +28,10 @@ export interface KxmUpdateNotice {
27
28
  latest?: string;
28
29
  available: boolean;
29
30
  auto: boolean;
30
- source: KxmUpdateSource;
31
+ source: KxmApplySource;
31
32
  message: string;
32
33
  asset?: KxmReleaseAsset;
34
+ localRoot?: string;
33
35
  }
34
36
 
35
37
  export class KxmUpdateConfigError extends Error {
@@ -100,7 +102,7 @@ export function compareSemver(left: string, right: string): number | undefined {
100
102
 
101
103
  export function formatKxmUpdateNotice(notice: KxmUpdateNotice): string {
102
104
  if (!notice.available) return notice.message;
103
- const apply = notice.auto ? "kxm update --kxm (auto)" : "kxm update --kxm";
105
+ const apply = notice.auto ? "kxm update (auto)" : "kxm update";
104
106
  return `kxm ${notice.current} → ${notice.latest} available · ${apply}`;
105
107
  }
106
108
 
@@ -243,14 +245,19 @@ export function verifyReleaseAssetDigest(path: string, sha256: string): boolean
243
245
  }
244
246
  }
245
247
 
246
- /** npm registry apply is for after the public package exists. npm verifies registry integrity itself; do not duplicate that check here. Git installs use GitHub release tarballs. */
248
+ /** npm is the default source and npm verifies registry integrity itself. "local" installs this repository's checkout. "github" downloads the release tarball (gh, or curl without gh) and verifies its sha256. */
247
249
  export function planKxmPackageUpdate(
248
- source: KxmUpdateSource,
250
+ source: KxmApplySource,
249
251
  latest: string,
250
252
  releaseDir: string,
251
253
  asset?: KxmReleaseAsset,
252
254
  downloader: "gh" | "curl" = "gh",
255
+ localRoot?: string,
253
256
  ): KxmPackageUpdateStep[] {
257
+ if (source === "local") {
258
+ if (!localRoot) return [];
259
+ return [{ kind: "install", command: "npm", args: ["install", "--global", "--omit=peer", localRoot] }];
260
+ }
254
261
  if (source === "npm") {
255
262
  return [{ kind: "install", command: "npm", args: ["install", "--global", "--omit=peer", `@kontextmind/kxm@${latest}`] }];
256
263
  }
@@ -11,7 +11,7 @@ import { deliverInboxNotification } from "./inbox.ts";
11
11
  import type { HubEvent, MessageRecord } from "./protocol.ts";
12
12
  import { sessionTokenFixHint } from "./session-token-hint.ts";
13
13
 
14
- const VERSION = "0.7.173";
14
+ const VERSION = "0.7.175";
15
15
  const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
16
16
  const inbox = new Map<string, MessageRecord>();
17
17
  const notifiedInbox = new Set<string>();
@@ -14,6 +14,7 @@ import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "nod
14
14
  import { homedir, tmpdir } from "node:os";
15
15
  import { join, resolve } from "node:path";
16
16
 
17
+ import { npmPackArtifacts } from "./npm-pack-json.mjs";
17
18
  const repoRoot = resolve(import.meta.dirname, "..");
18
19
 
19
20
  const vault = process.env.KXM_SMOKE_VAULT ?? "AI Provider Keys";
@@ -92,7 +93,7 @@ function main() {
92
93
  // commit gate (`npm run verify` -> check:generated) enforces that.
93
94
  process.stdout.write("==> npm pack\n");
94
95
  const packed = run("npm", ["pack", "--pack-destination", scratch, "--json"], { cwd: repoRoot });
95
- const filename = JSON.parse(packed.stdout)[0]?.filename;
96
+ const filename = npmPackArtifacts(packed.stdout)[0]?.filename;
96
97
  if (!filename) die("npm pack produced no tarball");
97
98
  const tarball = join(scratch, filename);
98
99
 
@@ -0,0 +1,4 @@
1
+ export function npmPackArtifacts(stdout: string): Array<{
2
+ filename?: string;
3
+ files?: Array<{ path: string }>;
4
+ }>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `npm pack --json` is a one-element array through npm 11. npm 12 returns
3
+ * one object keyed by package name. Both shapes carry `filename` and `files`.
4
+ */
5
+ export function npmPackArtifacts(stdout) {
6
+ const parsed = JSON.parse(stdout);
7
+ if (Array.isArray(parsed)) return parsed;
8
+ if (parsed && typeof parsed === "object") {
9
+ const values = Object.values(parsed);
10
+ if (values.length > 0 && values.every((value) => value && typeof value === "object" && !Array.isArray(value))) {
11
+ return values;
12
+ }
13
+ }
14
+ throw new Error(`unexpected npm pack JSON: ${String(stdout).slice(0, 200)}`);
15
+ }
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { spawnSync } from "node:child_process";
10
10
  import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
11
- import { join, resolve } from "node:path";
11
+ import { delimiter, join, resolve } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
13
 
14
14
  const STAGES = Object.freeze([
@@ -67,13 +67,55 @@ function childEnv() {
67
67
  return env;
68
68
  }
69
69
 
70
+ function pathValue(env) {
71
+ const key = Object.keys(env).find((name) => name.toUpperCase() === "PATH");
72
+ return key ? env[key] : "";
73
+ }
74
+
75
+ function comSpec(env) {
76
+ const key = Object.keys(env).find((name) => name.toUpperCase() === "COMSPEC");
77
+ return (key && env[key].trim()) || "cmd.exe";
78
+ }
79
+
80
+ /** CreateProcess does not run `npm.cmd`. Prefer a real `.exe`; otherwise launch the `.cmd`/`.bat` through cmd. */
81
+ function windowsScriptOnPath(command, env) {
82
+ if (process.platform !== "win32") return undefined;
83
+ if (/\.(?:exe|com)$/i.test(command) || /[\\/]/.test(command)) return undefined;
84
+ if (/\.(?:cmd|bat)$/i.test(command)) return command;
85
+ for (const dir of pathValue(env).split(delimiter)) {
86
+ if (!dir) continue;
87
+ if (existsSync(join(dir, `${command}.exe`))) return undefined;
88
+ const cmd = join(dir, `${command}.cmd`);
89
+ if (existsSync(cmd)) return cmd;
90
+ const bat = join(dir, `${command}.bat`);
91
+ if (existsSync(bat)) return bat;
92
+ }
93
+ return undefined;
94
+ }
95
+
96
+ function quoteCmdArg(value) {
97
+ const text = String(value);
98
+ if (/[\0\r\n"%!]/.test(text)) {
99
+ throw new Error(`cannot pass ${JSON.stringify(text)} to a Windows command script`);
100
+ }
101
+ return `"${text}"`;
102
+ }
103
+
70
104
  function run(command, args, timeout = 120_000) {
71
- const result = spawnSync(command, args, {
105
+ const env = childEnv();
106
+ const script = windowsScriptOnPath(command, env);
107
+ const windowsCommandScript = Boolean(script);
108
+ const launchCommand = windowsCommandScript ? comSpec(env) : command;
109
+ const launchArgs = windowsCommandScript
110
+ ? ["/d", "/s", "/v:off", "/c", `"${[script, ...args].map(quoteCmdArg).join(" ")}"`]
111
+ : args;
112
+ const result = spawnSync(launchCommand, launchArgs, {
72
113
  cwd: root,
73
114
  encoding: "utf8",
74
- env: childEnv(),
115
+ env,
75
116
  timeout,
76
117
  windowsHide: true,
118
+ windowsVerbatimArguments: windowsCommandScript,
77
119
  });
78
120
  return {
79
121
  status: result.status ?? 1,