@kontextmind/kxm 0.7.192 → 0.7.193
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 +18 -0
- package/docs/contributing/ci-and-release.md +1 -1
- package/docs/contributing/development.md +37 -0
- package/docs/contributing/harness-routing-internals.md +2 -2
- package/docs/reference/config-reference.md +7 -2
- package/docs/reference/harness-routing.md +3 -2
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/model-catalog-refresh.ts +77 -5
package/CHANGELOG.md
CHANGED
|
@@ -714,6 +714,24 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
714
714
|
|
|
715
715
|
### Fixed
|
|
716
716
|
|
|
717
|
+
- **The weekly model refresh opens its pull request with a GitHub App.**
|
|
718
|
+
`.github/workflows/refresh-models.yml` uses `actions/create-github-app-token`
|
|
719
|
+
with repository secrets `KXM_REFRESH_APP_ID` and
|
|
720
|
+
`KXM_REFRESH_APP_PRIVATE_KEY`. The App is installed on `kontextmind/kxm`
|
|
721
|
+
only, with Contents read and write and Pull requests read and write. A
|
|
722
|
+
missing secret fails the job and names both. `GITHUB_TOKEN` is not used to
|
|
723
|
+
create the pull request. Setup is in
|
|
724
|
+
[Refresh model prices and inventory](docs/contributing/development.md#refresh-model-prices-and-inventory).
|
|
725
|
+
- **GLM 5.3 prices no longer share one YAML anchor.**
|
|
726
|
+
`openrouter/z-ai/glm-5.3-flash` tracks the OpenRouter models API on
|
|
727
|
+
2026-10-20: $0.047076 input, $0.12 output, and $0.006 cache read per 1M
|
|
728
|
+
tokens. `zai-coding-cn/glm-5.3-flash` keeps the coding-plan rate of $0.15 /
|
|
729
|
+
$0.50 (cache read $0.03). `zai-coding-cn/glm-5.3` output is $1.40, matching
|
|
730
|
+
OpenRouter `z-ai/glm-5.3` (input $0.079 and cache read $0.078 were already
|
|
731
|
+
the same). The refresh does not rewrite a shared anchor that a
|
|
732
|
+
non-OpenRouter row depends on. `.kxm/prices.yaml` is dated 2026-10-20.
|
|
733
|
+
The OpenRouter `standard` rates for those two inventory rows match.
|
|
734
|
+
|
|
717
735
|
- **`kxm improve report` and `kxm routing report` include attempts settled in a registered lane.**
|
|
718
736
|
Inside a KXM project, a present user-state `registry.db` is opened read-only at schema version 2. One query loads the home row and every lane row for that project id. Event-store paths come from the stored project keys, so a stale checkout path cannot hide a lane. A missing store stays a zero-record source. An absent registry, or a checkout that is not registered, still reads only that checkout's own store. `--file` still reads only the named file. See
|
|
719
737
|
[kxm improve report](docs/reference/cli-reference.md#kxm-improve-report).
|
|
@@ -46,7 +46,7 @@ together.
|
|
|
46
46
|
| Push to `main`, or manual | `ci.yml` (Validate matrix) | `validate:pr` on Linux and Windows for Node 22.19.0 and Node 24; not on a pull request |
|
|
47
47
|
| Code pull request and code push | `ci.yml` (Plugin validation) | `claude plugin validate --strict` on the marketplace and the plugin |
|
|
48
48
|
| Daily at 04:00 UTC, or manual | `nightly.yml` | `test:coverage:complete`, `check`, `check:generated`, `npm pack --dry-run` |
|
|
49
|
-
| Monday at 13:00 UTC, or manual | `refresh-models.yml` | Fetches the public OpenRouter catalog, and opens or updates one pull request on `automation/model-refresh` when `.kxm/prices.yaml` or `.kxm/models/inventory.yaml` changes.
|
|
49
|
+
| Monday at 13:00 UTC, or manual | `refresh-models.yml` | Fetches the public OpenRouter catalog, and opens or updates one pull request on `automation/model-refresh` when `.kxm/prices.yaml` or `.kxm/models/inventory.yaml` changes. The pull request uses a GitHub App token from `KXM_REFRESH_APP_ID` and `KXM_REFRESH_APP_PRIVATE_KEY` (`actions/create-github-app-token`). `GITHUB_TOKEN` posts `model-catalog-guard` and dispatches `ci.yml` on that branch. App setup is in [Refresh model prices and inventory](development.md#refresh-model-prices-and-inventory) |
|
|
50
50
|
| Merged pull request | `auto-release.yml` | Tags the merge commit and dispatches `release.yml` |
|
|
51
51
|
| Tag push or dispatch | `release.yml` | Verifies, packs and publishes (see [Release flow](#release-flow)) |
|
|
52
52
|
| Manual only | `smoke.yml` | Real Pi smoke, currently disabled (see [Smoke tests](#smoke-tests)) |
|
|
@@ -418,6 +418,43 @@ Review the diff and commit it yourself, or wait for the Monday pull request on
|
|
|
418
418
|
`automation/model-refresh`. The field reference is
|
|
419
419
|
[Weekly OpenRouter refresh](../reference/config-reference.md#weekly-openrouter-refresh).
|
|
420
420
|
|
|
421
|
+
The Monday job opens that pull request with a GitHub App installation token,
|
|
422
|
+
not the workflow's `GITHUB_TOKEN`. This repository does not permit GitHub
|
|
423
|
+
Actions to create pull requests, and the workflow does not change that
|
|
424
|
+
setting. The App is installed on `kontextmind/kxm` only. Its repository
|
|
425
|
+
permissions are Contents read and write, and Pull requests read and write.
|
|
426
|
+
No other repository is installed, and the token asks only for those two
|
|
427
|
+
permissions on `kxm`.
|
|
428
|
+
|
|
429
|
+
Create the App as an organization owner:
|
|
430
|
+
|
|
431
|
+
1. Open [New GitHub App](https://github.com/organizations/kontextmind/settings/apps/new).
|
|
432
|
+
Set the homepage URL to `https://github.com/kontextmind/kxm`. Uncheck
|
|
433
|
+
Active under Webhook. The workflow only mints an installation token.
|
|
434
|
+
2. Under Repository permissions, set **Contents** to Read and write and
|
|
435
|
+
**Pull requests** to Read and write. Leave every other permission at No
|
|
436
|
+
access.
|
|
437
|
+
3. Under "Where can this GitHub App be installed?", choose **Only on this
|
|
438
|
+
account**. Create the App.
|
|
439
|
+
4. On the App's settings page, copy the numeric **App ID**. Generate a
|
|
440
|
+
private key and keep the downloaded PEM.
|
|
441
|
+
5. Install the App: **Install App**, then **Only select repositories**,
|
|
442
|
+
then `kontextmind/kxm`, then Install. Do not install it on any other
|
|
443
|
+
repository.
|
|
444
|
+
6. On [Actions secrets](https://github.com/kontextmind/kxm/settings/secrets/actions)
|
|
445
|
+
for this repository, add `KXM_REFRESH_APP_ID` (the numeric App ID, not
|
|
446
|
+
the Client ID) and `KXM_REFRESH_APP_PRIVATE_KEY` (the full PEM, including
|
|
447
|
+
the `BEGIN` and `END` lines).
|
|
448
|
+
|
|
449
|
+
`actions/create-github-app-token` reads those secrets as `app-id` and
|
|
450
|
+
`private-key`. If either secret is missing, the job fails before it pushes
|
|
451
|
+
and names both `KXM_REFRESH_APP_ID` and `KXM_REFRESH_APP_PRIVATE_KEY`. An
|
|
452
|
+
unchanged catalog still requires the secrets: a Monday with no diff exits 0
|
|
453
|
+
only after both are set. The workflow `GITHUB_TOKEN` still posts the
|
|
454
|
+
`model-catalog-guard` commit status and dispatches `ci.yml`, because the App
|
|
455
|
+
is not granted statuses or actions. Do not turn on "Allow GitHub Actions to
|
|
456
|
+
create and approve pull requests."
|
|
457
|
+
|
|
421
458
|
`kxm models inventory-refresh` still rewrites the whole inventory from Pi, Grok,
|
|
422
459
|
agy, OpenRouter, and Nous, including a new `fetchedAt` timestamp. The weekly
|
|
423
460
|
script does not call those local CLIs.
|
|
@@ -76,7 +76,7 @@ On the capture machine, `kxm harness list` showed `claude` detected but logged o
|
|
|
76
76
|
|
|
77
77
|
### Price catalog and inventory
|
|
78
78
|
|
|
79
|
-
`.kxm/prices.yaml` is dated `2026-10-
|
|
79
|
+
`.kxm/prices.yaml` is dated `2026-10-20`. 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-10-17T13:45:00.000Z`. `pi`, `grok`, and `agy` were not installed for that refresh, so models that exist only in those CLIs stay, including the previous `agy` rows, and the Gemini 4 Argon and Claude 5.5 `agy` rows were added by hand from `agy models`. 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`.
|
|
80
80
|
|
|
81
81
|
## The developer roster
|
|
82
82
|
|
|
@@ -182,7 +182,7 @@ The code differs from that decision: `.kxm/routes.yaml` admits `google/gemini-3.
|
|
|
182
182
|
|---|---|---|---|
|
|
183
183
|
| Qwen3.8 Flash | `qwen-token-plan/qwen3.8-flash`: removed from the admitted list on 2026-09-27; `qwen-token-plan` is not allowlisted | `openrouter/qwen/qwen3.8-flash`: admitted, route `pi-qwen3-8-flash-openrouter`, read-only fallback for planner and reviewer-cli | `prices.yaml` keeps both rows. |
|
|
184
184
|
| Qwen3.7 Plus | none | `openrouter/qwen/qwen3.7-plus`: route `pi-qwen3-7-plus-openrouter`, writer #2 | The only admitted Pi writer: exact model, `edit` permission. OpenRouter list on 2026-10-17 is $0.32 input and $1.28 output per 1M tokens, 1,000,000 context, tools supported. |
|
|
185
|
-
| GLM 5.3 and 5.3 Flash | `zai-coding-cn/glm-5.3` and `…/glm-5.3-flash`: removed from the admitted list on 2026-09-27; `zai-coding-cn` is not allowlisted | `openrouter/z-ai/glm-5.3-flash`: admitted, route `pi-glm-5-3-flash-openrouter`, read-only fallback for reviewer-arch | Z.ai has no native harness. |
|
|
185
|
+
| GLM 5.3 and 5.3 Flash | `zai-coding-cn/glm-5.3` and `…/glm-5.3-flash`: removed from the admitted list on 2026-09-27; `zai-coding-cn` is not allowlisted. The flash coding-plan row keeps $0.15 / $0.50 (cache read $0.03). `zai-coding-cn/glm-5.3` output is $1.40 | `openrouter/z-ai/glm-5.3-flash`: admitted, route `pi-glm-5-3-flash-openrouter`, read-only fallback for reviewer-arch. Its own tier, verified 2026-10-20: $0.047076 input, $0.12 output, $0.006 cache read | Z.ai has no native harness. The two flash rows do not share a YAML anchor. |
|
|
186
186
|
| DeepSeek V4.1 Flash | `qwen-token-plan/deepseek-v4.1-flash`: removed from the admitted list on 2026-09-27 | `deepseek/deepseek-v4.1-flash` in the OpenRouter feed | Bills a DeepSeek model through Alibaba's plan. `deepseek` is a native-vendor Pi brake, so this is not a route. |
|
|
187
187
|
|
|
188
188
|
The developer runner is stricter. Its only Pi writer is `openrouter/qwen/qwen3.7-plus`, and it does not allowlist `qwen-token-plan` or `zai-coding-cn`. Today OpenRouter is the right answer here for Qwen3.7 Plus, Qwen3.8 Flash and GLM 5.3 Flash. `qwen/qwen3-coder-plus` is retired.
|
|
@@ -1311,8 +1311,13 @@ node --disable-warning=ExperimentalWarning --experimental-strip-types scripts/re
|
|
|
1311
1311
|
`--dry-run` prints the summary and writes nothing. Exit 2 means a referenced
|
|
1312
1312
|
model would be kept after leaving the catalog. The workflow pushes a non-empty
|
|
1313
1313
|
diff to `automation/model-refresh` and opens or updates one pull request with
|
|
1314
|
-
|
|
1315
|
-
|
|
1314
|
+
a GitHub App installation token (`KXM_REFRESH_APP_ID` and
|
|
1315
|
+
`KXM_REFRESH_APP_PRIVATE_KEY`). The App is installed on `kontextmind/kxm`
|
|
1316
|
+
only, with Contents read and write and Pull requests read and write. A missing
|
|
1317
|
+
secret fails the job and names both secrets. Setup is in
|
|
1318
|
+
[Refresh model prices and inventory](../contributing/development.md#refresh-model-prices-and-inventory).
|
|
1319
|
+
`GITHUB_TOKEN` posts the `model-catalog-guard` status and dispatches `ci.yml`
|
|
1320
|
+
on that branch. `ci.yml` does not run on a push to `automation/model-refresh`.
|
|
1316
1321
|
|
|
1317
1322
|
## Personalization settings (`kxm.config.v1`)
|
|
1318
1323
|
|
|
@@ -528,8 +528,9 @@ OpenRouter list prices in `.kxm/prices.yaml` and `.kxm/models/inventory.yaml`
|
|
|
528
528
|
go stale as models are added, removed, and repriced. A Monday 13:00 UTC
|
|
529
529
|
workflow, `.github/workflows/refresh-models.yml`, fetches the public OpenRouter
|
|
530
530
|
models API and opens one pull request on `automation/model-refresh` when the
|
|
531
|
-
catalog changed. The pull request
|
|
532
|
-
|
|
531
|
+
catalog changed. The pull request is opened with a GitHub App token. App
|
|
532
|
+
setup is in [Refresh model prices and inventory](../contributing/development.md#refresh-model-prices-and-inventory).
|
|
533
|
+
The pull request body lists models added, removed, and repriced. A route or roster model that disappeared, or whose input or output
|
|
533
534
|
rate moved by more than 20 percent, is listed first. A disappeared referenced
|
|
534
535
|
model stays in the inventory with `missingUpstream: true`, and the
|
|
535
536
|
`model-catalog-guard` check fails until someone removes the reference or
|
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.193",
|
|
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",
|
|
@@ -25462,7 +25462,7 @@ function sessionTokenFixHint(policy) {
|
|
|
25462
25462
|
}
|
|
25463
25463
|
|
|
25464
25464
|
// plugins/kxm/src/mcp-server.ts
|
|
25465
|
-
var VERSION = "0.7.
|
|
25465
|
+
var VERSION = "0.7.193";
|
|
25466
25466
|
var CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
|
|
25467
25467
|
var inbox = /* @__PURE__ */ new Map();
|
|
25468
25468
|
var notifiedInbox = /* @__PURE__ */ new Set();
|
package/plugins/kxm/package.json
CHANGED
|
@@ -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.
|
|
14
|
+
const VERSION = "0.7.193";
|
|
15
15
|
const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
|
|
16
16
|
const inbox = new Map<string, MessageRecord>();
|
|
17
17
|
const notifiedInbox = new Set<string>();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { isAlias, isMap, isSeq, parse, parseDocument, stringify, type YAMLMap } from "yaml";
|
|
3
|
+
import { isAlias, isMap, isSeq, parse, parseDocument, stringify, type YAMLMap, type YAMLSeq } from "yaml";
|
|
4
4
|
import type { PriceCatalog } from "./price-calc.ts";
|
|
5
5
|
import { hashPriceCatalog, parsePriceCatalog } from "./prices.ts";
|
|
6
6
|
import {
|
|
@@ -131,7 +131,9 @@ function priceRowTokens(row: LoosePriceRow): string[] {
|
|
|
131
131
|
if (row.model.includes("/")) add(row.model);
|
|
132
132
|
else add(`${row.provider}/${row.model}`);
|
|
133
133
|
// Aliases on native and OpenRouter rows point at the catalog id. A different
|
|
134
|
-
// plan (qwen-token-plan, zai-coding-cn) keeps its own rate
|
|
134
|
+
// plan (qwen-token-plan, zai-coding-cn) keeps its own rate. A YAML anchor that
|
|
135
|
+
// a non-OpenRouter row depends on is not rewritten: the OpenRouter row is
|
|
136
|
+
// detached onto its own tier and the plan row keeps the anchored rates.
|
|
135
137
|
const trackAliases = new Set(["anthropic", "openai", "google", "xai", "openrouter"]);
|
|
136
138
|
if (trackAliases.has(row.provider)) {
|
|
137
139
|
for (const alias of row.aliases ?? []) add(alias.includes("/") ? alias : `${row.provider}/${alias}`);
|
|
@@ -250,6 +252,71 @@ function writeTierRates(map: YAMLMap, previous: InventoryPrice, incoming: Invent
|
|
|
250
252
|
return changed;
|
|
251
253
|
}
|
|
252
254
|
|
|
255
|
+
function mapString(row: YAMLMap, key: string): string | undefined {
|
|
256
|
+
const value = row.get(key);
|
|
257
|
+
return typeof value === "string" ? value : undefined;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function soleTier(row: YAMLMap): { tiers: YAMLSeq; tier: unknown } | undefined {
|
|
261
|
+
const tiers = row.get("tiers", true);
|
|
262
|
+
if (!isSeq(tiers) || tiers.items.length !== 1) return undefined;
|
|
263
|
+
return { tiers, tier: tiers.items[0] };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function anchorName(tier: unknown): string | undefined {
|
|
267
|
+
if (isMap(tier) && tier.anchor) return tier.anchor;
|
|
268
|
+
if (isAlias(tier)) return tier.source;
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function nonOpenRouterDependsOn(models: YAMLSeq, anchor: string): boolean {
|
|
273
|
+
for (const row of models.items) {
|
|
274
|
+
if (!isMap(row) || mapString(row, "provider") === "openrouter") continue;
|
|
275
|
+
const sole = soleTier(row);
|
|
276
|
+
if (sole && anchorName(sole.tier) === anchor) return true;
|
|
277
|
+
}
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function rateTierNode(doc: ReturnType<typeof parseDocument>, rates: InventoryPrice, anchor?: string): YAMLMap {
|
|
282
|
+
const node = doc.createNode({
|
|
283
|
+
inputPerMillion: rates.inputPerMillion,
|
|
284
|
+
outputPerMillion: rates.outputPerMillion,
|
|
285
|
+
cacheReadPerMillion: rates.cacheReadPerMillion,
|
|
286
|
+
cacheWritePerMillion: rates.cacheWritePerMillion,
|
|
287
|
+
});
|
|
288
|
+
if (!isMap(node)) throw new Error("price tier must be a map");
|
|
289
|
+
if (anchor) node.anchor = anchor;
|
|
290
|
+
return node;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Gives an OpenRouter row its own tier. The shared anchor, and every
|
|
295
|
+
* non-OpenRouter row that uses it, keep the rates they had.
|
|
296
|
+
*/
|
|
297
|
+
function detachOpenRouterTier(doc: ReturnType<typeof parseDocument>, models: YAMLSeq, row: YAMLMap, anchor: string, incoming: InventoryPrice): boolean {
|
|
298
|
+
const sole = soleTier(row);
|
|
299
|
+
if (!sole) return false;
|
|
300
|
+
const { tiers, tier } = sole;
|
|
301
|
+
if (isAlias(tier)) {
|
|
302
|
+
tiers.items[0] = rateTierNode(doc, incoming);
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
if (!isMap(tier) || tier.anchor !== anchor) return false;
|
|
306
|
+
const previous = tierRates(tier.toJSON() as Record<string, unknown>);
|
|
307
|
+
if (!previous) return false;
|
|
308
|
+
for (const other of models.items) {
|
|
309
|
+
if (!isMap(other) || other === row || mapString(other, "provider") === "openrouter") continue;
|
|
310
|
+
const alias = soleTier(other);
|
|
311
|
+
if (!alias || !isAlias(alias.tier) || alias.tier.source !== anchor) continue;
|
|
312
|
+
delete tier.anchor;
|
|
313
|
+
alias.tiers.items[0] = rateTierNode(doc, previous, anchor);
|
|
314
|
+
tiers.items[0] = rateTierNode(doc, incoming);
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
|
|
253
320
|
function updatePrices(text: string, updates: Map<string, InventoryPrice>, date: string): { text: string; dateChanged: boolean } {
|
|
254
321
|
if (updates.size === 0) return { text, dateChanged: false };
|
|
255
322
|
const doc = parseDocument(text);
|
|
@@ -271,9 +338,14 @@ function updatePrices(text: string, updates: Map<string, InventoryPrice>, date:
|
|
|
271
338
|
if (typeof id !== "string" || !updates.has(id)) continue;
|
|
272
339
|
const incoming = updates.get(id);
|
|
273
340
|
if (!incoming) continue;
|
|
274
|
-
const
|
|
275
|
-
if (!
|
|
276
|
-
const tier =
|
|
341
|
+
const sole = soleTier(row);
|
|
342
|
+
if (!sole) continue;
|
|
343
|
+
const { tier } = sole;
|
|
344
|
+
const name = anchorName(tier);
|
|
345
|
+
if (name && mapString(row, "provider") === "openrouter" && nonOpenRouterDependsOn(models, name)) {
|
|
346
|
+
if (detachOpenRouterTier(doc, models, row, name, incoming)) wrote = true;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
277
349
|
const target = isMap(tier) ? tier : (isAlias(tier) ? maps.get(String(tier.source)) : undefined);
|
|
278
350
|
if (!target || seen.has(target)) continue;
|
|
279
351
|
seen.add(target);
|