@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.
@@ -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.192",
14
+ "version": "0.7.193",
15
15
  "category": "development",
16
16
  "tags": ["kxm", "multi-agent", "workflows", "mcp"]
17
17
  }
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. Uses `GITHUB_TOKEN` only, then dispatches `ci.yml` on that branch |
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-17`. 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`.
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
- `GITHUB_TOKEN`. It then dispatches `ci.yml` on that branch, because a push
1315
- made with `GITHUB_TOKEN` does not start CI by itself.
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 body lists models added, removed, and
532
- repriced. A route or roster model that disappeared, or whose input or output
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontextmind/kxm",
3
- "version": "0.7.192",
3
+ "version": "0.7.193",
4
4
  "description": "KXM local-first multi-agent orchestration and operator dashboard",
5
5
  "type": "module",
6
6
  "author": "KontextMind",
@@ -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.192",
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.192";
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();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin",
3
- "version": "0.7.192",
3
+ "version": "0.7.193",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -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.192";
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 unless it shares a YAML anchor.
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 tiers = row.get("tiers", true);
275
- if (!isSeq(tiers) || tiers.items.length !== 1) continue;
276
- const tier = tiers.items[0];
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);