@kontextmind/kxm 0.7.174 → 0.7.176

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.
@@ -33758,6 +33758,9 @@ var Protocol = class {
33758
33758
  this.setRequestHandler(GetTaskPayloadRequestSchema, async (request, extra) => {
33759
33759
  const handleTaskResult = async () => {
33760
33760
  const taskId = request.params.taskId;
33761
+ if (!await this._taskStore.getTask(taskId, extra.sessionId)) {
33762
+ throw new McpError(ErrorCode.InvalidParams, `Task not found: ${taskId}`);
33763
+ }
33761
33764
  if (this._taskMessageQueue) {
33762
33765
  let queuedMessage;
33763
33766
  while (queuedMessage = await this._taskMessageQueue.dequeue(taskId, extra.sessionId)) {
@@ -33788,12 +33791,12 @@ var Protocol = class {
33788
33791
  throw new McpError(ErrorCode.InvalidParams, `Task not found: ${taskId}`);
33789
33792
  }
33790
33793
  if (!isTerminal(task.status)) {
33791
- await this._waitForTaskUpdate(taskId, extra.signal);
33794
+ await this._waitForTaskUpdate(taskId, extra.signal, extra.sessionId);
33792
33795
  return await handleTaskResult();
33793
33796
  }
33794
33797
  if (isTerminal(task.status)) {
33795
33798
  const result2 = await this._taskStore.getTaskResult(taskId, extra.sessionId);
33796
- this._clearTaskQueue(taskId);
33799
+ this._clearTaskQueue(taskId, extra.sessionId);
33797
33800
  return {
33798
33801
  ...result2,
33799
33802
  _meta: {
@@ -33830,7 +33833,7 @@ var Protocol = class {
33830
33833
  throw new McpError(ErrorCode.InvalidParams, `Cannot cancel task in terminal status: ${task.status}`);
33831
33834
  }
33832
33835
  await this._taskStore.updateTaskStatus(request.params.taskId, "cancelled", "Client cancelled task execution.", extra.sessionId);
33833
- this._clearTaskQueue(request.params.taskId);
33836
+ this._clearTaskQueue(request.params.taskId, extra.sessionId);
33834
33837
  const cancelledTask = await this._taskStore.getTask(request.params.taskId, extra.sessionId);
33835
33838
  if (!cancelledTask) {
33836
33839
  throw new McpError(ErrorCode.InvalidParams, `Task not found after cancellation: ${request.params.taskId}`);
@@ -33958,6 +33961,19 @@ var Protocol = class {
33958
33961
  const handler = this._requestHandlers.get(request.method) ?? this.fallbackRequestHandler;
33959
33962
  const capturedTransport = this._transport;
33960
33963
  const relatedTaskId = request.params?._meta?.[RELATED_TASK_META_KEY]?.taskId;
33964
+ const sessionId = capturedTransport?.sessionId;
33965
+ const store = this._taskStore;
33966
+ let relatedTaskFound = true;
33967
+ let relatedTaskLookup;
33968
+ if (relatedTaskId && store && this._taskMessageQueue && sessionId !== void 0) {
33969
+ relatedTaskFound = false;
33970
+ relatedTaskLookup = (async () => {
33971
+ if (!await store.getTask(relatedTaskId, sessionId)) {
33972
+ throw new McpError(ErrorCode.InvalidParams, `Task not found: ${relatedTaskId}`);
33973
+ }
33974
+ relatedTaskFound = true;
33975
+ })();
33976
+ }
33961
33977
  if (handler === void 0) {
33962
33978
  const errorResponse = {
33963
33979
  jsonrpc: "2.0",
@@ -33967,7 +33983,10 @@ var Protocol = class {
33967
33983
  message: "Method not found"
33968
33984
  }
33969
33985
  };
33970
- if (relatedTaskId && this._taskMessageQueue) {
33986
+ if (relatedTaskId && relatedTaskLookup) {
33987
+ const queuedError = { type: "error", message: errorResponse, timestamp: Date.now() };
33988
+ relatedTaskLookup.then(() => this._enqueueTaskMessage(relatedTaskId, queuedError, sessionId), () => capturedTransport?.send(errorResponse)).catch((error2) => this._onerror(new Error(`Failed to send an error response: ${error2}`)));
33989
+ } else if (relatedTaskId && this._taskMessageQueue) {
33971
33990
  this._enqueueTaskMessage(relatedTaskId, {
33972
33991
  type: "error",
33973
33992
  message: errorResponse,
@@ -34018,7 +34037,10 @@ var Protocol = class {
34018
34037
  closeSSEStream: extra?.closeSSEStream,
34019
34038
  closeStandaloneSSEStream: extra?.closeStandaloneSSEStream
34020
34039
  };
34021
- Promise.resolve().then(() => {
34040
+ (relatedTaskLookup ?? Promise.resolve()).then(() => {
34041
+ if (relatedTaskLookup && abortController.signal.aborted) {
34042
+ throw new McpError(ErrorCode.ConnectionClosed, "Request was cancelled");
34043
+ }
34022
34044
  if (taskCreationParams) {
34023
34045
  this.assertTaskHandlerCapability(request.method);
34024
34046
  }
@@ -34053,7 +34075,7 @@ var Protocol = class {
34053
34075
  ...error2["data"] !== void 0 && { data: error2["data"] }
34054
34076
  }
34055
34077
  };
34056
- if (relatedTaskId && this._taskMessageQueue) {
34078
+ if (relatedTaskId && this._taskMessageQueue && relatedTaskFound) {
34057
34079
  await this._enqueueTaskMessage(relatedTaskId, {
34058
34080
  type: "error",
34059
34081
  message: errorResponse,
@@ -34533,7 +34555,7 @@ var Protocol = class {
34533
34555
  throw new Error("Cannot enqueue task message: taskStore and taskMessageQueue are not configured");
34534
34556
  }
34535
34557
  const maxQueueSize = this._options?.maxTaskQueueSize;
34536
- await this._taskMessageQueue.enqueue(taskId, message, sessionId, maxQueueSize);
34558
+ await this._taskMessageQueue.enqueue(taskId, message, sessionId ?? this._transport?.sessionId, maxQueueSize);
34537
34559
  }
34538
34560
  /**
34539
34561
  * Clears the message queue for a task and rejects any pending request resolvers.
@@ -34562,12 +34584,13 @@ var Protocol = class {
34562
34584
  * Uses polling to check for updates at the task's configured poll interval.
34563
34585
  * @param taskId The task ID to wait for
34564
34586
  * @param signal Abort signal to cancel the wait
34587
+ * @param sessionId Session of the request that waits, passed to the task store
34565
34588
  * @returns Promise that resolves when an update occurs or rejects if aborted
34566
34589
  */
34567
- async _waitForTaskUpdate(taskId, signal) {
34590
+ async _waitForTaskUpdate(taskId, signal, sessionId) {
34568
34591
  let interval = this._options?.defaultTaskPollInterval ?? 1e3;
34569
34592
  try {
34570
- const task = await this._taskStore?.getTask(taskId);
34593
+ const task = await this._taskStore?.getTask(taskId, sessionId);
34571
34594
  if (task?.pollInterval) {
34572
34595
  interval = task.pollInterval;
34573
34596
  }
@@ -35446,6 +35469,42 @@ var ExperimentalMcpServerTasks = class {
35446
35469
  };
35447
35470
 
35448
35471
  // node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js
35472
+ function toolInputElementCount(value, max) {
35473
+ let count = 0;
35474
+ const stack = [value];
35475
+ while (stack.length > 0) {
35476
+ const node = stack.pop();
35477
+ if (node === null || typeof node !== "object")
35478
+ continue;
35479
+ if (Array.isArray(node)) {
35480
+ for (const child of node) {
35481
+ if (++count > max)
35482
+ return count;
35483
+ if (child !== null && typeof child === "object")
35484
+ stack.push(child);
35485
+ }
35486
+ } else {
35487
+ for (const key in node) {
35488
+ if (!Object.prototype.hasOwnProperty.call(node, key))
35489
+ continue;
35490
+ if (++count > max)
35491
+ return count;
35492
+ const child = node[key];
35493
+ if (child !== null && typeof child === "object")
35494
+ stack.push(child);
35495
+ }
35496
+ }
35497
+ }
35498
+ return count;
35499
+ }
35500
+ function resolveMaxToolInputElements(value) {
35501
+ if (value === void 0 || value === Infinity)
35502
+ return void 0;
35503
+ if (typeof value !== "number" || Number.isNaN(value) || value < 1) {
35504
+ throw new RangeError(`maxToolInputElements must be a number of at least 1, or Infinity, got ${String(value)}`);
35505
+ }
35506
+ return value;
35507
+ }
35449
35508
  var McpServer = class {
35450
35509
  constructor(serverInfo, options) {
35451
35510
  this._registeredResources = {};
@@ -35457,6 +35516,7 @@ var McpServer = class {
35457
35516
  this._resourceHandlersInitialized = false;
35458
35517
  this._promptHandlersInitialized = false;
35459
35518
  this.server = new Server(serverInfo, options);
35519
+ this._maxToolInputElements = resolveMaxToolInputElements(options?.maxToolInputElements);
35460
35520
  }
35461
35521
  /**
35462
35522
  * Access experimental features.
@@ -35587,12 +35647,15 @@ var McpServer = class {
35587
35647
  * Validates tool input arguments against the tool's input schema.
35588
35648
  */
35589
35649
  async validateToolInput(tool, args, toolName) {
35650
+ if (this._maxToolInputElements !== void 0 && toolInputElementCount(args, this._maxToolInputElements) > this._maxToolInputElements) {
35651
+ throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for tool ${toolName}: arguments contain more than the maximum of ${this._maxToolInputElements} elements`);
35652
+ }
35590
35653
  if (!tool.inputSchema) {
35591
35654
  return void 0;
35592
35655
  }
35593
35656
  const inputObj = normalizeObjectSchema(tool.inputSchema);
35594
35657
  const schemaToParse = inputObj ?? tool.inputSchema;
35595
- const parseResult = await safeParseAsync2(schemaToParse, args);
35658
+ const parseResult = await safeParseAsync2(schemaToParse, args ?? {});
35596
35659
  if (!parseResult.success) {
35597
35660
  const error2 = "error" in parseResult ? parseResult.error : "Unknown error";
35598
35661
  const errorMessage = getParseErrorMessage(error2);
@@ -35830,7 +35893,7 @@ var McpServer = class {
35830
35893
  }
35831
35894
  if (prompt.argsSchema) {
35832
35895
  const argsObj = normalizeObjectSchema(prompt.argsSchema);
35833
- const parseResult = await safeParseAsync2(argsObj, request.params.arguments);
35896
+ const parseResult = await safeParseAsync2(argsObj, request.params.arguments ?? {});
35834
35897
  if (!parseResult.success) {
35835
35898
  const error2 = "error" in parseResult ? parseResult.error : "Unknown error";
35836
35899
  const errorMessage = getParseErrorMessage(error2);
@@ -21387,6 +21387,9 @@ var Protocol = class {
21387
21387
  this.setRequestHandler(GetTaskPayloadRequestSchema, async (request, extra) => {
21388
21388
  const handleTaskResult = async () => {
21389
21389
  const taskId = request.params.taskId;
21390
+ if (!await this._taskStore.getTask(taskId, extra.sessionId)) {
21391
+ throw new McpError(ErrorCode.InvalidParams, `Task not found: ${taskId}`);
21392
+ }
21390
21393
  if (this._taskMessageQueue) {
21391
21394
  let queuedMessage;
21392
21395
  while (queuedMessage = await this._taskMessageQueue.dequeue(taskId, extra.sessionId)) {
@@ -21417,12 +21420,12 @@ var Protocol = class {
21417
21420
  throw new McpError(ErrorCode.InvalidParams, `Task not found: ${taskId}`);
21418
21421
  }
21419
21422
  if (!isTerminal(task.status)) {
21420
- await this._waitForTaskUpdate(taskId, extra.signal);
21423
+ await this._waitForTaskUpdate(taskId, extra.signal, extra.sessionId);
21421
21424
  return await handleTaskResult();
21422
21425
  }
21423
21426
  if (isTerminal(task.status)) {
21424
21427
  const result = await this._taskStore.getTaskResult(taskId, extra.sessionId);
21425
- this._clearTaskQueue(taskId);
21428
+ this._clearTaskQueue(taskId, extra.sessionId);
21426
21429
  return {
21427
21430
  ...result,
21428
21431
  _meta: {
@@ -21459,7 +21462,7 @@ var Protocol = class {
21459
21462
  throw new McpError(ErrorCode.InvalidParams, `Cannot cancel task in terminal status: ${task.status}`);
21460
21463
  }
21461
21464
  await this._taskStore.updateTaskStatus(request.params.taskId, "cancelled", "Client cancelled task execution.", extra.sessionId);
21462
- this._clearTaskQueue(request.params.taskId);
21465
+ this._clearTaskQueue(request.params.taskId, extra.sessionId);
21463
21466
  const cancelledTask = await this._taskStore.getTask(request.params.taskId, extra.sessionId);
21464
21467
  if (!cancelledTask) {
21465
21468
  throw new McpError(ErrorCode.InvalidParams, `Task not found after cancellation: ${request.params.taskId}`);
@@ -21587,6 +21590,19 @@ var Protocol = class {
21587
21590
  const handler = this._requestHandlers.get(request.method) ?? this.fallbackRequestHandler;
21588
21591
  const capturedTransport = this._transport;
21589
21592
  const relatedTaskId = request.params?._meta?.[RELATED_TASK_META_KEY]?.taskId;
21593
+ const sessionId = capturedTransport?.sessionId;
21594
+ const store = this._taskStore;
21595
+ let relatedTaskFound = true;
21596
+ let relatedTaskLookup;
21597
+ if (relatedTaskId && store && this._taskMessageQueue && sessionId !== void 0) {
21598
+ relatedTaskFound = false;
21599
+ relatedTaskLookup = (async () => {
21600
+ if (!await store.getTask(relatedTaskId, sessionId)) {
21601
+ throw new McpError(ErrorCode.InvalidParams, `Task not found: ${relatedTaskId}`);
21602
+ }
21603
+ relatedTaskFound = true;
21604
+ })();
21605
+ }
21590
21606
  if (handler === void 0) {
21591
21607
  const errorResponse = {
21592
21608
  jsonrpc: "2.0",
@@ -21596,7 +21612,10 @@ var Protocol = class {
21596
21612
  message: "Method not found"
21597
21613
  }
21598
21614
  };
21599
- if (relatedTaskId && this._taskMessageQueue) {
21615
+ if (relatedTaskId && relatedTaskLookup) {
21616
+ const queuedError = { type: "error", message: errorResponse, timestamp: Date.now() };
21617
+ relatedTaskLookup.then(() => this._enqueueTaskMessage(relatedTaskId, queuedError, sessionId), () => capturedTransport?.send(errorResponse)).catch((error2) => this._onerror(new Error(`Failed to send an error response: ${error2}`)));
21618
+ } else if (relatedTaskId && this._taskMessageQueue) {
21600
21619
  this._enqueueTaskMessage(relatedTaskId, {
21601
21620
  type: "error",
21602
21621
  message: errorResponse,
@@ -21647,7 +21666,10 @@ var Protocol = class {
21647
21666
  closeSSEStream: extra?.closeSSEStream,
21648
21667
  closeStandaloneSSEStream: extra?.closeStandaloneSSEStream
21649
21668
  };
21650
- Promise.resolve().then(() => {
21669
+ (relatedTaskLookup ?? Promise.resolve()).then(() => {
21670
+ if (relatedTaskLookup && abortController.signal.aborted) {
21671
+ throw new McpError(ErrorCode.ConnectionClosed, "Request was cancelled");
21672
+ }
21651
21673
  if (taskCreationParams) {
21652
21674
  this.assertTaskHandlerCapability(request.method);
21653
21675
  }
@@ -21682,7 +21704,7 @@ var Protocol = class {
21682
21704
  ...error2["data"] !== void 0 && { data: error2["data"] }
21683
21705
  }
21684
21706
  };
21685
- if (relatedTaskId && this._taskMessageQueue) {
21707
+ if (relatedTaskId && this._taskMessageQueue && relatedTaskFound) {
21686
21708
  await this._enqueueTaskMessage(relatedTaskId, {
21687
21709
  type: "error",
21688
21710
  message: errorResponse,
@@ -22162,7 +22184,7 @@ var Protocol = class {
22162
22184
  throw new Error("Cannot enqueue task message: taskStore and taskMessageQueue are not configured");
22163
22185
  }
22164
22186
  const maxQueueSize = this._options?.maxTaskQueueSize;
22165
- await this._taskMessageQueue.enqueue(taskId, message, sessionId, maxQueueSize);
22187
+ await this._taskMessageQueue.enqueue(taskId, message, sessionId ?? this._transport?.sessionId, maxQueueSize);
22166
22188
  }
22167
22189
  /**
22168
22190
  * Clears the message queue for a task and rejects any pending request resolvers.
@@ -22191,12 +22213,13 @@ var Protocol = class {
22191
22213
  * Uses polling to check for updates at the task's configured poll interval.
22192
22214
  * @param taskId The task ID to wait for
22193
22215
  * @param signal Abort signal to cancel the wait
22216
+ * @param sessionId Session of the request that waits, passed to the task store
22194
22217
  * @returns Promise that resolves when an update occurs or rejects if aborted
22195
22218
  */
22196
- async _waitForTaskUpdate(taskId, signal) {
22219
+ async _waitForTaskUpdate(taskId, signal, sessionId) {
22197
22220
  let interval = this._options?.defaultTaskPollInterval ?? 1e3;
22198
22221
  try {
22199
- const task = await this._taskStore?.getTask(taskId);
22222
+ const task = await this._taskStore?.getTask(taskId, sessionId);
22200
22223
  if (task?.pollInterval) {
22201
22224
  interval = task.pollInterval;
22202
22225
  }
@@ -25438,7 +25461,7 @@ function sessionTokenFixHint(policy) {
25438
25461
  }
25439
25462
 
25440
25463
  // plugins/kxm/src/mcp-server.ts
25441
- var VERSION = "0.7.174";
25464
+ var VERSION = "0.7.176";
25442
25465
  var CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
25443
25466
  var inbox = /* @__PURE__ */ new Map();
25444
25467
  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.174",
3
+ "version": "0.7.176",
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
  });