camstack 1.2.211 → 1.2.212

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.
@@ -17,7 +17,7 @@ import {
17
17
  prepareRecipeUnits,
18
18
  unitsForQuantity,
19
19
  validateRecipeBounds
20
- } from "./chunk-NAUJQNCX.js";
20
+ } from "./chunk-MLWXP2SI.js";
21
21
  import "./chunk-LMMQX4CK.js";
22
22
 
23
23
  // src/mcp/server.ts
@@ -27,10 +27,10 @@ import { MutationGate, tryConnectHub } from "@camstack/develop";
27
27
 
28
28
  // src/mcp/configure-tools.ts
29
29
  import {
30
- runGated as runGated3,
31
- text as text11
30
+ runGated as runGated4,
31
+ text as text12
32
32
  } from "@camstack/develop";
33
- import { z as z19 } from "zod";
33
+ import { z as z26 } from "zod";
34
34
 
35
35
  // src/mcp/configure-automations.ts
36
36
  import { z as z4 } from "zod";
@@ -3730,7 +3730,7 @@ function addCameraSpec(client) {
3730
3730
  const plan = await planAdd(client, args);
3731
3731
  if (!plan.ok) throw new Error(plan.problems.join("; "));
3732
3732
  const link = await providerPage(client, args.addonId);
3733
- const needs = plan.secretNeeds.map((f) => secretRequired(f, link));
3733
+ const needs2 = plan.secretNeeds.map((f) => secretRequired(f, link));
3734
3734
  let raw;
3735
3735
  try {
3736
3736
  raw = await client.call("POST", "deviceManager.createDevice", {
@@ -3742,7 +3742,7 @@ function addCameraSpec(client) {
3742
3742
  return {
3743
3743
  created: false,
3744
3744
  adminUi: link,
3745
- incompleteUntilSecretSet: needs,
3745
+ incompleteUntilSecretSet: needs2,
3746
3746
  error: reasonOf(err)
3747
3747
  };
3748
3748
  }
@@ -3751,7 +3751,7 @@ function addCameraSpec(client) {
3751
3751
  return {
3752
3752
  created: false,
3753
3753
  adminUi: link,
3754
- incompleteUntilSecretSet: needs,
3754
+ incompleteUntilSecretSet: needs2,
3755
3755
  error: "the hub answered in an unexpected shape; check the Devices page before retrying"
3756
3756
  };
3757
3757
  }
@@ -3761,7 +3761,7 @@ function addCameraSpec(client) {
3761
3761
  deviceId: created.data.id,
3762
3762
  name: created.data.name,
3763
3763
  adminUi: base.base === null ? null : `${base.base}/devices/${String(created.data.id)}`,
3764
- incompleteUntilSecretSet: needs
3764
+ incompleteUntilSecretSet: needs2
3765
3765
  };
3766
3766
  }
3767
3767
  };
@@ -4050,12 +4050,12 @@ function testTargetSpec(client) {
4050
4050
  if (view.incomplete.length > 0) {
4051
4051
  const form = kinds.ok ? kinds.value.find((k) => k.kind === target.kind) : void 0;
4052
4052
  const link = await notificationsPage(client);
4053
- const needs = (form?.fields ?? []).filter((f) => view.incomplete.includes(f.key)).map((f) => secretRequired(f, link));
4053
+ const needs2 = (form?.fields ?? []).filter((f) => view.incomplete.includes(f.key)).map((f) => secretRequired(f, link));
4054
4054
  return {
4055
4055
  confirmable: false,
4056
4056
  describe: {
4057
4057
  problem: `target ${target.id} is incomplete until its secret is set`,
4058
- secretRequired: needs
4058
+ secretRequired: needs2
4059
4059
  }
4060
4060
  };
4061
4061
  }
@@ -4150,8 +4150,8 @@ function registerNotifierTools(server, ctx) {
4150
4150
  }
4151
4151
 
4152
4152
  // src/mcp/ops-tools.ts
4153
- import { z as z18 } from "zod";
4154
- import { jwtClaims, text as text10 } from "@camstack/develop";
4153
+ import { z as z25 } from "zod";
4154
+ import { jwtClaims, text as text11 } from "@camstack/develop";
4155
4155
 
4156
4156
  // src/mcp/ops-accelerators.ts
4157
4157
  import { z as z14 } from "zod";
@@ -5040,195 +5040,189 @@ async function opsStatus(ctx) {
5040
5040
  });
5041
5041
  }
5042
5042
 
5043
- // src/mcp/ops-tools.ts
5044
- var IDENTITY_CLAIMS = ["sub", "username", "name", "role"];
5045
- function identityOf(token) {
5046
- const claims = jwtClaims(token);
5047
- const out = {};
5048
- if (claims === null) return out;
5049
- for (const key of IDENTITY_CLAIMS) {
5050
- const value = claims[key];
5051
- if (typeof value === "string" && value !== "") out[key] = value;
5052
- }
5053
- return out;
5043
+ // src/mcp/ops-update-apply.ts
5044
+ import {
5045
+ runGated as runGated3
5046
+ } from "@camstack/develop";
5047
+ import { z as z23 } from "zod";
5048
+
5049
+ // src/mcp/ops-update-model.ts
5050
+ import { z as z18 } from "zod";
5051
+ var ImageContractShape = z18.looseObject({
5052
+ state: z18.enum(["in-sync", "behind-patch", "behind-series", "ahead", "unknown"]),
5053
+ seedVersion: z18.string().nullable(),
5054
+ contractVersion: z18.string().nullable(),
5055
+ message: z18.string().optional()
5056
+ });
5057
+ var WrapperShape = z18.looseObject({
5058
+ kind: z18.enum(["docker", "electron", "native", "contradictory", "unknown"]),
5059
+ currentVersion: z18.string().nullable().optional()
5060
+ });
5061
+ var ServerStatusShape2 = z18.looseObject({
5062
+ runningVersion: z18.string().nullable(),
5063
+ installedVersion: z18.string().nullable().optional(),
5064
+ latestVersion: z18.string().nullable().optional(),
5065
+ pendingVersion: z18.string().nullable().optional(),
5066
+ restartRequired: z18.boolean().optional(),
5067
+ updateAvailable: z18.boolean().optional(),
5068
+ updateState: z18.string().optional(),
5069
+ lastCheckedAtMs: z18.number().nullable().optional(),
5070
+ rolledBack: z18.looseObject({
5071
+ fromVersion: z18.string(),
5072
+ toVersion: z18.string().nullable(),
5073
+ reason: z18.string()
5074
+ }).nullable().optional(),
5075
+ imageContract: ImageContractShape.optional(),
5076
+ wrapper: WrapperShape.optional()
5077
+ });
5078
+ var AddonUpdateShape = z18.looseObject({
5079
+ name: z18.string(),
5080
+ currentVersion: z18.string(),
5081
+ latestVersion: z18.string(),
5082
+ category: z18.enum(["addon", "core"]),
5083
+ requiresRestart: z18.boolean().optional(),
5084
+ isSystem: z18.boolean()
5085
+ });
5086
+ var InstallationsShape2 = z18.looseObject({
5087
+ installations: z18.array(
5088
+ z18.looseObject({
5089
+ packageName: z18.string(),
5090
+ nodeId: z18.string(),
5091
+ isHub: z18.boolean(),
5092
+ isOnline: z18.boolean(),
5093
+ version: z18.string().nullable(),
5094
+ onHub: z18.boolean().optional()
5095
+ })
5096
+ ),
5097
+ unreachable: z18.array(z18.looseObject({ nodeId: z18.string(), error: z18.string() }))
5098
+ });
5099
+ var ServerActionShape = z18.looseObject({
5100
+ accepted: z18.boolean().optional(),
5101
+ outcome: z18.enum(["staged", "restart-scheduled", "already-current", "refused", "failed"]).optional(),
5102
+ reason: z18.string().nullable().optional(),
5103
+ error: z18.string().nullable().optional(),
5104
+ message: z18.string().optional(),
5105
+ blockers: z18.array(z18.looseObject({ addonId: z18.string(), reason: z18.string() })).optional()
5106
+ });
5107
+ function statusInput(node) {
5108
+ return node.isHub === true ? void 0 : { nodeId: node.id };
5054
5109
  }
5055
- async function whoami(ctx) {
5056
- const outcome = await ctx.hub();
5057
- if (!outcome.ok) return noSessionResult(outcome);
5058
- const c = outcome.connection;
5059
- return {
5060
- status: "ok",
5061
- hub: c.server,
5062
- tier: c.tier,
5063
- source: c.source,
5064
- identity: identityOf(c.token)
5065
- };
5110
+ function readTopology(client) {
5111
+ return readQuery(client, "nodes.topology", void 0, TopologySchema);
5066
5112
  }
5067
- function registerOpsTools(server, ctx) {
5068
- server.tool(
5069
- "ops_whoami",
5070
- "Which hub this MCP talks to and as whom, from the `camstack login` session (no network call). If there is no session, says to run `camstack login <host>` in a terminal.",
5071
- {},
5072
- async () => text10(await whoami(ctx))
5073
- );
5074
- server.tool(
5075
- "ops_status",
5076
- 'Cluster summary: every node (online, platform, server version, capability count, "online-but-capless" flag), the active alerts and the addons with an update. A part the hub did not answer is null with its reason in `unavailable`, never zero. Node list and updates need an admin `camstack login`.',
5077
- {},
5078
- async () => text10(await opsStatus(ctx))
5079
- );
5080
- server.tool(
5081
- "ops_device",
5082
- "One device: online state, provider addon and node, bound capabilities, runtime-state age and, for a camera, its pipeline, recording state, the functions an operator switched off (not faults) and its latest object events.",
5083
- { deviceId: z18.number().int() },
5084
- async ({ deviceId }) => text10(await opsDevice(ctx, deviceId))
5085
- );
5086
- server.tool(
5087
- "ops_unhealthy_devices",
5088
- "Every enabled device that is unhealthy, each with a typed reason (offline, detection-provisioning-failed, detection-not-assigned, recording-inactive). A function an operator switched off is never listed; a camera whose status could not be fully read is listed under `unknown`.",
5089
- {},
5090
- async () => text10(await opsUnhealthyDevices(ctx))
5091
- );
5092
- server.tool(
5093
- "ops_logs",
5094
- "Hub logs scoped by deviceId, addon, node, level, window and free text, grouped and deduplicated with counts. Says which tier answered (ring = recent in-RAM; persisted = the default log destination). With `around` (a track or event id) returns `contextLines` lines on each side of its first mention. Needs an admin `camstack login`.",
5095
- {
5096
- deviceId: z18.number().int().optional(),
5097
- addonId: z18.string().optional(),
5098
- nodeId: z18.string().optional(),
5099
- level: z18.enum(["debug", "info", "warn", "error"]).optional(),
5100
- windowMinutes: z18.number().int().min(1).max(1440).optional(),
5101
- text: z18.string().max(200).optional(),
5102
- around: z18.string().max(200).optional(),
5103
- contextLines: z18.number().int().min(1).max(50).optional()
5104
- },
5105
- async (args) => text10(
5106
- await opsLogs(ctx, {
5107
- ...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
5108
- ...args.addonId ? { addonId: args.addonId } : {},
5109
- ...args.nodeId ? { nodeId: args.nodeId } : {},
5110
- ...args.level ? { level: args.level } : {},
5111
- ...args.windowMinutes !== void 0 ? { windowMinutes: args.windowMinutes } : {},
5112
- ...args.text ? { text: args.text } : {},
5113
- ...args.around ? { around: args.around } : {},
5114
- ...args.contextLines !== void 0 ? { contextLines: args.contextLines } : {}
5115
- })
5116
- )
5117
- );
5118
- server.tool(
5119
- "ops_events",
5120
- "Find tracks (and, for one camera, object events) by camera, class, zone, time window and free text. Each result carries an admin-UI link and a viewer link built on the hub address flagged for app connections.",
5121
- {
5122
- deviceId: z18.number().int().optional(),
5123
- className: z18.string().optional(),
5124
- zone: z18.string().optional(),
5125
- text: z18.string().max(200).optional(),
5126
- windowMinutes: z18.number().int().min(1).max(10080).optional(),
5127
- limit: z18.number().int().min(1).max(50).optional()
5128
- },
5129
- async (args) => text10(
5130
- await opsEvents(ctx, {
5131
- ...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
5132
- ...args.className ? { className: args.className } : {},
5133
- ...args.zone ? { zone: args.zone } : {},
5134
- ...args.text ? { text: args.text } : {},
5135
- ...args.windowMinutes !== void 0 ? { windowMinutes: args.windowMinutes } : {},
5136
- ...args.limit !== void 0 ? { limit: args.limit } : {}
5137
- })
5138
- )
5139
- );
5140
- server.tool(
5141
- "ops_track",
5142
- "One track with its media audit: best shot, keyframes, every missing media kind with the reason the track recorded for it, and the audit findings. Links to the admin UI and the viewer.",
5143
- { deviceId: z18.number().int(), trackId: z18.string().min(1) },
5144
- async ({ deviceId, trackId }) => text10(await opsTrack(ctx, { deviceId, trackId }))
5145
- );
5146
- server.tool(
5147
- "ops_accelerators",
5148
- 'Per node: the inference devices it has, the Auto-tune hardware class the node maps to, and whether the measured detector-latency table has a row for that class and accelerator. A node that could not be read is null with a reason, never "no accelerator". Needs an admin `camstack login`.',
5149
- {},
5150
- async () => text10(await opsAccelerators(ctx))
5113
+ function readNodeStatus(client, node) {
5114
+ return readQuery(
5115
+ client,
5116
+ "serverManagement.getServerPackageStatus",
5117
+ statusInput(node),
5118
+ ServerStatusShape2
5151
5119
  );
5152
5120
  }
5153
-
5154
- // src/mcp/configure-tools.ts
5155
- var CurrentDeviceSchema = z19.object({ name: z19.string() }).nullable();
5156
- var RENAME_DEVICE_TOOL = "configure_rename_device";
5157
- function renameDeviceSpec(client) {
5121
+ function readInstallations(client) {
5122
+ return readQuery(client, "addons.listInstallations", {}, InstallationsShape2);
5123
+ }
5124
+ function serverVerdict(status) {
5158
5125
  return {
5159
- tool: RENAME_DEVICE_TOOL,
5160
- async dryRun(args) {
5161
- const current = CurrentDeviceSchema.safeParse(
5162
- await client.call("GET", "deviceManager.getDevice", {
5163
- deviceId: args.deviceId
5164
- })
5165
- );
5166
- if (!current.success || current.data === null) {
5167
- return {
5168
- confirmable: false,
5169
- describe: {
5170
- problem: `device ${args.deviceId} was not found on the hub`
5171
- }
5172
- };
5173
- }
5174
- if (current.data.name === args.name) {
5175
- return {
5176
- confirmable: false,
5177
- describe: {
5178
- problem: `device ${args.deviceId} is already named "${args.name}"`
5179
- }
5180
- };
5181
- }
5182
- return {
5183
- confirmable: true,
5184
- describe: {
5185
- action: "rename device",
5186
- deviceId: args.deviceId,
5187
- from: current.data.name,
5188
- to: args.name
5189
- }
5190
- };
5191
- },
5192
- async execute(args) {
5193
- await client.call("POST", "deviceManager.setName", {
5194
- deviceId: args.deviceId,
5195
- name: args.name
5196
- });
5197
- return { deviceId: args.deviceId, name: args.name };
5198
- }
5126
+ running: status.runningVersion,
5127
+ installed: status.installedVersion ?? null,
5128
+ latest: status.latestVersion ?? null,
5129
+ pending: status.pendingVersion ?? null,
5130
+ updateAvailable: status.updateAvailable ?? null,
5131
+ restartRequired: status.restartRequired ?? null,
5132
+ updateState: status.updateState ?? null,
5133
+ rolledBack: status.rolledBack ? {
5134
+ from: status.rolledBack.fromVersion,
5135
+ to: status.rolledBack.toVersion,
5136
+ reason: status.rolledBack.reason
5137
+ } : null,
5138
+ latestCheckedAt: status.lastCheckedAtMs !== void 0 && status.lastCheckedAtMs !== null ? new Date(status.lastCheckedAtMs).toISOString() : null
5199
5139
  };
5200
5140
  }
5201
- async function renameDevice(ctx, args, confirmToken) {
5202
- const outcome = await ctx.hub();
5203
- if (!outcome.ok) return noSessionResult(outcome);
5204
- if (outcome.connection.tier !== "admin") {
5141
+ function wrapperInstructions(kind) {
5142
+ switch (kind) {
5143
+ case "docker":
5144
+ return [
5145
+ "The image is replaced, not the closure: pull the new image and RECREATE the container (a plain restart keeps the old image).",
5146
+ "Docker Compose: `docker compose pull` then `docker compose up -d`.",
5147
+ 'Unraid: Docker tab, "Check for updates", then apply the update on the CamStack container (it is recreated from the new image; keep the same appdata paths).',
5148
+ 'Proxmox LXC: recreate the container the way it was installed; `setup_install_guide` with hostKind "proxmox-lxc" returns the official steps.',
5149
+ "Back up the data volume first; no tool here does it."
5150
+ ];
5151
+ case "electron":
5152
+ return [
5153
+ "Update the CamStack desktop app on that machine: install the new app version (the in-app update, or reinstall from the release) and let the app restart.",
5154
+ "The server closure update does not change the app bundle."
5155
+ ];
5156
+ case "native":
5157
+ return [
5158
+ "This node runs as a bare host process: update the package or service that starts it (the starter is not delivered by the closure update), then restart the service."
5159
+ ];
5160
+ default:
5161
+ return [
5162
+ "The shell this node runs in could not be identified; check how it was installed (`setup_install_guide` lists the routes) and update it that way."
5163
+ ];
5164
+ }
5165
+ }
5166
+ function wrapperVerdict(status) {
5167
+ const kind = status.wrapper?.kind ?? null;
5168
+ const contract = status.imageContract;
5169
+ if (contract === void 0) {
5205
5170
  return {
5206
- status: "refused",
5207
- reason: outcome.connection.adminHint ?? "this credential is not admin-scoped"
5171
+ kind,
5172
+ behind: null,
5173
+ state: null,
5174
+ from: null,
5175
+ to: null,
5176
+ message: "this node reports no image contract (its server predates it); the shell is unverified",
5177
+ instructions: []
5208
5178
  };
5209
5179
  }
5210
- return runGated3(ctx.gate, renameDeviceSpec(outcome.connection.client), args, confirmToken);
5180
+ const behind = contract.state === "behind-patch" || contract.state === "behind-series" ? true : contract.state === "in-sync" ? false : null;
5181
+ return {
5182
+ kind,
5183
+ behind,
5184
+ state: contract.state,
5185
+ from: contract.seedVersion,
5186
+ to: contract.contractVersion,
5187
+ message: contract.message ?? null,
5188
+ instructions: behind === true ? wrapperInstructions(kind) : []
5189
+ };
5211
5190
  }
5212
- function registerConfigureTools(server, ctx) {
5213
- server.tool(
5214
- RENAME_DEVICE_TOOL,
5215
- "Rename a device. Two steps: call without confirm_token for a preview and a one-shot token, then call again with the SAME arguments and that token to apply. Needs an admin `camstack login` session.",
5216
- {
5217
- deviceId: z19.number().int(),
5218
- name: z19.string().min(1),
5219
- confirm_token: z19.string().optional()
5220
- },
5221
- async ({ deviceId, name, confirm_token }) => text11(await renameDevice(ctx, { deviceId, name }, confirm_token))
5222
- );
5223
- registerAddonTools(server, ctx);
5224
- registerCameraTools(server, ctx);
5225
- registerNotifierTools(server, ctx);
5226
- registerConfigureP3bTools(server, ctx);
5191
+ function needsServerUpdate(verdict) {
5192
+ return verdict.updateAvailable === true || verdict.restartRequired === true;
5193
+ }
5194
+ function capState(node) {
5195
+ const count = distinctCaps(node);
5196
+ return { count, capless: isCapless(node, count) };
5197
+ }
5198
+ function addonDrift(installations) {
5199
+ const hubVersion = /* @__PURE__ */ new Map();
5200
+ for (const row of installations.installations) {
5201
+ if (row.isHub && row.version !== null) hubVersion.set(row.packageName, row.version);
5202
+ }
5203
+ const drift = [];
5204
+ for (const row of installations.installations) {
5205
+ if (row.isHub) continue;
5206
+ const hub = hubVersion.get(row.packageName);
5207
+ if (hub === void 0) continue;
5208
+ if (row.version !== hub) {
5209
+ drift.push({
5210
+ packageName: row.packageName,
5211
+ nodeId: row.nodeId,
5212
+ hubVersion: hub,
5213
+ nodeVersion: row.version
5214
+ });
5215
+ }
5216
+ }
5217
+ return drift;
5227
5218
  }
5228
5219
 
5220
+ // src/mcp/ops-update-cli.ts
5221
+ import { execFile } from "child_process";
5222
+
5229
5223
  // src/mcp/setup-tools.ts
5230
- import { text as text12 } from "@camstack/develop";
5231
- import { z as z23 } from "zod";
5224
+ import { text as text10 } from "@camstack/develop";
5225
+ import { z as z22 } from "zod";
5232
5226
 
5233
5227
  // src/mcp/setup-hubs.ts
5234
5228
  import * as https from "https";
@@ -5241,13 +5235,13 @@ import {
5241
5235
  normalizeHubAddress,
5242
5236
  resolveHubTls
5243
5237
  } from "@camstack/develop";
5244
- import { z as z22 } from "zod";
5238
+ import { z as z21 } from "zod";
5245
5239
 
5246
5240
  // src/mcp/setup-install.ts
5247
- import { z as z21 } from "zod";
5241
+ import { z as z20 } from "zod";
5248
5242
 
5249
5243
  // src/mcp/setup-install-rules.ts
5250
- import { z as z20 } from "zod";
5244
+ import { z as z19 } from "zod";
5251
5245
  var HOST_KINDS = [
5252
5246
  "docker-compose",
5253
5247
  "docker",
@@ -5255,24 +5249,24 @@ var HOST_KINDS = [
5255
5249
  "proxmox-lxc",
5256
5250
  "macos-desktop"
5257
5251
  ];
5258
- var HostKindSchema = z20.enum(HOST_KINDS);
5252
+ var HostKindSchema = z19.enum(HOST_KINDS);
5259
5253
  var ROLES = ["hub", "agent"];
5260
- var RoleSchema = z20.enum(ROLES);
5254
+ var RoleSchema = z19.enum(ROLES);
5261
5255
  var ACCELERATORS = ["intel", "amd", "coral", "apple", "cpu"];
5262
- var AcceleratorSchema = z20.enum(ACCELERATORS);
5263
- var InstallPathsSchema = z20.object({
5264
- data: z20.string().min(1),
5265
- recordings: z20.string().min(1),
5266
- config: z20.string().min(1).optional(),
5267
- backups: z20.string().min(1).optional()
5256
+ var AcceleratorSchema = z19.enum(ACCELERATORS);
5257
+ var InstallPathsSchema = z19.object({
5258
+ data: z19.string().min(1),
5259
+ recordings: z19.string().min(1),
5260
+ config: z19.string().min(1).optional(),
5261
+ backups: z19.string().min(1).optional()
5268
5262
  });
5269
- var InstallGuideInputSchema = z20.object({
5263
+ var InstallGuideInputSchema = z19.object({
5270
5264
  hostKind: HostKindSchema.optional(),
5271
5265
  role: RoleSchema.optional(),
5272
5266
  accelerator: AcceleratorSchema.optional(),
5273
5267
  paths: InstallPathsSchema.optional(),
5274
5268
  /** Agents only: the hub the agent should dial (host or IP). */
5275
- hubAddress: z20.string().min(1).optional()
5269
+ hubAddress: z19.string().min(1).optional()
5276
5270
  });
5277
5271
  var RAW = "https://raw.githubusercontent.com/camstack";
5278
5272
  var TEMPLATE_URLS = {
@@ -5532,9 +5526,9 @@ async function fetchText(url, fetchFn) {
5532
5526
  return { ok: false, failure: { status: "fetch-failed", url, error } };
5533
5527
  }
5534
5528
  }
5535
- var ReleaseSchema = z21.object({
5536
- tag_name: z21.string(),
5537
- assets: z21.array(z21.object({ name: z21.string(), browser_download_url: z21.string() }))
5529
+ var ReleaseSchema = z20.object({
5530
+ tag_name: z20.string(),
5531
+ assets: z20.array(z20.object({ name: z20.string(), browser_download_url: z20.string() }))
5538
5532
  });
5539
5533
  var DMG = /^CamStack-[0-9][0-9A-Za-z.-]*-(arm64|x64)\.dmg$/;
5540
5534
  function acceleratorEdits(accelerator, role) {
@@ -5825,15 +5819,15 @@ async function connectTool(server, deps) {
5825
5819
  next: session === null ? LOGIN(url) : "a session exists: the ops tools can use it"
5826
5820
  };
5827
5821
  }
5828
- var DiscoveredAgentsSchema = z22.object({
5829
- agents: z22.array(
5830
- z22.object({
5831
- nodeId: z22.string(),
5832
- host: z22.string(),
5833
- port: z22.number(),
5834
- version: z22.string().nullable(),
5835
- name: z22.string().nullable(),
5836
- state: z22.enum(["unpaired", "pending", "paired"])
5822
+ var DiscoveredAgentsSchema = z21.object({
5823
+ agents: z21.array(
5824
+ z21.object({
5825
+ nodeId: z21.string(),
5826
+ host: z21.string(),
5827
+ port: z21.number(),
5828
+ version: z21.string().nullable(),
5829
+ name: z21.string().nullable(),
5830
+ state: z21.enum(["unpaired", "pending", "paired"])
5837
5831
  })
5838
5832
  )
5839
5833
  });
@@ -5880,7 +5874,7 @@ async function addNodeTool(input, ctx) {
5880
5874
  var SUPPORTED_HOST_KINDS = HOST_KINDS;
5881
5875
  var REGISTRY2 = "https://registry.npmjs.org";
5882
5876
  var REGISTRY_TIMEOUT_MS2 = 5e3;
5883
- var RegistryLatestSchema = z23.object({ version: z23.string() });
5877
+ var RegistryLatestSchema = z22.object({ version: z22.string() });
5884
5878
  async function latestOnNpm(name, fetchFn) {
5885
5879
  const url = `${REGISTRY2}/${name.replace("/", "%2f")}/latest`;
5886
5880
  try {
@@ -5914,7 +5908,7 @@ function registerSetupTools(server, ctx, deps = {}) {
5914
5908
  "setup_about",
5915
5909
  "Versions of the camstack CLI (installed and latest on npm), the latest @camstack/server on npm, and the host kinds CamStack supports. Needs no hub and no login.",
5916
5910
  {},
5917
- async () => text12(await about(ctx))
5911
+ async () => text10(await about(ctx))
5918
5912
  );
5919
5913
  server.tool(
5920
5914
  "setup_install_guide",
@@ -5924,21 +5918,21 @@ function registerSetupTools(server, ctx, deps = {}) {
5924
5918
  role: RoleSchema.optional(),
5925
5919
  accelerator: AcceleratorSchema.optional(),
5926
5920
  paths: InstallPathsSchema.optional(),
5927
- hubAddress: z23.string().min(1).optional().describe("Agents only: the hub to dial.")
5921
+ hubAddress: z22.string().min(1).optional().describe("Agents only: the hub to dial.")
5928
5922
  },
5929
- async (input) => text12(await installGuide(input, ctx.fetch))
5923
+ async (input) => text10(await installGuide(input, ctx.fetch))
5930
5924
  );
5931
5925
  server.tool(
5932
5926
  "setup_discover_hubs",
5933
5927
  "Find CamStack hubs on the local network (mDNS) plus the hubs this machine already knows, with host, port, name and version. Needs no login.",
5934
- { timeoutMs: z23.number().int().min(200).max(15e3).optional() },
5935
- async ({ timeoutMs }) => text12(await discoverHubsTool(timeoutMs, deps))
5928
+ { timeoutMs: z22.number().int().min(200).max(15e3).optional() },
5929
+ async ({ timeoutMs }) => text10(await discoverHubsTool(timeoutMs, deps))
5936
5930
  );
5937
5931
  server.tool(
5938
5932
  "setup_connect",
5939
5933
  "Check that a hub answers on https://<server>:4443/health, whether its CA is already pinned for TLS, and whether a `camstack login` session exists. Never takes a password: when there is no session it says to run `camstack login <server>` in a terminal.",
5940
- { server: z23.string().min(1) },
5941
- async ({ server: address }) => text12(await connectTool(address, deps))
5934
+ { server: z22.string().min(1) },
5935
+ async ({ server: address }) => text10(await connectTool(address, deps))
5942
5936
  );
5943
5937
  server.tool(
5944
5938
  "setup_add_node",
@@ -5947,90 +5941,821 @@ function registerSetupTools(server, ctx, deps = {}) {
5947
5941
  hostKind: HostKindSchema.optional(),
5948
5942
  accelerator: AcceleratorSchema.optional(),
5949
5943
  paths: InstallPathsSchema.optional(),
5950
- hubAddress: z23.string().min(1).optional()
5944
+ hubAddress: z22.string().min(1).optional()
5951
5945
  },
5952
- async (input) => text12(await addNodeTool(input, ctx))
5946
+ async (input) => text10(await addNodeTool(input, ctx))
5953
5947
  );
5954
5948
  }
5955
5949
 
5956
- // src/mcp/support-tools.ts
5957
- import { text as text13 } from "@camstack/develop";
5958
- import { z as z28 } from "zod";
5959
-
5960
- // src/mcp/support-accelerator.ts
5961
- import { z as z24 } from "zod";
5962
- var EXPECTED_ACCELERATORS = [
5963
- "intel-igpu",
5964
- "intel-npu",
5965
- "amd-webgpu",
5966
- "amd-vaapi",
5967
- "amd-npu",
5968
- "coral-usb",
5969
- "coral-pcie",
5970
- "apple-ane",
5971
- "nvidia"
5972
- ];
5973
- var ExpectedAcceleratorSchema = z24.enum(EXPECTED_ACCELERATORS);
5974
- var HardwareSchema2 = z24.looseObject({
5975
- platform: z24.enum(["darwin", "linux", "win32"]),
5976
- arch: z24.enum(["arm64", "x64"]),
5977
- cpuModel: z24.string(),
5978
- gpu: z24.looseObject({ type: z24.string(), name: z24.string() }).nullable(),
5979
- npu: z24.looseObject({ type: z24.string() }).nullable()
5980
- });
5981
- var InferenceDevicesSchema2 = z24.looseObject({
5982
- reachable: z24.boolean(),
5983
- devices: z24.array(
5984
- z24.looseObject({
5985
- key: z24.string(),
5986
- available: z24.boolean(),
5987
- presence: z24.enum(["present", "absent", "unknown"]).optional(),
5988
- enabled: z24.boolean(),
5989
- exclusion: z24.string().nullable()
5990
- })
5991
- )
5950
+ // src/mcp/ops-update-cli.ts
5951
+ var CLI_PACKAGE = "camstack";
5952
+ var NPM_GLOBAL = /[\\/]lib[\\/]node_modules[\\/]camstack[\\/]|[\\/]npm[\\/]node_modules[\\/]camstack[\\/]/;
5953
+ var OTHER_MANAGER = /[\\/]\.bun[\\/]|[\\/]pnpm[\\/]|[\\/]\.yarn[\\/]|[\\/]yarn[\\/]|[\\/]\.volta[\\/]/;
5954
+ function detectCliInstall(entryPath) {
5955
+ const manual = `Update it with the package manager it was installed with, for example \`npm install -g ${CLI_PACKAGE}@latest\` (no sudo: fix the npm prefix permissions instead).`;
5956
+ if (/[\\/]_npx[\\/]/.test(entryPath)) {
5957
+ return {
5958
+ method: "npx",
5959
+ command: null,
5960
+ instructions: `The CLI runs through npx: there is nothing installed to update. Start it as \`npx ${CLI_PACKAGE}@latest mcp\` (or clear the npx cache), then restart the MCP in your client.`
5961
+ };
5962
+ }
5963
+ if (OTHER_MANAGER.test(entryPath) || !NPM_GLOBAL.test(entryPath)) {
5964
+ return { method: "unknown", command: null, instructions: manual };
5965
+ }
5966
+ return {
5967
+ method: "npm-global",
5968
+ command: ["npm", "install", "-g", `${CLI_PACKAGE}@latest`],
5969
+ instructions: `npm install -g ${CLI_PACKAGE}@latest`
5970
+ };
5971
+ }
5972
+ var runCommand = (file, args) => new Promise((resolve) => {
5973
+ execFile(file, [...args], { timeout: 5 * 6e4 }, (err, stdout, stderr) => {
5974
+ const out = `${stdout}${stderr}`.trim().slice(-600);
5975
+ resolve({
5976
+ ok: err === null,
5977
+ output: err ? `${err.message} ${out}`.trim() : out
5978
+ });
5979
+ });
5992
5980
  });
5993
- var CORAL_OPT_IN = [
5994
- "A Coral is never picked automatically (D39): it must be enabled by an operator.",
5995
- "In the admin UI open the node, Inference devices, and enable the Coral (edgetpu) device; Auto tune only sizes devices that are already enabled."
5996
- ];
5997
- var EXPECTATIONS = {
5998
- "intel-igpu": {
5999
- deviceKeys: (k) => k === "openvino:gpu",
6000
- probe: (hw) => hw.gpu?.type === "intel",
6001
- image: "intel",
6002
- steps: [
6003
- 'Pass the render node into the container: --device /dev/dri (Compose: devices: ["/dev/dri:/dev/dri"]).',
6004
- "On the host, `ls -l /dev/dri` must list renderD128; if it does not, the i915/xe driver is not loaded on the host.",
6005
- "Use the intel-latest (or intel-<version>) image: the AMD image carries no OpenVINO GPU runtime."
6006
- ]
6007
- },
6008
- "intel-npu": {
6009
- deviceKeys: (k) => k === "openvino:npu",
6010
- probe: (hw) => hw.npu?.type === "intel-npu",
6011
- image: "intel",
6012
- steps: [
6013
- 'Pass the accelerator node into the container: --device /dev/accel (Compose: devices: ["/dev/accel:/dev/accel"]).',
6014
- "On the host, `ls -l /dev/accel` must list accel0; if it does not, the intel_vpu kernel driver is missing (needs kernel 6.6 or newer and the NPU firmware).",
6015
- "Use the intel-latest (or intel-<version>) image."
6016
- ]
6017
- },
6018
- "amd-webgpu": {
6019
- deviceKeys: (k) => k === "onnx:webgpu",
6020
- probe: (hw) => hw.gpu?.type === "amd",
6021
- image: "amd",
6022
- steps: [
6023
- "Pass the render node into the container: --device /dev/dri.",
6024
- "On the host, `ls -l /dev/dri` must list renderD128; the amdgpu driver and Vulkan (Mesa RADV) must be present.",
6025
- "Use the amd-latest (or amd-<version>) image: the Intel image does not carry the WebGPU runtime."
6026
- ]
6027
- },
6028
- "amd-vaapi": {
6029
- deviceKeys: () => false,
6030
- probe: (hw) => hw.gpu?.type === "amd",
6031
- image: "amd",
6032
- steps: [
6033
- "VAAPI is a hardware DECODE path, not an inference device: this check can only prove the GPU is visible.",
5981
+ function parts(v) {
5982
+ return v.split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
5983
+ }
5984
+ function isOlder(a, b) {
5985
+ const x = parts(a);
5986
+ const y = parts(b);
5987
+ for (let i = 0; i < 3; i++) {
5988
+ const d = (x[i] ?? 0) - (y[i] ?? 0);
5989
+ if (d !== 0) return d < 0;
5990
+ }
5991
+ return false;
5992
+ }
5993
+ async function cliVerdict(installed, fetchFn, entryPath) {
5994
+ const latest = await latestOnNpm(CLI_PACKAGE, fetchFn);
5995
+ return {
5996
+ installed,
5997
+ latest: latest.latest,
5998
+ updateAvailable: latest.latest === null ? null : isOlder(installed, latest.latest),
5999
+ ...latest.error !== void 0 ? { error: latest.error } : {},
6000
+ install: detectCliInstall(entryPath)
6001
+ };
6002
+ }
6003
+
6004
+ // src/mcp/ops-update-apply.ts
6005
+ var UPDATE_APPLY_TOOL = "ops_update_apply";
6006
+ function timing(deps) {
6007
+ return {
6008
+ sleep: deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms))),
6009
+ now: deps.now ?? Date.now,
6010
+ pollMs: deps.pollMs ?? 5e3,
6011
+ hubTimeoutMs: deps.hubTimeoutMs ?? 10 * 6e4,
6012
+ agentTimeoutMs: deps.agentTimeoutMs ?? 10 * 6e4,
6013
+ addonTimeoutMs: deps.addonTimeoutMs ?? 5 * 6e4
6014
+ };
6015
+ }
6016
+ function needs(p, target, restart) {
6017
+ return restart || p.running !== null && isOlder(p.running, target);
6018
+ }
6019
+ async function readPlan(client, args, ctx, deps) {
6020
+ const topology = await readTopology(client);
6021
+ if (!topology.ok) return { ok: false, problem: topology.reason };
6022
+ const hubNode = topology.value.find((n) => n.isHub === true);
6023
+ if (hubNode === void 0) return { ok: false, problem: "the topology lists no hub node" };
6024
+ const hubStatus = await readNodeStatus(client, hubNode);
6025
+ if (!hubStatus.ok) return { ok: false, problem: hubStatus.reason };
6026
+ const target = args.version ?? hubStatus.value.latestVersion ?? null;
6027
+ if (target === null) {
6028
+ return {
6029
+ ok: false,
6030
+ problem: "the hub has no registry check yet (latestVersion is null); run a check in the admin UI Cluster page or pass `version`"
6031
+ };
6032
+ }
6033
+ const restartFlags = /* @__PURE__ */ new Map();
6034
+ const plans = [];
6035
+ const offline = [];
6036
+ const unreadable = [];
6037
+ for (const node of topology.value) {
6038
+ if (!node.isOnline) {
6039
+ offline.push(node.id);
6040
+ continue;
6041
+ }
6042
+ const status = node.isHub === true ? hubStatus : await readNodeStatus(client, node);
6043
+ if (!status.ok) {
6044
+ unreadable.push({ nodeId: node.id, reason: status.reason });
6045
+ continue;
6046
+ }
6047
+ const v = serverVerdict(status.value);
6048
+ restartFlags.set(node.id, v.restartRequired === true);
6049
+ plans.push({
6050
+ node,
6051
+ running: v.running,
6052
+ busy: v.updateState !== null && v.updateState !== "idle" ? v.updateState : null,
6053
+ wrapper: wrapperVerdict(status.value)
6054
+ });
6055
+ }
6056
+ const updates = args.addons === false ? null : await readQuery(client, "addons.listUpdates", {}, z23.array(AddonUpdateShape));
6057
+ const cli = args.cli === false ? null : await cliVerdict(ctx.cliVersion, ctx.fetch, deps.entryPath ?? process.argv[1] ?? "");
6058
+ const hub = plans.find((p) => p.node.isHub === true) ?? null;
6059
+ return {
6060
+ ok: true,
6061
+ plan: {
6062
+ target,
6063
+ hub,
6064
+ agents: plans.filter((p) => p.node.isHub !== true),
6065
+ offline,
6066
+ unreadable,
6067
+ addons: updates !== null && updates.ok ? updates.value.filter((u) => !u.isSystem && u.category === "addon") : [],
6068
+ cli
6069
+ }
6070
+ };
6071
+ }
6072
+ function serverSteps(plan, restartOf) {
6073
+ const out = [];
6074
+ if (plan.hub !== null && needs(plan.hub, plan.target, restartOf(plan.hub))) out.push(plan.hub);
6075
+ for (const a of plan.agents) if (needs(a, plan.target, restartOf(a))) out.push(a);
6076
+ return out;
6077
+ }
6078
+ async function waitUntil(t, timeoutMs, check) {
6079
+ const deadline = t.now() + timeoutMs;
6080
+ let last = "not checked";
6081
+ for (; ; ) {
6082
+ const why = await check();
6083
+ if (why === null) return { ok: true };
6084
+ last = why;
6085
+ if (t.now() >= deadline)
6086
+ return {
6087
+ ok: false,
6088
+ why: `timed out after ${Math.round(timeoutMs / 1e3)}s: ${last}`
6089
+ };
6090
+ await t.sleep(t.pollMs);
6091
+ }
6092
+ }
6093
+ async function nodeReady(client, nodeId, isHub, target) {
6094
+ const status = await readNodeStatus(client, { id: nodeId, isHub });
6095
+ if (!status.ok) return `status unreadable (${status.reason})`;
6096
+ const v = serverVerdict(status.value);
6097
+ if (v.running !== target) return `still running ${v.running ?? "unknown"}, target ${target}`;
6098
+ if (v.restartRequired === true) return "restart still required";
6099
+ if (v.updateState !== "idle") return `update state ${v.updateState ?? "unknown"}`;
6100
+ const topology = await readTopology(client);
6101
+ if (!topology.ok) return `topology unreadable (${topology.reason})`;
6102
+ const node = topology.value.find((n) => n.id === nodeId);
6103
+ if (node === void 0) return "node absent from the topology";
6104
+ if (!node.isOnline) return "node is not online yet";
6105
+ if (capState(node).capless) return "node is online but registered no capabilities";
6106
+ return null;
6107
+ }
6108
+ async function applyServer(client, p, target, t) {
6109
+ const isHub = p.node.isHub === true;
6110
+ const step = isHub ? "hub-server" : "agent-server";
6111
+ const nodeId = p.node.id;
6112
+ let note = "";
6113
+ try {
6114
+ const raw = await client.call("POST", "serverManagement.applyServerUpdate", {
6115
+ version: target,
6116
+ ...statusInput(p.node)
6117
+ });
6118
+ const parsed = ServerActionShape.safeParse(raw);
6119
+ if (parsed.success) {
6120
+ const r = parsed.data;
6121
+ if (r.outcome === "refused" || r.outcome === "failed" || r.accepted === false && r.outcome !== "already-current") {
6122
+ const blockers = (r.blockers ?? []).map((b) => `${b.addonId}: ${b.reason}`).join("; ");
6123
+ return {
6124
+ step,
6125
+ nodeId,
6126
+ status: "failed",
6127
+ detail: `${nodeId} refused the update (${r.outcome ?? "not accepted"}): ${r.reason ?? r.error ?? r.message ?? "no reason given"}${blockers ? ` [would refuse to start: ${blockers}]` : ""}`
6128
+ };
6129
+ }
6130
+ note = r.outcome ?? "";
6131
+ }
6132
+ } catch (err) {
6133
+ note = `no reply (${reasonOf(err)}); expected while the node restarts`;
6134
+ }
6135
+ const wait = await waitUntil(
6136
+ t,
6137
+ isHub ? t.hubTimeoutMs : t.agentTimeoutMs,
6138
+ () => nodeReady(client, nodeId, isHub, target)
6139
+ );
6140
+ if (!wait.ok) {
6141
+ return {
6142
+ step,
6143
+ nodeId,
6144
+ status: "failed",
6145
+ detail: `${nodeId} did not reach ${target}: ${wait.why}`
6146
+ };
6147
+ }
6148
+ return {
6149
+ step,
6150
+ nodeId,
6151
+ status: "ok",
6152
+ detail: `${nodeId} runs ${target}, capabilities registered${note ? ` (${note})` : ""}`
6153
+ };
6154
+ }
6155
+ async function applyAddons(client, addons, t) {
6156
+ const done = [];
6157
+ for (const u of addons) {
6158
+ try {
6159
+ await client.call("POST", "addons.updatePackage", {
6160
+ name: u.name,
6161
+ version: u.latestVersion
6162
+ });
6163
+ done.push(u);
6164
+ } catch (err) {
6165
+ return [
6166
+ {
6167
+ step: "addons",
6168
+ status: "failed",
6169
+ detail: `hub failed to update ${u.name} to ${u.latestVersion}: ${reasonOf(err)}${done.length > 0 ? `; already updated: ${done.map((d) => d.name).join(", ")}` : ""}`
6170
+ }
6171
+ ];
6172
+ }
6173
+ }
6174
+ const names = new Set(done.map((d) => d.name));
6175
+ const wait = await waitUntil(t, t.addonTimeoutMs, async () => {
6176
+ const inst = await readInstallations(client);
6177
+ if (!inst.ok) return `installations unreadable (${inst.reason})`;
6178
+ for (const u of done) {
6179
+ const hub = inst.value.installations.find((i) => i.isHub && i.packageName === u.name);
6180
+ if (hub === void 0 || hub.version !== u.latestVersion) {
6181
+ return `${u.name} is ${hub?.version ?? "absent"} on the hub, expected ${u.latestVersion}`;
6182
+ }
6183
+ }
6184
+ const behind = addonDrift({
6185
+ ...inst.value,
6186
+ installations: inst.value.installations.filter((i) => names.has(i.packageName) && i.isOnline)
6187
+ });
6188
+ if (behind.length > 0) {
6189
+ return `agents behind the hub: ${behind.map((d) => `${d.nodeId}/${d.packageName}=${d.nodeVersion ?? "absent"}`).join(", ")}`;
6190
+ }
6191
+ return null;
6192
+ });
6193
+ return [
6194
+ {
6195
+ step: "addons",
6196
+ status: wait.ok ? "ok" : "unverified",
6197
+ detail: wait.ok ? `${done.length} addon(s) updated on the hub and matched on every online agent` : `${done.length} addon(s) updated on the hub; verification: ${wait.why}`
6198
+ }
6199
+ ];
6200
+ }
6201
+ async function applyCli(cli, run) {
6202
+ const restart = "Restart the MCP server in your client to load the new version; a running stdio MCP cannot swap itself.";
6203
+ if (cli.install.command === null) {
6204
+ return {
6205
+ step: "cli",
6206
+ status: "instructions",
6207
+ detail: `${cli.install.instructions} ${restart}`
6208
+ };
6209
+ }
6210
+ const [file, ...rest] = cli.install.command;
6211
+ const result = await run(file ?? "", rest);
6212
+ if (!result.ok) {
6213
+ return {
6214
+ step: "cli",
6215
+ status: "failed",
6216
+ detail: `\`${cli.install.command.join(" ")}\` failed: ${result.output}`
6217
+ };
6218
+ }
6219
+ return {
6220
+ step: "cli",
6221
+ status: "ok",
6222
+ detail: `ran \`${cli.install.command.join(" ")}\` (${cli.installed} to ${cli.latest ?? "latest"}). ${restart}`
6223
+ };
6224
+ }
6225
+ function updateApplySpec(client, ctx, deps = {}) {
6226
+ const t = timing(deps);
6227
+ return {
6228
+ tool: UPDATE_APPLY_TOOL,
6229
+ async dryRun(args) {
6230
+ const read = await readPlan(client, args, ctx, deps);
6231
+ if (!read.ok) return { confirmable: false, describe: { problem: read.problem } };
6232
+ const plan = read.plan;
6233
+ const steps = serverSteps(plan, () => false);
6234
+ const busy = [plan.hub, ...plan.agents].filter(
6235
+ (p) => p !== null && p.busy !== null
6236
+ );
6237
+ if (busy.length > 0) {
6238
+ return {
6239
+ confirmable: false,
6240
+ describe: {
6241
+ problem: `a node is mid-update: ${busy.map((p) => `${p.node.id} (${p.busy})`).join(", ")}; wait for it to settle`
6242
+ }
6243
+ };
6244
+ }
6245
+ const cliNeeded = plan.cli?.updateAvailable === true;
6246
+ if (steps.length === 0 && plan.addons.length === 0 && !cliNeeded) {
6247
+ return {
6248
+ confirmable: false,
6249
+ describe: {
6250
+ problem: "nothing to apply: every online node runs the target, no addon update is pending and the CLI is current",
6251
+ target: plan.target,
6252
+ wrapperUpdates: wrapperReport(plan)
6253
+ }
6254
+ };
6255
+ }
6256
+ return {
6257
+ confirmable: true,
6258
+ describe: {
6259
+ action: "update the cluster, in this order, verifying each step",
6260
+ target: plan.target,
6261
+ order: [
6262
+ ...steps.map((p, i) => ({
6263
+ order: i + 1,
6264
+ step: p.node.isHub === true ? "hub-server" : "agent-server",
6265
+ nodeId: p.node.id,
6266
+ from: p.running,
6267
+ to: plan.target
6268
+ })),
6269
+ ...args.addons === false ? [] : [
6270
+ {
6271
+ step: "addons",
6272
+ packages: plan.addons.map(
6273
+ (a) => `${a.name} ${a.currentVersion} -> ${a.latestVersion}`
6274
+ )
6275
+ }
6276
+ ],
6277
+ ...cliNeeded && plan.cli ? [
6278
+ {
6279
+ step: "cli",
6280
+ from: plan.cli.installed,
6281
+ to: plan.cli.latest,
6282
+ method: plan.cli.install.method,
6283
+ command: plan.cli.install.command?.join(" ") ?? null,
6284
+ instructions: plan.cli.install.command === null ? plan.cli.install.instructions : null
6285
+ }
6286
+ ] : []
6287
+ ],
6288
+ skippedOffline: plan.offline,
6289
+ skippedUnreadable: plan.unreadable,
6290
+ wrapperUpdates: wrapperReport(plan),
6291
+ timeouts: {
6292
+ hubS: t.hubTimeoutMs / 1e3,
6293
+ agentS: t.agentTimeoutMs / 1e3,
6294
+ addonsS: t.addonTimeoutMs / 1e3
6295
+ },
6296
+ notes: [
6297
+ "The hub restarts during step 1: this connection drops for a minute or two; that is expected.",
6298
+ "A failed step stops the chain and says which node and why; addons are not touched if a server step failed.",
6299
+ "Wrapper (image / app) updates are NEVER performed here; the instructions are listed.",
6300
+ ...cliNeeded ? ["The CLI update is the last step; afterwards restart the MCP in your client."] : []
6301
+ ]
6302
+ }
6303
+ };
6304
+ },
6305
+ async execute(args) {
6306
+ const read = await readPlan(client, args, ctx, deps);
6307
+ if (!read.ok) return { outcome: "failed", problem: read.problem, steps: [] };
6308
+ const plan = read.plan;
6309
+ const results = [];
6310
+ const ok = () => results.every((r) => r.status === "ok" || r.status === "skipped");
6311
+ for (const p of serverSteps(plan, () => false)) {
6312
+ if (!ok()) break;
6313
+ results.push(await applyServer(client, p, plan.target, t));
6314
+ }
6315
+ const serverOk = ok();
6316
+ if (serverOk && args.addons !== false && plan.addons.length > 0) {
6317
+ results.push(...await applyAddons(client, plan.addons, t));
6318
+ }
6319
+ const clusterOk = results.every((r) => r.status === "ok");
6320
+ if (clusterOk && plan.cli?.updateAvailable === true) {
6321
+ results.push(await applyCli(plan.cli, deps.runCommand ?? runCommand));
6322
+ } else if (plan.cli?.updateAvailable === true) {
6323
+ results.push({
6324
+ step: "cli",
6325
+ status: "skipped",
6326
+ detail: "not run: a cluster step did not succeed"
6327
+ });
6328
+ }
6329
+ const failed = results.find((r) => r.status === "failed");
6330
+ return {
6331
+ outcome: failed ? "failed" : results.every((r) => r.status === "ok") ? "complete" : "partial",
6332
+ target: plan.target,
6333
+ steps: results,
6334
+ ...failed ? { stoppedAt: failed.nodeId ?? failed.step, reason: failed.detail } : {},
6335
+ skippedOffline: plan.offline,
6336
+ wrapperUpdates: wrapperReport(plan),
6337
+ mcpRestartNeeded: results.some(
6338
+ (r) => r.step === "cli" && (r.status === "ok" || r.status === "instructions")
6339
+ ),
6340
+ next: "Run ops_update_check to confirm; nodes still offline were not touched."
6341
+ };
6342
+ }
6343
+ };
6344
+ }
6345
+ function wrapperReport(plan) {
6346
+ return [plan.hub, ...plan.agents].filter((p) => p !== null && p.wrapper.behind === true).map((p) => ({
6347
+ nodeId: p.node.id,
6348
+ kind: p.wrapper.kind,
6349
+ from: p.wrapper.from,
6350
+ to: p.wrapper.to,
6351
+ instructions: p.wrapper.instructions
6352
+ }));
6353
+ }
6354
+ async function opsUpdateApply(ctx, args, confirmToken, deps = {}) {
6355
+ const outcome = await ctx.hub();
6356
+ if (!outcome.ok) return noSessionResult(outcome);
6357
+ if (outcome.connection.tier !== "admin") {
6358
+ return {
6359
+ status: "refused",
6360
+ reason: outcome.connection.adminHint ?? "this credential is not admin-scoped"
6361
+ };
6362
+ }
6363
+ return runGated3(
6364
+ ctx.gate,
6365
+ updateApplySpec(outcome.connection.client, ctx, deps),
6366
+ args,
6367
+ confirmToken
6368
+ );
6369
+ }
6370
+
6371
+ // src/mcp/ops-update-check.ts
6372
+ import { z as z24 } from "zod";
6373
+ var ADDON_LIMIT = 30;
6374
+ async function opsUpdateCheck(ctx, deps = {}) {
6375
+ return withAdminHub(ctx, async ({ client, server }) => {
6376
+ const unavailable = {};
6377
+ const [topology, updates, installs, cli] = await Promise.all([
6378
+ readTopology(client),
6379
+ readQuery(client, "addons.listUpdates", {}, z24.array(AddonUpdateShape)),
6380
+ readInstallations(client),
6381
+ cliVerdict(ctx.cliVersion, ctx.fetch, deps.entryPath ?? process.argv[1] ?? "")
6382
+ ]);
6383
+ if (!topology.ok) {
6384
+ return { status: "unavailable", reason: topology.reason, cli };
6385
+ }
6386
+ const drift = installs.ok ? addonDrift(installs.value) : [];
6387
+ if (!installs.ok) unavailable["addonVersionsPerNode"] = installs.reason;
6388
+ else if (installs.value.unreachable.length > 0) {
6389
+ unavailable["addonVersionsPerNode"] = `no answer from ${installs.value.unreachable.map((u) => u.nodeId).join(", ")}`;
6390
+ }
6391
+ const nodes = await Promise.all(topology.value.map((n) => checkNode(client, n, drift)));
6392
+ let hubAddons = null;
6393
+ if (updates.ok) {
6394
+ const capped = cap(updates.value, ADDON_LIMIT);
6395
+ hubAddons = {
6396
+ items: capped.items.map((u) => ({
6397
+ name: u.name,
6398
+ from: u.currentVersion,
6399
+ to: u.latestVersion,
6400
+ framework: u.isSystem || u.category === "core"
6401
+ })),
6402
+ truncated: capped.truncated
6403
+ };
6404
+ } else unavailable["hubAddonUpdates"] = updates.reason;
6405
+ return {
6406
+ status: "ok",
6407
+ hub: server,
6408
+ nodes,
6409
+ hubAddons,
6410
+ cli,
6411
+ viewer: {
6412
+ status: "not-exposed",
6413
+ note: "No hub API reports the viewer version; the web viewer is served by the hub and the mobile apps update through their stores."
6414
+ },
6415
+ summary: {
6416
+ serverUpdateNeeded: nodes.filter((n) => n.serverNeedsUpdate === true).map((n) => n.id),
6417
+ wrapperUpdateNeeded: nodes.filter((n) => n.wrapper?.behind === true).map((n) => n.id),
6418
+ offline: nodes.filter((n) => !n.online).map((n) => n.id),
6419
+ capless: nodes.filter((n) => n.flags.length > 0).map((n) => n.id),
6420
+ nodesWithAddonDrift: [...new Set(drift.map((d) => d.nodeId))],
6421
+ cliUpdateNeeded: cli.updateAvailable
6422
+ },
6423
+ unavailable,
6424
+ note: "Latest versions come from the hub's last registry check (`latestCheckedAt`), not a fresh one; a version just published may not show yet."
6425
+ };
6426
+ });
6427
+ }
6428
+ async function checkNode(client, node, drift) {
6429
+ const caps = capState(node);
6430
+ const base = {
6431
+ id: node.id,
6432
+ name: node.name ?? null,
6433
+ isHub: node.isHub === true,
6434
+ online: node.isOnline,
6435
+ platform: node.platform ?? null,
6436
+ capabilityCount: caps.count,
6437
+ flags: caps.capless && node.isOnline ? ["online-but-capless"] : [],
6438
+ addonsBehindHub: drift.filter((d) => d.nodeId === node.id)
6439
+ };
6440
+ if (!node.isOnline) {
6441
+ return {
6442
+ ...base,
6443
+ server: null,
6444
+ serverNeedsUpdate: null,
6445
+ wrapper: null,
6446
+ unreadable: "node is offline"
6447
+ };
6448
+ }
6449
+ const status = await readNodeStatus(client, node);
6450
+ if (!status.ok) {
6451
+ return {
6452
+ ...base,
6453
+ server: null,
6454
+ serverNeedsUpdate: null,
6455
+ wrapper: null,
6456
+ unreadable: status.reason
6457
+ };
6458
+ }
6459
+ const server = serverVerdict(status.value);
6460
+ return {
6461
+ ...base,
6462
+ server,
6463
+ serverNeedsUpdate: needsServerUpdate(server),
6464
+ wrapper: wrapperVerdict(status.value)
6465
+ };
6466
+ }
6467
+
6468
+ // src/mcp/ops-tools.ts
6469
+ var IDENTITY_CLAIMS = ["sub", "username", "name", "role"];
6470
+ function identityOf(token) {
6471
+ const claims = jwtClaims(token);
6472
+ const out = {};
6473
+ if (claims === null) return out;
6474
+ for (const key of IDENTITY_CLAIMS) {
6475
+ const value = claims[key];
6476
+ if (typeof value === "string" && value !== "") out[key] = value;
6477
+ }
6478
+ return out;
6479
+ }
6480
+ async function whoami(ctx) {
6481
+ const outcome = await ctx.hub();
6482
+ if (!outcome.ok) return noSessionResult(outcome);
6483
+ const c = outcome.connection;
6484
+ return {
6485
+ status: "ok",
6486
+ hub: c.server,
6487
+ tier: c.tier,
6488
+ source: c.source,
6489
+ identity: identityOf(c.token)
6490
+ };
6491
+ }
6492
+ function registerOpsTools(server, ctx) {
6493
+ server.tool(
6494
+ "ops_whoami",
6495
+ "Which hub this MCP talks to and as whom, from the `camstack login` session (no network call). If there is no session, says to run `camstack login <host>` in a terminal.",
6496
+ {},
6497
+ async () => text11(await whoami(ctx))
6498
+ );
6499
+ server.tool(
6500
+ "ops_status",
6501
+ 'Cluster summary: every node (online, platform, server version, capability count, "online-but-capless" flag), the active alerts and the addons with an update. A part the hub did not answer is null with its reason in `unavailable`, never zero. Node list and updates need an admin `camstack login`.',
6502
+ {},
6503
+ async () => text11(await opsStatus(ctx))
6504
+ );
6505
+ server.tool(
6506
+ "ops_device",
6507
+ "One device: online state, provider addon and node, bound capabilities, runtime-state age and, for a camera, its pipeline, recording state, the functions an operator switched off (not faults) and its latest object events.",
6508
+ { deviceId: z25.number().int() },
6509
+ async ({ deviceId }) => text11(await opsDevice(ctx, deviceId))
6510
+ );
6511
+ server.tool(
6512
+ "ops_unhealthy_devices",
6513
+ "Every enabled device that is unhealthy, each with a typed reason (offline, detection-provisioning-failed, detection-not-assigned, recording-inactive). A function an operator switched off is never listed; a camera whose status could not be fully read is listed under `unknown`.",
6514
+ {},
6515
+ async () => text11(await opsUnhealthyDevices(ctx))
6516
+ );
6517
+ server.tool(
6518
+ "ops_logs",
6519
+ "Hub logs scoped by deviceId, addon, node, level, window and free text, grouped and deduplicated with counts. Says which tier answered (ring = recent in-RAM; persisted = the default log destination). With `around` (a track or event id) returns `contextLines` lines on each side of its first mention. Needs an admin `camstack login`.",
6520
+ {
6521
+ deviceId: z25.number().int().optional(),
6522
+ addonId: z25.string().optional(),
6523
+ nodeId: z25.string().optional(),
6524
+ level: z25.enum(["debug", "info", "warn", "error"]).optional(),
6525
+ windowMinutes: z25.number().int().min(1).max(1440).optional(),
6526
+ text: z25.string().max(200).optional(),
6527
+ around: z25.string().max(200).optional(),
6528
+ contextLines: z25.number().int().min(1).max(50).optional()
6529
+ },
6530
+ async (args) => text11(
6531
+ await opsLogs(ctx, {
6532
+ ...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
6533
+ ...args.addonId ? { addonId: args.addonId } : {},
6534
+ ...args.nodeId ? { nodeId: args.nodeId } : {},
6535
+ ...args.level ? { level: args.level } : {},
6536
+ ...args.windowMinutes !== void 0 ? { windowMinutes: args.windowMinutes } : {},
6537
+ ...args.text ? { text: args.text } : {},
6538
+ ...args.around ? { around: args.around } : {},
6539
+ ...args.contextLines !== void 0 ? { contextLines: args.contextLines } : {}
6540
+ })
6541
+ )
6542
+ );
6543
+ server.tool(
6544
+ "ops_events",
6545
+ "Find tracks (and, for one camera, object events) by camera, class, zone, time window and free text. Each result carries an admin-UI link and a viewer link built on the hub address flagged for app connections.",
6546
+ {
6547
+ deviceId: z25.number().int().optional(),
6548
+ className: z25.string().optional(),
6549
+ zone: z25.string().optional(),
6550
+ text: z25.string().max(200).optional(),
6551
+ windowMinutes: z25.number().int().min(1).max(10080).optional(),
6552
+ limit: z25.number().int().min(1).max(50).optional()
6553
+ },
6554
+ async (args) => text11(
6555
+ await opsEvents(ctx, {
6556
+ ...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
6557
+ ...args.className ? { className: args.className } : {},
6558
+ ...args.zone ? { zone: args.zone } : {},
6559
+ ...args.text ? { text: args.text } : {},
6560
+ ...args.windowMinutes !== void 0 ? { windowMinutes: args.windowMinutes } : {},
6561
+ ...args.limit !== void 0 ? { limit: args.limit } : {}
6562
+ })
6563
+ )
6564
+ );
6565
+ server.tool(
6566
+ "ops_track",
6567
+ "One track with its media audit: best shot, keyframes, every missing media kind with the reason the track recorded for it, and the audit findings. Links to the admin UI and the viewer.",
6568
+ { deviceId: z25.number().int(), trackId: z25.string().min(1) },
6569
+ async ({ deviceId, trackId }) => text11(await opsTrack(ctx, { deviceId, trackId }))
6570
+ );
6571
+ server.tool(
6572
+ "ops_accelerators",
6573
+ 'Per node: the inference devices it has, the Auto-tune hardware class the node maps to, and whether the measured detector-latency table has a row for that class and accelerator. A node that could not be read is null with a reason, never "no accelerator". Needs an admin `camstack login`.',
6574
+ {},
6575
+ async () => text11(await opsAccelerators(ctx))
6576
+ );
6577
+ server.tool(
6578
+ "ops_update_check",
6579
+ 'Read-only update report for the whole cluster. Every node from the topology: running vs installed vs latest @camstack/server, restartRequired / updateState / rolledBack, the wrapper (Docker image / desktop app) verdict, and whether its addon versions match the hub; the hub\'s addon updates; the viewer (not exposed by any API, said so); and this CLI/MCP against npm. A node that cannot be read is null with a reason, never "up to date". Needs an admin `camstack login`.',
6580
+ {},
6581
+ async () => text11(await opsUpdateCheck(ctx))
6582
+ );
6583
+ server.tool(
6584
+ UPDATE_APPLY_TOOL,
6585
+ "Bring the cluster up to date, verified step by step: hub server closure, wait until the hub runs it, then each online agent, then addons (agents follow by reconciliation, verified), then this CLI last. Two steps: call without confirm_token for a preview (order, versions, wrapper instructions, timeouts) and a one-shot token, then again with the SAME arguments and that token. A failed step stops the chain and names the node. Wrapper (image/app) updates are only reported with instructions. Needs an admin `camstack login`.",
6586
+ {
6587
+ version: z25.string().min(1).optional().describe("Server target; default = the hub's latest."),
6588
+ addons: z25.boolean().optional().describe("Update addons too (default true)."),
6589
+ cli: z25.boolean().optional().describe("Update this CLI last (default true)."),
6590
+ confirm_token: z25.string().optional()
6591
+ },
6592
+ async ({ version, addons, cli, confirm_token }) => text11(
6593
+ await opsUpdateApply(
6594
+ ctx,
6595
+ {
6596
+ ...version !== void 0 ? { version } : {},
6597
+ ...addons !== void 0 ? { addons } : {},
6598
+ ...cli !== void 0 ? { cli } : {}
6599
+ },
6600
+ confirm_token
6601
+ )
6602
+ )
6603
+ );
6604
+ }
6605
+
6606
+ // src/mcp/configure-tools.ts
6607
+ var CurrentDeviceSchema = z26.object({ name: z26.string() }).nullable();
6608
+ var RENAME_DEVICE_TOOL = "configure_rename_device";
6609
+ function renameDeviceSpec(client) {
6610
+ return {
6611
+ tool: RENAME_DEVICE_TOOL,
6612
+ async dryRun(args) {
6613
+ const current = CurrentDeviceSchema.safeParse(
6614
+ await client.call("GET", "deviceManager.getDevice", {
6615
+ deviceId: args.deviceId
6616
+ })
6617
+ );
6618
+ if (!current.success || current.data === null) {
6619
+ return {
6620
+ confirmable: false,
6621
+ describe: {
6622
+ problem: `device ${args.deviceId} was not found on the hub`
6623
+ }
6624
+ };
6625
+ }
6626
+ if (current.data.name === args.name) {
6627
+ return {
6628
+ confirmable: false,
6629
+ describe: {
6630
+ problem: `device ${args.deviceId} is already named "${args.name}"`
6631
+ }
6632
+ };
6633
+ }
6634
+ return {
6635
+ confirmable: true,
6636
+ describe: {
6637
+ action: "rename device",
6638
+ deviceId: args.deviceId,
6639
+ from: current.data.name,
6640
+ to: args.name
6641
+ }
6642
+ };
6643
+ },
6644
+ async execute(args) {
6645
+ await client.call("POST", "deviceManager.setName", {
6646
+ deviceId: args.deviceId,
6647
+ name: args.name
6648
+ });
6649
+ return { deviceId: args.deviceId, name: args.name };
6650
+ }
6651
+ };
6652
+ }
6653
+ async function renameDevice(ctx, args, confirmToken) {
6654
+ const outcome = await ctx.hub();
6655
+ if (!outcome.ok) return noSessionResult(outcome);
6656
+ if (outcome.connection.tier !== "admin") {
6657
+ return {
6658
+ status: "refused",
6659
+ reason: outcome.connection.adminHint ?? "this credential is not admin-scoped"
6660
+ };
6661
+ }
6662
+ return runGated4(ctx.gate, renameDeviceSpec(outcome.connection.client), args, confirmToken);
6663
+ }
6664
+ function registerConfigureTools(server, ctx) {
6665
+ server.tool(
6666
+ RENAME_DEVICE_TOOL,
6667
+ "Rename a device. Two steps: call without confirm_token for a preview and a one-shot token, then call again with the SAME arguments and that token to apply. Needs an admin `camstack login` session.",
6668
+ {
6669
+ deviceId: z26.number().int(),
6670
+ name: z26.string().min(1),
6671
+ confirm_token: z26.string().optional()
6672
+ },
6673
+ async ({ deviceId, name, confirm_token }) => text12(await renameDevice(ctx, { deviceId, name }, confirm_token))
6674
+ );
6675
+ registerAddonTools(server, ctx);
6676
+ registerCameraTools(server, ctx);
6677
+ registerNotifierTools(server, ctx);
6678
+ registerConfigureP3bTools(server, ctx);
6679
+ }
6680
+
6681
+ // src/mcp/support-tools.ts
6682
+ import { text as text13 } from "@camstack/develop";
6683
+ import { z as z31 } from "zod";
6684
+
6685
+ // src/mcp/support-accelerator.ts
6686
+ import { z as z27 } from "zod";
6687
+ var EXPECTED_ACCELERATORS = [
6688
+ "intel-igpu",
6689
+ "intel-npu",
6690
+ "amd-webgpu",
6691
+ "amd-vaapi",
6692
+ "amd-npu",
6693
+ "coral-usb",
6694
+ "coral-pcie",
6695
+ "apple-ane",
6696
+ "nvidia"
6697
+ ];
6698
+ var ExpectedAcceleratorSchema = z27.enum(EXPECTED_ACCELERATORS);
6699
+ var HardwareSchema2 = z27.looseObject({
6700
+ platform: z27.enum(["darwin", "linux", "win32"]),
6701
+ arch: z27.enum(["arm64", "x64"]),
6702
+ cpuModel: z27.string(),
6703
+ gpu: z27.looseObject({ type: z27.string(), name: z27.string() }).nullable(),
6704
+ npu: z27.looseObject({ type: z27.string() }).nullable()
6705
+ });
6706
+ var InferenceDevicesSchema2 = z27.looseObject({
6707
+ reachable: z27.boolean(),
6708
+ devices: z27.array(
6709
+ z27.looseObject({
6710
+ key: z27.string(),
6711
+ available: z27.boolean(),
6712
+ presence: z27.enum(["present", "absent", "unknown"]).optional(),
6713
+ enabled: z27.boolean(),
6714
+ exclusion: z27.string().nullable()
6715
+ })
6716
+ )
6717
+ });
6718
+ var CORAL_OPT_IN = [
6719
+ "A Coral is never picked automatically (D39): it must be enabled by an operator.",
6720
+ "In the admin UI open the node, Inference devices, and enable the Coral (edgetpu) device; Auto tune only sizes devices that are already enabled."
6721
+ ];
6722
+ var EXPECTATIONS = {
6723
+ "intel-igpu": {
6724
+ deviceKeys: (k) => k === "openvino:gpu",
6725
+ probe: (hw) => hw.gpu?.type === "intel",
6726
+ image: "intel",
6727
+ steps: [
6728
+ 'Pass the render node into the container: --device /dev/dri (Compose: devices: ["/dev/dri:/dev/dri"]).',
6729
+ "On the host, `ls -l /dev/dri` must list renderD128; if it does not, the i915/xe driver is not loaded on the host.",
6730
+ "Use the intel-latest (or intel-<version>) image: the AMD image carries no OpenVINO GPU runtime."
6731
+ ]
6732
+ },
6733
+ "intel-npu": {
6734
+ deviceKeys: (k) => k === "openvino:npu",
6735
+ probe: (hw) => hw.npu?.type === "intel-npu",
6736
+ image: "intel",
6737
+ steps: [
6738
+ 'Pass the accelerator node into the container: --device /dev/accel (Compose: devices: ["/dev/accel:/dev/accel"]).',
6739
+ "On the host, `ls -l /dev/accel` must list accel0; if it does not, the intel_vpu kernel driver is missing (needs kernel 6.6 or newer and the NPU firmware).",
6740
+ "Use the intel-latest (or intel-<version>) image."
6741
+ ]
6742
+ },
6743
+ "amd-webgpu": {
6744
+ deviceKeys: (k) => k === "onnx:webgpu",
6745
+ probe: (hw) => hw.gpu?.type === "amd",
6746
+ image: "amd",
6747
+ steps: [
6748
+ "Pass the render node into the container: --device /dev/dri.",
6749
+ "On the host, `ls -l /dev/dri` must list renderD128; the amdgpu driver and Vulkan (Mesa RADV) must be present.",
6750
+ "Use the amd-latest (or amd-<version>) image: the Intel image does not carry the WebGPU runtime."
6751
+ ]
6752
+ },
6753
+ "amd-vaapi": {
6754
+ deviceKeys: () => false,
6755
+ probe: (hw) => hw.gpu?.type === "amd",
6756
+ image: "amd",
6757
+ steps: [
6758
+ "VAAPI is a hardware DECODE path, not an inference device: this check can only prove the GPU is visible.",
6034
6759
  "Pass the render node into the container: --device /dev/dri, and check `vainfo` on the host.",
6035
6760
  "Use the amd-latest (or amd-<version>) image."
6036
6761
  ]
@@ -6263,9 +6988,9 @@ async function supportDiagnoseAccelerator(ctx, args) {
6263
6988
 
6264
6989
  // src/mcp/support-benchmark.ts
6265
6990
  import {
6266
- runGated as runGated4
6991
+ runGated as runGated5
6267
6992
  } from "@camstack/develop";
6268
- import { z as z25 } from "zod";
6993
+ import { z as z28 } from "zod";
6269
6994
  var BENCHMARK_RUN_TOOL = "support_benchmark_run";
6270
6995
  var AUTO_TUNE_MODELS = [
6271
6996
  "yolov9t-320",
@@ -6287,64 +7012,64 @@ var CONTRIBUTE_HOW = [
6287
7012
  "The measured medians belong in the Auto-tune latency table: docs/benchmarks/detector-latency.json and packages/types/src/pipeline/auto-tune/detector-performance.ts, changed together (detector-performance.spec.ts fails until they agree), in the camstack/server repository.",
6288
7013
  'Use support_draft_pr with kind "benchmark", the column ("<hardwareClass>/<accelerator>") and these cells to get the branch plan and the `gh pr create` command.'
6289
7014
  ];
6290
- var HardwareSchema3 = z25.looseObject({
6291
- platform: z25.enum(["darwin", "linux", "win32"]),
6292
- arch: z25.enum(["arm64", "x64"]),
6293
- cpuModel: z25.string(),
6294
- cpuCores: z25.number(),
6295
- totalRAM_MB: z25.number(),
6296
- availableRAM_MB: z25.number(),
6297
- gpu: z25.looseObject({
6298
- type: z25.enum(["nvidia", "amd", "intel", "apple"]),
6299
- name: z25.string()
7015
+ var HardwareSchema3 = z28.looseObject({
7016
+ platform: z28.enum(["darwin", "linux", "win32"]),
7017
+ arch: z28.enum(["arm64", "x64"]),
7018
+ cpuModel: z28.string(),
7019
+ cpuCores: z28.number(),
7020
+ totalRAM_MB: z28.number(),
7021
+ availableRAM_MB: z28.number(),
7022
+ gpu: z28.looseObject({
7023
+ type: z28.enum(["nvidia", "amd", "intel", "apple"]),
7024
+ name: z28.string()
6300
7025
  }).nullable(),
6301
- npu: z25.looseObject({ type: z25.enum(["apple-ane", "intel-npu", "amd-xdna"]) }).nullable()
7026
+ npu: z28.looseObject({ type: z28.enum(["apple-ane", "intel-npu", "amd-xdna"]) }).nullable()
6302
7027
  });
6303
- var InferenceDevicesSchema3 = z25.looseObject({
6304
- reachable: z25.boolean(),
6305
- devices: z25.array(
6306
- z25.looseObject({
6307
- key: z25.string(),
6308
- backend: z25.string(),
6309
- available: z25.boolean(),
6310
- enabled: z25.boolean()
7028
+ var InferenceDevicesSchema3 = z28.looseObject({
7029
+ reachable: z28.boolean(),
7030
+ devices: z28.array(
7031
+ z28.looseObject({
7032
+ key: z28.string(),
7033
+ backend: z28.string(),
7034
+ available: z28.boolean(),
7035
+ enabled: z28.boolean()
6311
7036
  })
6312
7037
  )
6313
7038
  });
6314
- var RunStatusSchema = z25.looseObject({
6315
- run: z25.looseObject({
6316
- runId: z25.string(),
6317
- nodeId: z25.string(),
6318
- state: z25.string(),
6319
- endReason: z25.string().nullable().optional(),
6320
- error: z25.string().nullable().optional()
7039
+ var RunStatusSchema = z28.looseObject({
7040
+ run: z28.looseObject({
7041
+ runId: z28.string(),
7042
+ nodeId: z28.string(),
7043
+ state: z28.string(),
7044
+ endReason: z28.string().nullable().optional(),
7045
+ error: z28.string().nullable().optional()
6321
7046
  }).nullable()
6322
7047
  });
6323
- var ModelListSchema = z25.array(
6324
- z25.looseObject({
6325
- id: z25.string(),
6326
- formats: z25.record(z25.string(), z25.looseObject({ downloaded: z25.boolean().optional() })).optional()
7048
+ var ModelListSchema = z28.array(
7049
+ z28.looseObject({
7050
+ id: z28.string(),
7051
+ formats: z28.record(z28.string(), z28.looseObject({ downloaded: z28.boolean().optional() })).optional()
6327
7052
  })
6328
7053
  );
6329
- var PipelineTestSchema = z25.looseObject({
6330
- kind: z25.string().optional(),
6331
- detections: z25.array(
6332
- z25.looseObject({
6333
- debug: z25.looseObject({
6334
- stepLatencyMs: z25.record(z25.string(), z25.number()).optional()
7054
+ var PipelineTestSchema = z28.looseObject({
7055
+ kind: z28.string().optional(),
7056
+ detections: z28.array(
7057
+ z28.looseObject({
7058
+ debug: z28.looseObject({
7059
+ stepLatencyMs: z28.record(z28.string(), z28.number()).optional()
6335
7060
  }).optional()
6336
7061
  })
6337
7062
  ).optional(),
6338
- discarded: z25.array(z25.unknown()).optional(),
6339
- debug: z25.looseObject({
6340
- stepTimings: z25.array(
6341
- z25.looseObject({
6342
- source: z25.string().optional(),
6343
- ms: z25.number().optional()
7063
+ discarded: z28.array(z28.unknown()).optional(),
7064
+ debug: z28.looseObject({
7065
+ stepTimings: z28.array(
7066
+ z28.looseObject({
7067
+ source: z28.string().optional(),
7068
+ ms: z28.number().optional()
6344
7069
  })
6345
7070
  ).optional(),
6346
- predictMs: z25.number().optional(),
6347
- totalInferenceMs: z25.number().optional()
7071
+ predictMs: z28.number().optional(),
7072
+ totalInferenceMs: z28.number().optional()
6348
7073
  }).optional()
6349
7074
  });
6350
7075
  var FORMAT_BY_BACKEND = {
@@ -6371,10 +7096,10 @@ function inferMsOf(raw) {
6371
7096
  if (!parsed.success) return { inferMs: null, shed: false };
6372
7097
  const r = parsed.data;
6373
7098
  const fromDetection = r.detections?.[0]?.debug?.stepLatencyMs?.[DETECTOR_STEP];
6374
- const timing = r.debug?.stepTimings?.find(
7099
+ const timing2 = r.debug?.stepTimings?.find(
6375
7100
  (t) => t.source === DETECTOR_STEP && typeof t.ms === "number"
6376
7101
  );
6377
- const candidates = [fromDetection, timing?.ms, r.debug?.predictMs, r.debug?.totalInferenceMs];
7102
+ const candidates = [fromDetection, timing2?.ms, r.debug?.predictMs, r.debug?.totalInferenceMs];
6378
7103
  const inferMs = candidates.find((c) => typeof c === "number" && c > 0) ?? null;
6379
7104
  const shedReason = (r.discarded ?? []).some(
6380
7105
  (d) => /deadline|shed/i.test(String(JSON.stringify(d)))
@@ -6629,17 +7354,17 @@ async function supportBenchmarkRun(ctx, deps, args, confirmToken) {
6629
7354
  reason: outcome.connection.adminHint ?? "this credential is not admin-scoped"
6630
7355
  };
6631
7356
  }
6632
- return runGated4(ctx.gate, benchmarkRunSpec(outcome.connection.client, deps), args, confirmToken);
6633
- }
6634
- var ResultsSchema = z25.array(
6635
- z25.looseObject({
6636
- nodeId: z25.string(),
6637
- cell: z25.looseObject({
6638
- phase: z25.string(),
6639
- deviceKey: z25.string(),
6640
- modelId: z25.string(),
6641
- p50Ms: z25.number().optional(),
6642
- fps: z25.number().optional()
7357
+ return runGated5(ctx.gate, benchmarkRunSpec(outcome.connection.client, deps), args, confirmToken);
7358
+ }
7359
+ var ResultsSchema = z28.array(
7360
+ z28.looseObject({
7361
+ nodeId: z28.string(),
7362
+ cell: z28.looseObject({
7363
+ phase: z28.string(),
7364
+ deviceKey: z28.string(),
7365
+ modelId: z28.string(),
7366
+ p50Ms: z28.number().optional(),
7367
+ fps: z28.number().optional()
6643
7368
  })
6644
7369
  })
6645
7370
  );
@@ -6692,20 +7417,20 @@ async function supportBenchmarkStatus(ctx, args) {
6692
7417
  }
6693
7418
 
6694
7419
  // src/mcp/support-camera.ts
6695
- import { z as z26 } from "zod";
6696
- var DeviceSchema2 = z26.looseObject({
6697
- id: z26.number(),
6698
- name: z26.string(),
6699
- addonId: z26.string().optional(),
6700
- disabled: z26.boolean().optional(),
6701
- online: z26.boolean().optional()
7420
+ import { z as z29 } from "zod";
7421
+ var DeviceSchema2 = z29.looseObject({
7422
+ id: z29.number(),
7423
+ name: z29.string(),
7424
+ addonId: z29.string().optional(),
7425
+ disabled: z29.boolean().optional(),
7426
+ online: z29.boolean().optional()
6702
7427
  }).nullable();
6703
7428
  var GENERIC = /onvif|rtsp/i;
6704
7429
  function words(text14) {
6705
7430
  return text14.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length >= 3);
6706
7431
  }
6707
- function isGeneric(parts) {
6708
- return parts.some((p) => GENERIC.test(p));
7432
+ function isGeneric(parts2) {
7433
+ return parts2.some((p) => GENERIC.test(p));
6709
7434
  }
6710
7435
  function installedPick(p) {
6711
7436
  return {
@@ -6726,9 +7451,9 @@ function availablePick(p) {
6726
7451
  var MATCHING_NOTE = "A provider manifest declares no brand or model list: matching is on provider name, description and keywords, so a brand that is supported under another name can be missed.";
6727
7452
  function matchIn(pools, tokens) {
6728
7453
  if (tokens.length === 0) return null;
6729
- const hit = (parts) => {
6730
- if (isGeneric(parts)) return false;
6731
- const joined = parts.join(" ").toLowerCase();
7454
+ const hit = (parts2) => {
7455
+ if (isGeneric(parts2)) return false;
7456
+ const joined = parts2.join(" ").toLowerCase();
6732
7457
  return tokens.some((t) => joined.includes(t));
6733
7458
  };
6734
7459
  const installed = pools.installed.find(
@@ -6864,10 +7589,10 @@ async function supportDiagnoseCamera(ctx, args) {
6864
7589
  }
6865
7590
 
6866
7591
  // src/mcp/support-deps.ts
6867
- import { execFile } from "child_process";
7592
+ import { execFile as execFile2 } from "child_process";
6868
7593
  var RUN_TIMEOUT_MS = 6e4;
6869
7594
  var spawnRunner = (command, args) => new Promise((resolve) => {
6870
- execFile(
7595
+ execFile2(
6871
7596
  command,
6872
7597
  [...args],
6873
7598
  { timeout: RUN_TIMEOUT_MS, maxBuffer: 1e6 },
@@ -6897,9 +7622,9 @@ import { tmpdir } from "os";
6897
7622
  import { join } from "path";
6898
7623
  import {
6899
7624
  digestArgs,
6900
- runGated as runGated5
7625
+ runGated as runGated6
6901
7626
  } from "@camstack/develop";
6902
- import { z as z27 } from "zod";
7627
+ import { z as z30 } from "zod";
6903
7628
 
6904
7629
  // src/mcp/support-scrub.ts
6905
7630
  import { redactToolArgs, scrubSecrets } from "@camstack/develop";
@@ -6908,10 +7633,10 @@ var IPV6 = /\b(?:[0-9a-fA-F]{1,4}:){4,7}[0-9a-fA-F]{1,4}(?![\w:])|\b[0-9a-fA-F]{
6908
7633
  var MAC = /\b(?:[0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}\b/g;
6909
7634
  var HOME_DIR = /(\/Users|\/home)\/[^/\s'"]+/g;
6910
7635
  function octets(ip) {
6911
- const parts = ip.split(".").map((p) => Number(p));
6912
- if (parts.length !== 4) return null;
6913
- if (parts.some((p) => !Number.isInteger(p) || p < 0 || p > 255)) return null;
6914
- return parts;
7636
+ const parts2 = ip.split(".").map((p) => Number(p));
7637
+ if (parts2.length !== 4) return null;
7638
+ if (parts2.some((p) => !Number.isInteger(p) || p < 0 || p > 255)) return null;
7639
+ return parts2;
6915
7640
  }
6916
7641
  function maskIpv4(ip) {
6917
7642
  const o = octets(ip);
@@ -6952,16 +7677,16 @@ var INFERENCE_PACKAGE = "@camstack/addon-pipeline";
6952
7677
  var TITLE_MAX = 140;
6953
7678
  var DIAGNOSIS_MAX = 4e3;
6954
7679
  var DRAFT_KINDS = ["accelerator", "camera", "benchmark", "other"];
6955
- var DraftKindSchema = z27.enum(DRAFT_KINDS);
6956
- var BenchmarkCellsSchema = z27.record(
6957
- z27.string().regex(/^[a-z0-9][a-z0-9._-]{1,63}$/),
6958
- z27.object({ p50Ms: z27.number().positive(), fps: z27.number().positive() })
7680
+ var DraftKindSchema = z30.enum(DRAFT_KINDS);
7681
+ var BenchmarkCellsSchema = z30.record(
7682
+ z30.string().regex(/^[a-z0-9][a-z0-9._-]{1,63}$/),
7683
+ z30.object({ p50Ms: z30.number().positive(), fps: z30.number().positive() })
6959
7684
  );
6960
7685
  var PackageNameRe = /^(@[a-z0-9._-]+\/)?[a-z0-9._-]+$/;
6961
7686
  var GithubRepoRe = /github\.com[:/]([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?(?:[/#?].*)?$/;
6962
7687
  var ShorthandRe = /^(?:github:)?([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/;
6963
- var RegistryDocSchema = z27.looseObject({
6964
- repository: z27.union([z27.string(), z27.looseObject({ url: z27.string() })]).optional()
7688
+ var RegistryDocSchema = z30.looseObject({
7689
+ repository: z30.union([z30.string(), z30.looseObject({ url: z30.string() })]).optional()
6965
7690
  });
6966
7691
  function githubRepoOf(value) {
6967
7692
  const url = GithubRepoRe.exec(value);
@@ -7003,7 +7728,7 @@ async function resolveRepo(ctx, packageName) {
7003
7728
  reason: null
7004
7729
  };
7005
7730
  }
7006
- var ServerStatusSchema2 = z27.looseObject({ runningVersion: z27.string() });
7731
+ var ServerStatusSchema2 = z30.looseObject({ runningVersion: z30.string() });
7007
7732
  async function serverVersion(client, nodeId) {
7008
7733
  const status = await readQuery(
7009
7734
  client,
@@ -7262,27 +7987,27 @@ function prSpec(ctx) {
7262
7987
  };
7263
7988
  }
7264
7989
  async function supportDraftIssue(ctx, deps, args, confirmToken) {
7265
- return runGated5(ctx.gate, issueSpec({ ctx, deps }), args, confirmToken);
7990
+ return runGated6(ctx.gate, issueSpec({ ctx, deps }), args, confirmToken);
7266
7991
  }
7267
7992
  async function supportDraftPr(ctx, args, confirmToken) {
7268
- return runGated5(ctx.gate, prSpec(ctx), args, confirmToken);
7993
+ return runGated6(ctx.gate, prSpec(ctx), args, confirmToken);
7269
7994
  }
7270
7995
 
7271
7996
  // src/mcp/support-tools.ts
7272
- var ModelId = z28.string().regex(/^[a-z0-9][a-z0-9._-]{1,63}$/);
7997
+ var ModelId = z31.string().regex(/^[a-z0-9][a-z0-9._-]{1,63}$/);
7273
7998
  var DraftShape = {
7274
7999
  kind: DraftKindSchema,
7275
- title: z28.string().min(3).max(300),
7276
- summary: z28.string().min(3).max(4e3),
7277
- nodeId: z28.string().max(100).optional(),
8000
+ title: z31.string().min(3).max(300),
8001
+ summary: z31.string().min(3).max(4e3),
8002
+ nodeId: z31.string().max(100).optional(),
7278
8003
  expected: ExpectedAcceleratorSchema.optional(),
7279
- brand: z28.string().max(100).optional(),
7280
- model: z28.string().max(100).optional(),
7281
- deviceId: z28.number().int().optional(),
7282
- packageName: z28.string().max(214).optional(),
7283
- column: z28.string().max(100).optional(),
8004
+ brand: z31.string().max(100).optional(),
8005
+ model: z31.string().max(100).optional(),
8006
+ deviceId: z31.number().int().optional(),
8007
+ packageName: z31.string().max(214).optional(),
8008
+ column: z31.string().max(100).optional(),
7284
8009
  cells: BenchmarkCellsSchema.optional(),
7285
- confirm_token: z28.string().optional()
8010
+ confirm_token: z31.string().optional()
7286
8011
  };
7287
8012
  function draftArgs(input) {
7288
8013
  return {
@@ -7303,18 +8028,18 @@ function registerSupportTools(server, ctx, deps = defaultSupportDeps) {
7303
8028
  server.tool(
7304
8029
  "support_benchmark_status",
7305
8030
  "The hub's own node-benchmark state: the active or last run and the per-device latency cells it stored. Read-only. The Auto-tune latency table is filled with support_benchmark_run. Needs an admin `camstack login`.",
7306
- { nodeId: z28.string().max(100).optional() },
8031
+ { nodeId: z31.string().max(100).optional() },
7307
8032
  async ({ nodeId }) => text13(await supportBenchmarkStatus(ctx, nodeId !== void 0 ? { nodeId } : {}))
7308
8033
  );
7309
8034
  server.tool(
7310
8035
  BENCHMARK_RUN_TOOL,
7311
8036
  "Measure detector latency on a node x device (cold run, then warm median per model, via runPipelineTest) and return it next to the Auto-tune table cell for the same hardware, plus how to contribute it. LOADS THE NODE: the detection pool restarts between models. Pick the node and deviceKey from ops_accelerators. Two steps: call without confirm_token for the plan and a one-shot token, then again with the SAME arguments and that token. Needs an admin `camstack login`.",
7312
8037
  {
7313
- nodeId: z28.string().min(1).max(100),
7314
- deviceKey: z28.string().min(1).max(60),
7315
- models: z28.array(ModelId).min(1).max(MAX_MODELS).optional(),
7316
- warmRuns: z28.number().int().min(1).max(MAX_WARM_RUNS).optional(),
7317
- confirm_token: z28.string().optional()
8038
+ nodeId: z31.string().min(1).max(100),
8039
+ deviceKey: z31.string().min(1).max(60),
8040
+ models: z31.array(ModelId).min(1).max(MAX_MODELS).optional(),
8041
+ warmRuns: z31.number().int().min(1).max(MAX_WARM_RUNS).optional(),
8042
+ confirm_token: z31.string().optional()
7318
8043
  },
7319
8044
  async ({ nodeId, deviceKey, models, warmRuns, confirm_token }) => text13(
7320
8045
  await supportBenchmarkRun(
@@ -7333,16 +8058,16 @@ function registerSupportTools(server, ctx, deps = defaultSupportDeps) {
7333
8058
  server.tool(
7334
8059
  "support_diagnose_accelerator",
7335
8060
  "A node should have an accelerator (Intel iGPU/NPU, AMD WebGPU/VAAPI/NPU, Coral USB/PCIe, Apple Neural Engine, NVIDIA) and the hub does not offer it. Reads what the platform probe saw and what the node's inference runtime offers, and answers ok | misconfigured (with fix steps: passthrough, driver, image tag, opt-in) | unsupported (which addon would need extending) | unknown (a read failed). A Coral is never auto-picked: the answer says how to opt in. Read-only.",
7336
- { nodeId: z28.string().min(1).max(100), expected: ExpectedAcceleratorSchema },
8061
+ { nodeId: z31.string().min(1).max(100), expected: ExpectedAcceleratorSchema },
7337
8062
  async ({ nodeId, expected }) => text13(await supportDiagnoseAccelerator(ctx, { nodeId, expected }))
7338
8063
  );
7339
8064
  server.tool(
7340
8065
  "support_diagnose_camera",
7341
8066
  "A camera brand or model, or an existing device that fails: which provider addons could serve it (installed, then published), and a verdict use-provider | generic-fallback (ONVIF / RTSP) | unsupported | unknown, with the next steps. Give a brand and/or model, or a deviceId. Read-only.",
7342
8067
  {
7343
- brand: z28.string().max(100).optional(),
7344
- model: z28.string().max(100).optional(),
7345
- deviceId: z28.number().int().optional()
8068
+ brand: z31.string().max(100).optional(),
8069
+ model: z31.string().max(100).optional(),
8070
+ deviceId: z31.number().int().optional()
7346
8071
  },
7347
8072
  async ({ brand, model, deviceId }) => text13(
7348
8073
  await supportDiagnoseCamera(ctx, {