@kontextmind/kxm 0.7.163 → 0.7.165
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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +22 -0
- package/README.md +2 -0
- package/docs/README.md +1 -0
- package/docs/architecture/inventory.md +4 -3
- package/docs/contributing/harness-routing-internals.md +28 -27
- package/docs/glossary.md +3 -3
- package/docs/operations/troubleshooting.md +3 -0
- package/docs/operations.md +154 -0
- package/docs/reference/cli-reference.md +4 -3
- package/docs/reference/configuration.md +11 -1
- package/docs/reference/harness-routing.md +17 -15
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +2 -2
- package/plugins/kxm/README.md +2 -2
- package/plugins/kxm/dist/claude-hook.js +120 -16
- package/plugins/kxm/dist/cli.js +14929 -14542
- package/plugins/kxm/dist/extension.js +299 -122
- package/plugins/kxm/dist/mcp-server.js +7690 -106
- package/plugins/kxm/dist/runtime-supervisor.js +151 -4
- package/plugins/kxm/dist/runtime.js +158 -11
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +5 -1
- package/plugins/kxm/src/cli/context-skills.ts +64 -10
- package/plugins/kxm/src/cli/hub.ts +128 -17
- package/plugins/kxm/src/cli/project.ts +7 -3
- package/plugins/kxm/src/cli/types.ts +6 -0
- package/plugins/kxm/src/cli/workflows.ts +12 -1
- package/plugins/kxm/src/cli.ts +23 -5
- package/plugins/kxm/src/extension.ts +7 -3
- package/plugins/kxm/src/hub-autostart.ts +3 -0
- package/plugins/kxm/src/hub-binding.ts +182 -2
- package/plugins/kxm/src/hub-env.ts +71 -0
- package/plugins/kxm/src/mcp-server.ts +3 -2
- package/plugins/kxm/src/project-name.ts +34 -7
- package/plugins/kxm/src/runtime-supervisor.ts +9 -3
- package/plugins/kxm/src/tenant-status.ts +9 -2
- package/scripts/build-runtime.mjs +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,15 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
6
6
|
|
|
7
7
|
### Added
|
|
8
8
|
|
|
9
|
+
- **`kxm hub bind --cloud` attaches a client to a remote hub reached through a loopback forward.**
|
|
10
|
+
The binding records `--token-env` and/or `--token-command` and never the token.
|
|
11
|
+
Later commands, including the environment for a Pi worker, use that source and
|
|
12
|
+
do not fall back to this machine's hub-env token. Peer commands and
|
|
13
|
+
`kxm tenant status` resolve one project id: flag or `KXM_PROJECT`, then
|
|
14
|
+
`.kxm/project.yaml`, then `package.json`, then the directory name. The
|
|
15
|
+
SSH-forward recipe is in
|
|
16
|
+
[Operate a KXM hub](docs/operations.md#cross-box-peer-attach).
|
|
17
|
+
|
|
9
18
|
- **A live agent step uses a configurable one-shot timeout, and a cancelling run recovers when its child has already exited.**
|
|
10
19
|
The bound is the step `timeoutMs`, or the project `limits.agentStepTimeoutMs`
|
|
11
20
|
when the step omits it (minimum 60 seconds, default one hour). A wider step
|
|
@@ -156,6 +165,19 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
156
165
|
|
|
157
166
|
### Changed
|
|
158
167
|
|
|
168
|
+
- **The checked-in price catalog and model inventory match the live OpenRouter and Nous feeds.**
|
|
169
|
+
`.kxm/prices.yaml` is dated 2026-09-28. It adds `xai/grok-4.7` (alias
|
|
170
|
+
`x-ai/grok-4.7`) at $1.60 input and $4.80 output per 1M tokens, with cache
|
|
171
|
+
read $0.40. `openrouter/z-ai/glm-5.3-flash` moves from $0.09 / $0.30 to
|
|
172
|
+
$0.15 / $0.50 (cache read $0.03); the anchored `zai-coding-cn/glm-5.3-flash`
|
|
173
|
+
row moves with it. `qwen-token-plan/deepseek-v4.1-flash` (alias
|
|
174
|
+
`deepseek/deepseek-v4.1-flash`) moves from $0.15 / $0.60 to $0.014 / $0.40
|
|
175
|
+
(cache read $0.004) because the routing docs still name that selector.
|
|
176
|
+
`xai/grok-4.6` stays: its base rate still matches the live catalog, and code
|
|
177
|
+
and tests still name it. `.kxm/models/inventory.yaml` was refreshed with
|
|
178
|
+
`kxm models inventory-refresh`. Pi, Grok, and agy were not installed for that
|
|
179
|
+
fetch, so models that exist only in those CLIs remain the 2026-09-16 rows.
|
|
180
|
+
|
|
159
181
|
- **Windows pull-request unit lanes finish inside the job limit.** The light
|
|
160
182
|
lane on `windows-latest` was cancelled at 20 minutes on #361, #362, and
|
|
161
183
|
#363 after it stopped reporting tests. `package-install.test.ts` (171s on
|
package/README.md
CHANGED
|
@@ -105,6 +105,8 @@ You need Node.js 22.19 or newer on the 22.x line, or Node.js 24 or newer, plus G
|
|
|
105
105
|
kxm hub view
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
+
A hub on another machine, reached through an SSH forward to loopback, uses `kxm hub bind --cloud` so this machine's hub-env token is not sent. The recipe is in [Operate a KXM hub](docs/operations.md#cross-box-peer-attach).
|
|
109
|
+
|
|
108
110
|
Expected output:
|
|
109
111
|
|
|
110
112
|
```text
|
package/docs/README.md
CHANGED
|
@@ -96,6 +96,7 @@ KXM connects coding agents through a durable, authenticated [hub](glossary.md#hu
|
|
|
96
96
|
|
|
97
97
|
| Page | For | What you get |
|
|
98
98
|
|---|---|---|
|
|
99
|
+
| [Operate a KXM hub](operations.md) | Hub operators | Index of the operations guides, and cross-box attach over an SSH forward |
|
|
99
100
|
| [Deploy KXM](operations/deploy.md) | Hub operators | Deployment classes, supervision, and hosted per-tenant hubs behind a proxy |
|
|
100
101
|
| [Monitor KXM](operations/monitoring.md) | Hub operators | `kxm dash`, health and readiness, metrics, logs and alerts |
|
|
101
102
|
| [Back up and restore KXM](operations/backup-and-restore.md) | Hub operators | `kxm backup` and `kxm restore`, the state roots, and a stopped-hub recipe |
|
|
@@ -32,9 +32,10 @@ On macOS the user state root is `Library/Application Support/KXM` under the
|
|
|
32
32
|
home directory (`kxmUserStateRoot` in `plugins/kxm/src/bindings.ts`). Windows
|
|
33
33
|
uses `AppData/Local/KXM`. Other platforms use `$XDG_STATE_HOME/kxm` or
|
|
34
34
|
`.local/state/kxm`. `KXM_STATE_HOME` overrides that root when it is absolute.
|
|
35
|
-
The tracker mentions `docs/operations.md` for the six backup roots. That
|
|
36
|
-
|
|
37
|
-
[Backup and restore](../operations/backup-and-restore.md).
|
|
35
|
+
The tracker mentions `docs/operations.md` for the six backup roots. That page
|
|
36
|
+
indexes the operations guides and links the six roots to
|
|
37
|
+
[Backup and restore](../operations/backup-and-restore.md). It also holds
|
|
38
|
+
[Cross-box peer attach](../operations.md#cross-box-peer-attach).
|
|
38
39
|
|
|
39
40
|
## Related
|
|
40
41
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Harness routing internals
|
|
2
2
|
|
|
3
|
-
This page records how the KXM repository applies [harness routing](../reference/harness-routing.md) to its own work: the routes this checkout admits, its writer roster, its price catalog, and the developer roster policy that `just assign` and the dev helper enforce. It is maintainer material. The
|
|
3
|
+
This page records how the KXM repository applies [harness routing](../reference/harness-routing.md) to its own work: the routes this checkout admits, its writer roster, its price catalog, and the developer roster policy that `just assign` and the dev helper enforce. It is maintainer material. The route and roster transcripts below were regenerated on 2026-09-28 from this checkout, where `node scripts/kxm.mjs` is the same program as `kxm`. Readiness notes that name a capture machine are from 2026-09-23 and were not re-probed. The transcripts change whenever an admission changes.
|
|
4
4
|
|
|
5
5
|
> [!IMPORTANT]
|
|
6
6
|
> The files are the authority, not this page: `.kxm/routes.yaml`, `.kxm/roles/*.yaml`, `.kxm/models/*.yaml`, and `.kxm/prices.yaml`. Product routing decisions are recorded under Tracking → Decided in `plans/implementation-plan.md`.
|
|
7
7
|
|
|
8
8
|
## Current ids (2026-09-27)
|
|
9
9
|
|
|
10
|
-
The
|
|
10
|
+
The files use one id shape per kind. A route id is `<harness>-<model-slug>[-<provider>]`, with `.` written as `-`. An agent id equals its role. A workflow id is `default`, `land`, or `<role>-only`. An agent step id equals that role.
|
|
11
11
|
|
|
12
12
|
This checkout's routes are `grok-grok-4-7`, `pi-qwen3-coder-plus-openrouter`, `agy-gemini-3-8-flash-high`, `agy-gemini-3-8-flash-medium`, `claude-fable`, `codex-gpt-5-6-sol`, `pi-qwen3-8-flash-openrouter`, and `pi-glm-5-3-flash-openrouter`. Agents are `planner`, `writer`, `reviewer-arch`, and `reviewer-cli`. `opus-claude` was removed because `opus` is not an admitted selector. `qwen-token-plan/*` and `zai-coding-cn/*` left the admitted list because those harnesses are not allowlisted; their `prices.yaml` rows stay. Old ids resolve as aliases. Every roster entry names an effort. `reviewer-arch` dispatches `claude-fable`. #343's architecture critic ran on opus because the justfile recipe review-arch hardcoded that model; the role file on that commit listed only `fable-claude`. The helper now refuses `opus`.
|
|
13
13
|
|
|
@@ -18,26 +18,22 @@ This checkout's routes are `grok-grok-4-7`, `pi-qwen3-coder-plus-openrouter`, `a
|
|
|
18
18
|
```text
|
|
19
19
|
schema: kxm.role.v2
|
|
20
20
|
id: writer
|
|
21
|
-
|
|
21
|
+
purpose: writer
|
|
22
|
+
permission: edit
|
|
23
|
+
description: Primary implementation agent.
|
|
22
24
|
skills: []
|
|
23
25
|
roster:
|
|
24
|
-
-
|
|
26
|
+
- route: grok-grok-4-7
|
|
25
27
|
effort: medium
|
|
26
|
-
|
|
27
|
-
- model: openrouter/qwen/qwen3-coder-plus
|
|
28
|
+
- route: pi-qwen3-coder-plus-openrouter
|
|
28
29
|
effort: medium
|
|
29
|
-
|
|
30
|
-
- model: zai-coding-cn/glm-5.3-flash
|
|
30
|
+
- route: agy-gemini-3-8-flash-high
|
|
31
31
|
effort: medium
|
|
32
|
-
|
|
33
|
-
- model: qwen-token-plan/qwen3.8-flash
|
|
32
|
+
- route: agy-gemini-3-8-flash-medium
|
|
34
33
|
effort: medium
|
|
35
|
-
enabled: true
|
|
36
|
-
- model: google/gemini-3.8-flash-high
|
|
37
|
-
enabled: true
|
|
38
34
|
```
|
|
39
35
|
|
|
40
|
-
The
|
|
36
|
+
The writer role lists route ids, in order: `grok-grok-4-7`, then `pi-qwen3-coder-plus-openrouter`, then `agy-gemini-3-8-flash-high` and `agy-gemini-3-8-flash-medium`. `grok-grok-4-7` is harness `grok`, model `grok-4.7`, vendor `xai`. An agent file does not declare `harness:`.
|
|
41
37
|
|
|
42
38
|
`node scripts/kxm.mjs routes list`:
|
|
43
39
|
|
|
@@ -49,12 +45,17 @@ admitted openai/gpt-5.6-sol
|
|
|
49
45
|
admitted openrouter/qwen/qwen3-coder-plus
|
|
50
46
|
admitted openrouter/qwen/qwen3.8-flash
|
|
51
47
|
admitted openrouter/z-ai/glm-5.3-flash
|
|
52
|
-
admitted
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
48
|
+
admitted xai/grok-4.7
|
|
49
|
+
planner claude-fable
|
|
50
|
+
planner pi-qwen3-8-flash-openrouter
|
|
51
|
+
reviewer-arch claude-fable
|
|
52
|
+
reviewer-arch pi-glm-5-3-flash-openrouter
|
|
53
|
+
reviewer-cli codex-gpt-5-6-sol
|
|
54
|
+
reviewer-cli pi-qwen3-8-flash-openrouter
|
|
55
|
+
writer grok-grok-4-7
|
|
56
|
+
writer pi-qwen3-coder-plus-openrouter
|
|
57
|
+
writer agy-gemini-3-8-flash-high
|
|
58
|
+
writer agy-gemini-3-8-flash-medium
|
|
58
59
|
```
|
|
59
60
|
|
|
60
61
|
`routes admit --model openrouter/qwen/qwen3-coder-plus --dry-run` exits 2 with `select a model from the refreshed inventory` even though that selector is already admitted, because the inventory never carries `openrouter/…` selectors. Selectors like these are admitted by a Git-reviewed edit of `.kxm/routes.yaml`.
|
|
@@ -63,7 +64,7 @@ On the capture machine, `kxm harness list` showed `claude` detected but logged o
|
|
|
63
64
|
|
|
64
65
|
### Price catalog and inventory
|
|
65
66
|
|
|
66
|
-
`.kxm/prices.yaml` is dated `2026-09-
|
|
67
|
+
`.kxm/prices.yaml` is dated `2026-09-28`. A run on that date can record a list estimate. Any other day records `providerMetadata.priceCatalogStale: true` and no list estimate. The list prices below come from `.kxm/models/inventory.yaml`. OpenRouter and Nous were fetched at `2026-09-28T14:02:09.052Z`. `pi`, `grok`, and `agy` were not installed for that refresh, so models that exist only in those CLIs are still the 2026-09-16 rows. Prices are USD per 1M tokens. The checkout has no routing records yet, so none of the examples has recorded latency; for latency, run a bounded side-by-side experiment and compare p50 and p95 in `kxm routing report`.
|
|
67
68
|
|
|
68
69
|
## The developer roster
|
|
69
70
|
|
|
@@ -108,15 +109,15 @@ The product brake, the dev helper and the roster policy now agree on the vendor
|
|
|
108
109
|
|
|
109
110
|
These extend the generic examples on the reference page with this checkout's admissions, developer policy routes, readiness on the capture machine, and inventory prices.
|
|
110
111
|
|
|
111
|
-
### Grok 4.
|
|
112
|
+
### Grok 4.7
|
|
112
113
|
|
|
113
114
|
| | Native `grok` | Pi + OpenRouter |
|
|
114
115
|
|---|---|---|
|
|
115
|
-
| Selector | `xai/grok-4.
|
|
116
|
+
| Selector | `xai/grok-4.7`: admitted, and first in the writer roster | `openrouter/x-ai/grok-4.7`: not admitted |
|
|
116
117
|
| Developer roster | `grok-grok-4-7`, the writer route with `edit` | Refused: `native vendor cannot use Pi` |
|
|
117
|
-
| Readiness | `grok`
|
|
118
|
-
| Billing | grok.com subscription (OAuth) | $
|
|
119
|
-
| Context |
|
|
118
|
+
| Readiness | `grok` showed auth `yes` on the 2026-09-23 capture machine | `pi auth check --provider openrouter` returned `not_ready` on that capture |
|
|
119
|
+
| Billing | grok.com subscription (OAuth) | $1.60 input, $4.80 output, $0.40 cached input |
|
|
120
|
+
| Context | 500,000 (`x-ai/grok-4.7` in the refreshed inventory) | 500,000 |
|
|
120
121
|
|
|
121
122
|
Pi's own `xai` provider reported `ready` (OAuth) on the capture machine. When the grok quota runs out, the next writer in the lineup is `pi-qwen3-coder-plus-openrouter`, a different vendor.
|
|
122
123
|
|
|
@@ -147,7 +148,7 @@ In the inventory, `openai/gpt-5.6-sol` has sources `openrouter+nous` and carries
|
|
|
147
148
|
|
|
148
149
|
| | Native `agy` | `antigravity` Pi provider | Pi + OpenRouter |
|
|
149
150
|
|---|---|---|---|
|
|
150
|
-
| Selector | `google/gemini-3.8-flash-high`: admitted, last
|
|
151
|
+
| Selector | `google/gemini-3.8-flash-high` and `google/gemini-3.8-flash-medium`: admitted, the last two writer routes | `antigravity/gemini-3.8-flash`: not admitted | `openrouter/google/gemini-3.8-flash`: not admitted |
|
|
151
152
|
| Billing | Google subscription | Google subscription | $0.75 input, $3.75 output, $0.075 cached input |
|
|
152
153
|
| Context | `agy` does not print it; the vendored catalog lists 1,048,576 | 1,048,576 | 1,048,576 |
|
|
153
154
|
|
package/docs/glossary.md
CHANGED
|
@@ -38,7 +38,7 @@ The service that authenticates [agents](#agent), stores [messages](#message) and
|
|
|
38
38
|
|
|
39
39
|
### Hub binding
|
|
40
40
|
|
|
41
|
-
The record that `kxm hub bind <url>` writes to `hub-binding.json` in the [user state root](#user-state-root), naming the hub this machine uses. `kxm hub view` checks it, and the Runtime syncs to it. Binding a remote hub requires a credential.
|
|
41
|
+
The record that `kxm hub bind <url>` writes to `hub-binding.json` in the [user state root](#user-state-root), naming the hub this machine uses. `kxm hub view` checks it, and the Runtime syncs to it. Binding a remote hub requires a credential. `kxm hub bind --cloud` marks a loopback forward as remote and records a token variable or command, never the token.
|
|
42
42
|
|
|
43
43
|
### KontextMind
|
|
44
44
|
|
|
@@ -104,11 +104,11 @@ Another [agent](#agent) in the same [project](#project). `kxm_list` and `kxm pee
|
|
|
104
104
|
|
|
105
105
|
### Project
|
|
106
106
|
|
|
107
|
-
The authentication and discovery namespace on the hub.
|
|
107
|
+
The authentication and discovery namespace on the hub. Every command resolves it the same way: `--project` or `KXM_PROJECT`, then the [project ID](#project-id) in `.kxm/project.yaml`, then the `package.json` name, then the directory name. Its key in the hub's `KXM_PROJECT_TOKENS` holds its token. Agents see only peers in the same project.
|
|
108
108
|
|
|
109
109
|
### Project ID
|
|
110
110
|
|
|
111
|
-
The stable `prj_` identifier in `.kxm/project.yaml`.
|
|
111
|
+
The stable `prj_` identifier in `.kxm/project.yaml`. It is the hub [project](#project) name unless `--project` or `KXM_PROJECT` overrides it. The [Runtime](#runtime) store keeps this id as the local key.
|
|
112
112
|
|
|
113
113
|
### Project token
|
|
114
114
|
|
|
@@ -87,6 +87,9 @@ If a malformed claim remains, read the exact `.pid` JSON in the state directory
|
|
|
87
87
|
| `hub_url_invalid` | Use an `http` or `https` URL without user info, query or fragment |
|
|
88
88
|
| `hub_bind_unauthenticated` | The URL is remote and this machine has no token for the project; export it and bind again |
|
|
89
89
|
| `hub_credential_unreadable` | Repair or remove `hub-env.json` under the user state root |
|
|
90
|
+
| `cloud_token_missing` | A `--cloud` binding's variable is empty and it has no working command. Export the variable or fix `--token-command`. This machine's hub-env token is not sent |
|
|
91
|
+
| `cloud_flag_required` | `--token-env` or `--token-command` was passed without `--cloud` |
|
|
92
|
+
| 401 after binding a loopback forward | The bind was not `--cloud`, so the client sent the local hub-env token. See [Cross-box peer attach](../operations.md#cross-box-peer-attach) |
|
|
90
93
|
|
|
91
94
|
## Claude Code plugin and MCP
|
|
92
95
|
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Operate a KXM hub
|
|
2
|
+
|
|
3
|
+
This page is the operator index for a [hub](glossary.md#hub): where the other
|
|
4
|
+
operations guides live, and how to attach a client on another machine to a hub
|
|
5
|
+
that is not reachable from the internet. The client in that setup is a local
|
|
6
|
+
`kxm` CLI, plus the Pi and Claude Code agents that CLI starts.
|
|
7
|
+
|
|
8
|
+
## Where each procedure lives
|
|
9
|
+
|
|
10
|
+
| Procedure | Page |
|
|
11
|
+
|---|---|
|
|
12
|
+
| Install the services on a tenant box, including loopback listeners | [Deploy KXM](operations/deploy.md) |
|
|
13
|
+
| Health, readiness, metrics, logs, and alerts | [Monitor KXM](operations/monitoring.md) |
|
|
14
|
+
| Stopped-state backup of all six state roots, and restore | [Back up and restore KXM](operations/backup-and-restore.md) |
|
|
15
|
+
| Package update, plugin reinstall, and schema ceilings | [Upgrade KXM](operations/upgrade.md) |
|
|
16
|
+
| Runtime outbox, sync refusals, and hub leases | [Operate Runtime sync and leases](operations/runtime-sync.md) |
|
|
17
|
+
| Symptoms, causes, and fixes | [Troubleshoot KXM](operations/troubleshooting.md) |
|
|
18
|
+
|
|
19
|
+
The six roots are the checkout, the workspace, workspace state, the user state
|
|
20
|
+
root, user config, and federated telemetry. The stopped-state copy of each one
|
|
21
|
+
is in [Back up and restore KXM](operations/backup-and-restore.md).
|
|
22
|
+
|
|
23
|
+
## Before you begin
|
|
24
|
+
|
|
25
|
+
- The `kxm` CLI is installed on the client machine.
|
|
26
|
+
- You can open SSH to the hub host, and you already have that host's key.
|
|
27
|
+
- The hub's admin token is in a password manager. 1Password CLI (`op`) is the
|
|
28
|
+
usual source. The token is never written into `hub-binding.json`.
|
|
29
|
+
|
|
30
|
+
## Cross-box peer attach
|
|
31
|
+
|
|
32
|
+
Use this when the hub listens on loopback on a private machine and your client
|
|
33
|
+
reaches it through an SSH local forward. A plain `kxm hub bind` of
|
|
34
|
+
`http://127.0.0.1:17331` is a loopback bind: it stores no credential, and later
|
|
35
|
+
commands send this machine's hub-env admin token. The remote hub rejects that
|
|
36
|
+
token with 401. `--cloud` marks the binding remote even though the URL is
|
|
37
|
+
loopback, and names where the token comes from without storing the token.
|
|
38
|
+
|
|
39
|
+
### Open the forward with a pinned host key
|
|
40
|
+
|
|
41
|
+
Pin the host key before the first connection. Do not accept a key on first use.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
ssh-keyscan -H 10.31.0.21 >> ~/.ssh/known_hosts
|
|
45
|
+
ssh -N \
|
|
46
|
+
-o ExitOnForwardFailure=yes \
|
|
47
|
+
-o StrictHostKeyChecking=yes \
|
|
48
|
+
-o UserKnownHostsFile="$HOME/.ssh/known_hosts" \
|
|
49
|
+
-L 127.0.0.1:17331:127.0.0.1:7331 \
|
|
50
|
+
user@10.31.0.21
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Leave that SSH session open. Hub traffic then goes to `127.0.0.1:17331` on the
|
|
54
|
+
client and comes out on the hub host's loopback port 7331.
|
|
55
|
+
|
|
56
|
+
<details><summary>PowerShell</summary>
|
|
57
|
+
|
|
58
|
+
```powershell
|
|
59
|
+
ssh-keyscan -H 10.31.0.21 >> $env:USERPROFILE\.ssh\known_hosts
|
|
60
|
+
ssh -N `
|
|
61
|
+
-o ExitOnForwardFailure=yes `
|
|
62
|
+
-o StrictHostKeyChecking=yes `
|
|
63
|
+
-o UserKnownHostsFile="$env:USERPROFILE\.ssh\known_hosts" `
|
|
64
|
+
-L 127.0.0.1:17331:127.0.0.1:7331 `
|
|
65
|
+
user@10.31.0.21
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
</details>
|
|
69
|
+
|
|
70
|
+
### Bind the forward as a cloud hub
|
|
71
|
+
|
|
72
|
+
Pick one token source. The environment variable is enough when the shell
|
|
73
|
+
already holds the token. The command form asks 1Password each time a command
|
|
74
|
+
needs the token. Do not put the token text in `--token-command`: that string
|
|
75
|
+
is stored in the binding.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
export KXMD_HUB_TOKEN="$(op read 'op://Private/kxmd/admin-token')"
|
|
79
|
+
kxm hub bind --cloud --token-env KXMD_HUB_TOKEN http://127.0.0.1:17331
|
|
80
|
+
kxm hub view
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Or name the 1Password command and skip the variable:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
kxm hub bind --cloud \
|
|
87
|
+
--token-command "op read op://Private/kxmd/admin-token" \
|
|
88
|
+
http://127.0.0.1:17331
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Expected output:
|
|
92
|
+
|
|
93
|
+
```text
|
|
94
|
+
bound hub http://127.0.0.1:17331 · remote · health=on · token is not stored
|
|
95
|
+
hub health=true ready=true · remote hub
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`--token-env` and `--token-command` can be combined. A non-empty variable wins.
|
|
99
|
+
Otherwise the command runs. The command is a program and arguments, not a shell
|
|
100
|
+
pipeline (`|`, `&`, `;`, and redirection are refused).
|
|
101
|
+
|
|
102
|
+
<details><summary>PowerShell</summary>
|
|
103
|
+
|
|
104
|
+
```powershell
|
|
105
|
+
$env:KXMD_HUB_TOKEN = op read "op://Private/kxmd/admin-token"
|
|
106
|
+
kxm hub bind --cloud --token-env KXMD_HUB_TOKEN http://127.0.0.1:17331
|
|
107
|
+
kxm hub view
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
</details>
|
|
111
|
+
|
|
112
|
+
### What --cloud changes
|
|
113
|
+
|
|
114
|
+
| | Local bind | `--cloud` bind |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| Loopback URL | Scope `loopback`. No credential required | Scope `remote`, including an SSH forward |
|
|
117
|
+
| Token stored in `hub-binding.json` | No | No. The file names `tokenEnv` and/or `tokenCommand` |
|
|
118
|
+
| Credential later commands send | `KXM_AUTH_TOKEN`, then this machine's hub-env | The named source only. Hub-env is not a fallback |
|
|
119
|
+
| Missing source | Agent commands report `project_token_missing` | `cloud_token_missing` or `cloud_token_command_failed`. The local admin token is not sent |
|
|
120
|
+
| `kxm hub start` from an extension | May start a local hub | Does not start a local hub when the forward is down (`cloud_hub_unreachable`) |
|
|
121
|
+
|
|
122
|
+
`kxm hub view`, `kxm tenant status`, `kxm peer`, `kxm workflow` agent commands,
|
|
123
|
+
`kxm context`, Runtime presence, and the environment exported for a Pi worker
|
|
124
|
+
all use that source. `kxm agent worker` puts `KXM_AUTH_TOKEN` in the child
|
|
125
|
+
process environment only. It does not write the token to disk.
|
|
126
|
+
|
|
127
|
+
The hub project id is the same value for peer commands and `kxm tenant status`:
|
|
128
|
+
`--project` or `KXM_PROJECT`, then the `id` in `.kxm/project.yaml`, then
|
|
129
|
+
`package.json` `name`, then the directory name.
|
|
130
|
+
|
|
131
|
+
### Remove the binding
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
kxm hub unbind
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Unbind deletes `hub-binding.json`. It does not delete hub-env, and it does not
|
|
138
|
+
close the SSH forward. Stop the forward by ending the `ssh -N` session.
|
|
139
|
+
|
|
140
|
+
## Troubleshooting
|
|
141
|
+
|
|
142
|
+
| Symptom | Cause | Fix |
|
|
143
|
+
|---|---|---|
|
|
144
|
+
| 401 `invalid_auth` after binding `127.0.0.1:17331` | The bind was not `--cloud`, so the client sent this machine's hub-env admin token | Bind again with `--cloud` and a token source that holds the remote hub's token. Unset a stale `KXM_AUTH_TOKEN` that points at the local hub |
|
|
145
|
+
| `cloud_token_missing` | `KXMD_HUB_TOKEN` (or the variable you named) is unset or empty, and no command is configured | Export the variable or pass `--token-command`. The local hub-env token is intentionally unused |
|
|
146
|
+
| `cloud_token_command_failed` | `op` exited non-zero, timed out, or printed more than one line | Run the same command in a terminal and sign in to 1Password. Do not paste the token into the binding |
|
|
147
|
+
| Clients through one SSH host get locked out together | The hub rate-limits by agent id, otherwise by source address. Every forward from one SSH session shares `127.0.0.1` on the hub. `sshd` `MaxStartups` and `MaxSessions` can refuse new forwards first | Give each long-lived agent its own name. Raise the SSH limits on that host if many people share one jump host. Do not point two tenants at one hub |
|
|
148
|
+
| `kxm hub view` says `loopback` for the forward | `--cloud` was omitted | Bind again with `--cloud`. `KXM_SERVER_URL` overrides the binding URL and is labeled on its own |
|
|
149
|
+
|
|
150
|
+
## Next steps
|
|
151
|
+
|
|
152
|
+
- Deploy the hub itself: [Deploy KXM](operations/deploy.md)
|
|
153
|
+
- Read health and metrics: [Monitor KXM](operations/monitoring.md)
|
|
154
|
+
- Command flags: [kxm hub bind](reference/cli-reference.md#kxm-hub-bind)
|
|
@@ -569,16 +569,17 @@ kxm hub stop --wait-ms 8000 --json
|
|
|
569
569
|
### `kxm hub bind`
|
|
570
570
|
|
|
571
571
|
```text
|
|
572
|
-
kxm hub bind <url>
|
|
572
|
+
kxm hub bind <url> [--cloud] [--token-env <name>] [--token-command <command>]
|
|
573
573
|
```
|
|
574
574
|
|
|
575
575
|
Binds this machine to a running hub by writing `hub-binding.json` under the user state root, then probes the hub's health for up to 300 ms. Every hub client uses the binding when `KXM_SERVER_URL` is unset.
|
|
576
576
|
|
|
577
577
|
- Arguments: `<url>`, Hub base URL (http or https).
|
|
578
578
|
- A URL with credentials, a query, a fragment, or a scheme other than http or https fails with `hub_url_invalid` (exit 2).
|
|
579
|
-
- A remote (non-loopback) URL is refused with `hub_bind_unauthenticated` (exit 2, `nextAction: "export_kxm_auth_token"`) unless a credential for the current project resolves from `KXM_AUTH_TOKEN` or the persisted `hub-env.json`. A malformed record fails with `hub_credential_unreadable` (exit 2).
|
|
579
|
+
- A remote (non-loopback) URL is refused with `hub_bind_unauthenticated` (exit 2, `nextAction: "export_kxm_auth_token"`) unless a credential for the current project resolves from `KXM_AUTH_TOKEN` or the persisted `hub-env.json`. A malformed record fails with `hub_credential_unreadable` (exit 2). A loopback URL without `--cloud` still binds with no credential.
|
|
580
|
+
- `--cloud` marks the binding remote even when the URL is a loopback forward. It requires `--token-env`, `--token-command`, or both. The token is not written. A non-empty variable wins; otherwise the command runs. Source flags without `--cloud` fail with `cloud_flag_required`. A missing source later fails with `cloud_token_missing` and does not fall back to hub-env. See [Cross-box peer attach](../operations.md#cross-box-peer-attach).
|
|
580
581
|
- Mutates the binding file. `--dry-run` validates and prints the plan without writing.
|
|
581
|
-
- JSON keys: `url`, `scope`, `file`, `health` (`on`, `off`, or `unknown`), `probeMs`.
|
|
582
|
+
- JSON keys: `url`, `scope`, `file`, `health` (`on`, `off`, or `unknown`), `probeMs`. A cloud bind also returns `cloud`, and `tokenEnv` or `tokenCommand` when set.
|
|
582
583
|
|
|
583
584
|
```bash
|
|
584
585
|
kxm hub bind http://127.0.0.1:7331 --dry-run
|
|
@@ -108,7 +108,7 @@ Every agent client (the Pi extension, the Claude Code MCP server, and the `kxm p
|
|
|
108
108
|
|---|---|---|
|
|
109
109
|
| `KXM_SERVER_URL` | `http://127.0.0.1:7331` | Hub base URL. For the CLI, the machine's `kxm hub bind` URL is used when this is unset |
|
|
110
110
|
| `KXM_AUTH_TOKEN` | See below | The project token for this agent's project |
|
|
111
|
-
| `KXM_PROJECT` | `name` in `package.json` in the working directory, else the directory name | Project for discovery and authentication |
|
|
111
|
+
| `KXM_PROJECT` | `id` in the nearest `.kxm/project.yaml`, else `name` in `package.json` in the working directory, else the directory name | Project for discovery and authentication. An explicit `--project` flag on `kxm peer list` and `kxm tenant status` wins over this variable. The same resolver is used by peer commands, tenant status, workers, the Pi extension, and the Claude Code MCP server |
|
|
112
112
|
| `KXM_AGENT_NAME` | Harness-specific (see below) | Name peers see; unique among online agents in the project, compared case-insensitively |
|
|
113
113
|
| `KXM_AGENT_PURPOSE` | Harness-specific (see below) | One line peers use to decide what to send this agent |
|
|
114
114
|
|
|
@@ -120,6 +120,16 @@ Every agent client (the Pi extension, the Claude Code MCP server, and the `kxm p
|
|
|
120
120
|
|
|
121
121
|
A clean shutdown marks an identity offline. Reconnecting with the same project and name resumes its durable agent ID and rotates its agent key. If the Claude Code name is already online in the project, the MCP server registers once more as `<name>-<pid>`.
|
|
122
122
|
|
|
123
|
+
A `--cloud` hub binding replaces this table's token lookup for every client, including agents. The token comes from the binding's `--token-env` or `--token-command`, never from this machine's hub-env file, and it is not written to disk. `kxm agent worker` copies it into the child process environment as `KXM_AUTH_TOKEN`.
|
|
124
|
+
|
|
125
|
+
Also check plugin.json is valid JSON.
|
|
126
|
+
|
|
127
|
+
And configuration table cell might be very long. That's ok.
|
|
128
|
+
|
|
129
|
+
Look at harness table "never the admin token" - the new paragraph clarifies cloud. Good.
|
|
130
|
+
|
|
131
|
+
Plugin README add one sentence under which token.
|
|
132
|
+
|
|
123
133
|
## Claude Code plugin settings
|
|
124
134
|
|
|
125
135
|
The plugin asks for `server_url`, `auth_token`, `agent_name`, `agent_purpose` and `project` when you install it, and passes them to its MCP server as `KXM_SERVER_URL`, `KXM_AUTH_TOKEN`, `KXM_AGENT_NAME`, `KXM_AGENT_PURPOSE` and `KXM_PROJECT`. It also sets `KXM_PROJECT_DIR` to the directory Claude Code started in. Change them with `/plugin configure kxm@kxm`. Every field, default and token rule is in [Claude Code plugin settings](config-reference.md#claude-code-plugin-settings) and the [plugin README](../../plugins/kxm/README.md).
|
|
@@ -131,11 +131,11 @@ This is the same vendor through Pi and OpenRouter, which the brake refuses:
|
|
|
131
131
|
|
|
132
132
|
```yaml
|
|
133
133
|
harness: pi
|
|
134
|
-
model: openrouter/x-ai/grok-4.
|
|
134
|
+
model: openrouter/x-ai/grok-4.7
|
|
135
135
|
vendor: xai
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
-
The selector is `openrouter/x-ai/grok-4.
|
|
138
|
+
The selector is `openrouter/x-ai/grok-4.7` and the harness is `pi`. The Pi brake refuses it before Pi starts, because the vendor segment `x-ai` is xAI, which has a native harness. A vendor with no native harness runs in the same shape: with `model: openrouter/qwen/qwen3-coder-plus`, Pi is spawned with `--model openrouter/qwen/qwen3-coder-plus`, its read-only flags (section 3) and `-p --mode json`. Write `model` exactly as the provider names it. For OpenRouter that is the vendor slug, so `x-ai`, not `xai`.
|
|
139
139
|
|
|
140
140
|
The harness and the selector have to agree. Pi refuses `provider: xai` and `openrouter/x-ai/…`. `grok` refuses `provider: openrouter`. [What the brake refuses](#what-the-brake-refuses) has the exact messages.
|
|
141
141
|
|
|
@@ -156,7 +156,9 @@ roster:
|
|
|
156
156
|
- route: pi-qwen3-coder-plus-openrouter
|
|
157
157
|
effort: medium
|
|
158
158
|
- route: agy-gemini-3-8-flash-high
|
|
159
|
+
effort: medium
|
|
159
160
|
- route: agy-gemini-3-8-flash-medium
|
|
161
|
+
effort: medium
|
|
160
162
|
```
|
|
161
163
|
|
|
162
164
|
`grok-grok-4-7` resolves to `.kxm/models/grok-grok-4-7.yaml`: harness `grok`, model `grok-4.7`, vendor `xai`, status `admitted`, permission `edit`. `pi-qwen3-coder-plus-openrouter` is harness `pi`, model `openrouter/qwen/qwen3-coder-plus`, vendor `alibaba`. `agy-gemini-3-8-flash-high` is harness `agy`, model `gemini-3.8-flash-high`, vendor `google`. `kxm role list` prints the first route id as the primary, for example `(grok-grok-4-7)`. The previous ids (`grok-native`, `qwen-openrouter-pi`, `gemini-agy`) are aliases.
|
|
@@ -171,8 +173,8 @@ Role files are what `kxm role` and the Runtime membership check use.
|
|
|
171
173
|
|
|
172
174
|
```text
|
|
173
175
|
admitted openrouter/qwen/qwen3-coder-plus
|
|
174
|
-
admitted xai/grok-4.
|
|
175
|
-
disabled openrouter/x-ai/grok-4.
|
|
176
|
+
admitted xai/grok-4.7
|
|
177
|
+
disabled openrouter/x-ai/grok-4.7
|
|
176
178
|
```
|
|
177
179
|
|
|
178
180
|
The first segment of an id is the provider, and the rest is the model id. A route id does not name a harness. The first segment tells you which harness can run it. For an aggregator, the next segment names the vendor, and the Pi brake reads that too:
|
|
@@ -205,11 +207,11 @@ select a model from the refreshed inventory
|
|
|
205
207
|
It exits with code 2. A bare OpenRouter slug is accepted, but it is not a dispatchable selector:
|
|
206
208
|
|
|
207
209
|
```bash
|
|
208
|
-
kxm routes admit --model x-ai/grok-4.
|
|
210
|
+
kxm routes admit --model x-ai/grok-4.7 --dry-run
|
|
209
211
|
```
|
|
210
212
|
|
|
211
213
|
```text
|
|
212
|
-
would admit x-ai/grok-4.
|
|
214
|
+
would admit x-ai/grok-4.7
|
|
213
215
|
```
|
|
214
216
|
|
|
215
217
|
If that id were admitted, an agent would need `provider: x-ai`. Pi has no provider called `x-ai`, so the Pi auth probe would fail closed. When a route needs a provider prefix that the inventory does not carry, admit it by a Git-reviewed edit of `.kxm/routes.yaml`, after the admission decision is reviewed.
|
|
@@ -218,13 +220,13 @@ If that id were admitted, an agent would need `provider: x-ai`. Pi has no provid
|
|
|
218
220
|
|
|
219
221
|
The same model shows up under different ids depending on the file:
|
|
220
222
|
|
|
221
|
-
| Where | Id for Grok 4.
|
|
223
|
+
| Where | Id for Grok 4.7 | What that id means |
|
|
222
224
|
|---|---|---|
|
|
223
|
-
| `.kxm/routes.yaml`, agent selector | `xai/grok-4.
|
|
224
|
-
| `.kxm/routes.yaml`, agent selector | `openrouter/x-ai/grok-4.
|
|
225
|
-
| `.kxm/models/inventory.yaml`, source `pi` | `xai/grok-4.6` | Pi's own `xai` provider. The brake refuses it. |
|
|
226
|
-
| `.kxm/models/inventory.yaml`, source `openrouter` | `x-ai/grok-4.
|
|
227
|
-
| `.kxm/prices.yaml` | `id: xai/grok-4.
|
|
225
|
+
| `.kxm/routes.yaml`, agent selector | `xai/grok-4.7` | Provider `xai`. Needs `harness: grok`. |
|
|
226
|
+
| `.kxm/routes.yaml`, agent selector | `openrouter/x-ai/grok-4.7` | OpenRouter through Pi. The brake refuses it. |
|
|
227
|
+
| `.kxm/models/inventory.yaml`, source `pi` | `xai/grok-4.6` | Pi's own `xai` provider from the last `pi --list-models` capture. The brake refuses it. This refresh did not re-run Pi, and that capture has no `xai/grok-4.7` row. |
|
|
228
|
+
| `.kxm/models/inventory.yaml`, source `openrouter` | `x-ai/grok-4.7` | The OpenRouter catalog slug. The inventory stores OpenRouter ids without an `openrouter/` prefix. |
|
|
229
|
+
| `.kxm/prices.yaml` | `id: xai/grok-4.7`, `provider: xai`, alias `x-ai/grok-4.7` | A list price that applies only to provider `xai`, because price lookup filters by provider. `xai/grok-4.6` remains a separate row. |
|
|
228
230
|
|
|
229
231
|
An inventory id can also mean something else in `.kxm/routes.yaml`. For example, the inventory can list `openai/gpt-5.6-sol` from the OpenRouter feed with OpenRouter prices, while in `.kxm/routes.yaml` the same string means provider `openai` through native `codex`.
|
|
230
232
|
|
|
@@ -341,12 +343,12 @@ The live one-shot producer runs every harness read-only, with a 120-second proce
|
|
|
341
343
|
|
|
342
344
|
The Runtime does not run a step live when it has `write` access to a repository; it hands the run off with `step_unsupported`. Only Pi runs as a long-lived worker.
|
|
343
345
|
|
|
344
|
-
### Grok 4.
|
|
346
|
+
### Grok 4.7
|
|
345
347
|
|
|
346
348
|
| | Native `grok` | Pi + OpenRouter |
|
|
347
349
|
|---|---|---|
|
|
348
|
-
| Model file | `harness: grok`, `model: grok-4.
|
|
349
|
-
| Selector | `xai/grok-4.
|
|
350
|
+
| Model file | `harness: grok`, `model: grok-4.7`, `vendor: xai` | `harness: pi`, `model: openrouter/x-ai/grok-4.7`, `vendor: xai` |
|
|
351
|
+
| Selector | `xai/grok-4.7` | `openrouter/x-ai/grok-4.7` |
|
|
350
352
|
| Billing | grok.com subscription (OAuth) | OpenRouter credit |
|
|
351
353
|
| Recorded `costBasis` | `unknown` | None: the brake refuses it before dispatch |
|
|
352
354
|
| Mode | One-shot `grok --single`. Not a worker. | Refused as a one-shot and as a worker model |
|
package/package.json
CHANGED
|
@@ -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.
|
|
5
|
+
"version": "0.7.165",
|
|
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",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"project": {
|
|
66
66
|
"type": "string",
|
|
67
67
|
"title": "Project",
|
|
68
|
-
"description": "Hub project key; must match a key in the hub KXM_PROJECT_TOKENS. Blank uses the package.json name, then the directory name",
|
|
68
|
+
"description": "Hub project key; must match a key in the hub KXM_PROJECT_TOKENS. Blank uses the .kxm/project.yaml id, then the package.json name, then the directory name",
|
|
69
69
|
"required": false
|
|
70
70
|
}
|
|
71
71
|
}
|
package/plugins/kxm/README.md
CHANGED
|
@@ -58,7 +58,7 @@ Claude Code asks for these options when you install the plugin. Change them late
|
|
|
58
58
|
| `auth_token` | `KXM_AUTH_TOKEN` | Blank | This project's token from the hub's `KXM_PROJECT_TOKENS`. Leave it blank on the machine that runs the hub. Never the hub admin token. Marked sensitive. |
|
|
59
59
|
| `agent_name` | `KXM_AGENT_NAME` | `claude` | Name other agents see. The first active session in a project keeps it; a later concurrent session registers as `<name>-<pid>`. |
|
|
60
60
|
| `agent_purpose` | `KXM_AGENT_PURPOSE` | `Claude Code implementation and review agent` | One line that peers use to decide what to send this agent. |
|
|
61
|
-
| `project` | `KXM_PROJECT` | Blank | Hub project key; must match a key in the hub's `KXM_PROJECT_TOKENS`. Blank uses `
|
|
61
|
+
| `project` | `KXM_PROJECT` | Blank | Hub project key; must match a key in the hub's `KXM_PROJECT_TOKENS`. Blank uses the `id` in `.kxm/project.yaml`, then `name` from `package.json`, then the directory name. |
|
|
62
62
|
|
|
63
63
|
The MCP server also receives `KXM_PROJECT_DIR`, set to the directory Claude Code was started in (`CLAUDE_PROJECT_DIR`). It decides the default project key and whether this is a KXM project (one with a `.kxm/` directory).
|
|
64
64
|
|
|
@@ -67,7 +67,7 @@ The MCP server also receives `KXM_PROJECT_DIR`, set to the directory Claude Code
|
|
|
67
67
|
The plugin acts as an agent of one hub project and authenticates with that project's token. It never uses the hub admin token.
|
|
68
68
|
|
|
69
69
|
- On the machine that runs the hub, leave `auth_token` blank. The MCP server then uses the project token the hub saved for this project in `hub-env.json` under the user state root (`KXM_STATE_HOME`, or the platform default listed in [State outside the project](../../docs/reference/config-reference.md#state-outside-the-project)). It uses only that entry, never the admin token saved beside it.
|
|
70
|
-
- On any other machine, enter this project's token at `/plugin configure kxm@kxm`. Get it from whoever runs the hub, through your password manager.
|
|
70
|
+
- On any other machine, enter this project's token at `/plugin configure kxm@kxm`. Get it from whoever runs the hub, through your password manager. When the machine is bound with `kxm hub bind --cloud`, leave `auth_token` blank: the MCP server reads the binding's token source and does not store the token. The recipe is [Cross-box peer attach](../../docs/operations.md#cross-box-peer-attach).
|
|
71
71
|
- Never enter the hub admin token. It is the operator's credential. The hub accepts it for any project that has no token of its own, so an agent holding it could act in projects it was never given.
|
|
72
72
|
- Without a project token, every `kxm_*` tool fails with `KXM has no project token for project <p> on this machine`, and the MCP server does not contact the hub.
|
|
73
73
|
|