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-
|
|
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
|
|
31
|
-
text as
|
|
30
|
+
runGated as runGated4,
|
|
31
|
+
text as text12
|
|
32
32
|
} from "@camstack/develop";
|
|
33
|
-
import { z as
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
4154
|
-
import { jwtClaims, text as
|
|
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-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
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
|
-
|
|
5056
|
-
|
|
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
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
"
|
|
5071
|
-
|
|
5072
|
-
|
|
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
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
function renameDeviceSpec(client) {
|
|
5121
|
+
function readInstallations(client) {
|
|
5122
|
+
return readQuery(client, "addons.listInstallations", {}, InstallationsShape2);
|
|
5123
|
+
}
|
|
5124
|
+
function serverVerdict(status) {
|
|
5158
5125
|
return {
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
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
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
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
|
-
|
|
5207
|
-
|
|
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
|
-
|
|
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
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
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
|
|
5231
|
-
import { z as
|
|
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
|
|
5238
|
+
import { z as z21 } from "zod";
|
|
5245
5239
|
|
|
5246
5240
|
// src/mcp/setup-install.ts
|
|
5247
|
-
import { z as
|
|
5241
|
+
import { z as z20 } from "zod";
|
|
5248
5242
|
|
|
5249
5243
|
// src/mcp/setup-install-rules.ts
|
|
5250
|
-
import { z as
|
|
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 =
|
|
5252
|
+
var HostKindSchema = z19.enum(HOST_KINDS);
|
|
5259
5253
|
var ROLES = ["hub", "agent"];
|
|
5260
|
-
var RoleSchema =
|
|
5254
|
+
var RoleSchema = z19.enum(ROLES);
|
|
5261
5255
|
var ACCELERATORS = ["intel", "amd", "coral", "apple", "cpu"];
|
|
5262
|
-
var AcceleratorSchema =
|
|
5263
|
-
var InstallPathsSchema =
|
|
5264
|
-
data:
|
|
5265
|
-
recordings:
|
|
5266
|
-
config:
|
|
5267
|
-
backups:
|
|
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 =
|
|
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:
|
|
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 =
|
|
5536
|
-
tag_name:
|
|
5537
|
-
assets:
|
|
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 =
|
|
5829
|
-
agents:
|
|
5830
|
-
|
|
5831
|
-
nodeId:
|
|
5832
|
-
host:
|
|
5833
|
-
port:
|
|
5834
|
-
version:
|
|
5835
|
-
name:
|
|
5836
|
-
state:
|
|
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 =
|
|
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 () =>
|
|
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:
|
|
5921
|
+
hubAddress: z22.string().min(1).optional().describe("Agents only: the hub to dial.")
|
|
5928
5922
|
},
|
|
5929
|
-
async (input) =>
|
|
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:
|
|
5935
|
-
async ({ timeoutMs }) =>
|
|
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:
|
|
5941
|
-
async ({ server: address }) =>
|
|
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:
|
|
5944
|
+
hubAddress: z22.string().min(1).optional()
|
|
5951
5945
|
},
|
|
5952
|
-
async (input) =>
|
|
5946
|
+
async (input) => text10(await addNodeTool(input, ctx))
|
|
5953
5947
|
);
|
|
5954
5948
|
}
|
|
5955
5949
|
|
|
5956
|
-
// src/mcp/
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
});
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
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
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
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
|
|
6991
|
+
runGated as runGated5
|
|
6267
6992
|
} from "@camstack/develop";
|
|
6268
|
-
import { z as
|
|
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 =
|
|
6291
|
-
platform:
|
|
6292
|
-
arch:
|
|
6293
|
-
cpuModel:
|
|
6294
|
-
cpuCores:
|
|
6295
|
-
totalRAM_MB:
|
|
6296
|
-
availableRAM_MB:
|
|
6297
|
-
gpu:
|
|
6298
|
-
type:
|
|
6299
|
-
name:
|
|
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:
|
|
7026
|
+
npu: z28.looseObject({ type: z28.enum(["apple-ane", "intel-npu", "amd-xdna"]) }).nullable()
|
|
6302
7027
|
});
|
|
6303
|
-
var InferenceDevicesSchema3 =
|
|
6304
|
-
reachable:
|
|
6305
|
-
devices:
|
|
6306
|
-
|
|
6307
|
-
key:
|
|
6308
|
-
backend:
|
|
6309
|
-
available:
|
|
6310
|
-
enabled:
|
|
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 =
|
|
6315
|
-
run:
|
|
6316
|
-
runId:
|
|
6317
|
-
nodeId:
|
|
6318
|
-
state:
|
|
6319
|
-
endReason:
|
|
6320
|
-
error:
|
|
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 =
|
|
6324
|
-
|
|
6325
|
-
id:
|
|
6326
|
-
formats:
|
|
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 =
|
|
6330
|
-
kind:
|
|
6331
|
-
detections:
|
|
6332
|
-
|
|
6333
|
-
debug:
|
|
6334
|
-
stepLatencyMs:
|
|
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:
|
|
6339
|
-
debug:
|
|
6340
|
-
stepTimings:
|
|
6341
|
-
|
|
6342
|
-
source:
|
|
6343
|
-
ms:
|
|
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:
|
|
6347
|
-
totalInferenceMs:
|
|
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
|
|
7099
|
+
const timing2 = r.debug?.stepTimings?.find(
|
|
6375
7100
|
(t) => t.source === DETECTOR_STEP && typeof t.ms === "number"
|
|
6376
7101
|
);
|
|
6377
|
-
const candidates = [fromDetection,
|
|
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
|
|
6633
|
-
}
|
|
6634
|
-
var ResultsSchema =
|
|
6635
|
-
|
|
6636
|
-
nodeId:
|
|
6637
|
-
cell:
|
|
6638
|
-
phase:
|
|
6639
|
-
deviceKey:
|
|
6640
|
-
modelId:
|
|
6641
|
-
p50Ms:
|
|
6642
|
-
fps:
|
|
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
|
|
6696
|
-
var DeviceSchema2 =
|
|
6697
|
-
id:
|
|
6698
|
-
name:
|
|
6699
|
-
addonId:
|
|
6700
|
-
disabled:
|
|
6701
|
-
online:
|
|
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(
|
|
6708
|
-
return
|
|
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 = (
|
|
6730
|
-
if (isGeneric(
|
|
6731
|
-
const joined =
|
|
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
|
-
|
|
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
|
|
7625
|
+
runGated as runGated6
|
|
6901
7626
|
} from "@camstack/develop";
|
|
6902
|
-
import { z as
|
|
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
|
|
6912
|
-
if (
|
|
6913
|
-
if (
|
|
6914
|
-
return
|
|
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 =
|
|
6956
|
-
var BenchmarkCellsSchema =
|
|
6957
|
-
|
|
6958
|
-
|
|
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 =
|
|
6964
|
-
repository:
|
|
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 =
|
|
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
|
|
7990
|
+
return runGated6(ctx.gate, issueSpec({ ctx, deps }), args, confirmToken);
|
|
7266
7991
|
}
|
|
7267
7992
|
async function supportDraftPr(ctx, args, confirmToken) {
|
|
7268
|
-
return
|
|
7993
|
+
return runGated6(ctx.gate, prSpec(ctx), args, confirmToken);
|
|
7269
7994
|
}
|
|
7270
7995
|
|
|
7271
7996
|
// src/mcp/support-tools.ts
|
|
7272
|
-
var ModelId =
|
|
7997
|
+
var ModelId = z31.string().regex(/^[a-z0-9][a-z0-9._-]{1,63}$/);
|
|
7273
7998
|
var DraftShape = {
|
|
7274
7999
|
kind: DraftKindSchema,
|
|
7275
|
-
title:
|
|
7276
|
-
summary:
|
|
7277
|
-
nodeId:
|
|
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:
|
|
7280
|
-
model:
|
|
7281
|
-
deviceId:
|
|
7282
|
-
packageName:
|
|
7283
|
-
column:
|
|
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:
|
|
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:
|
|
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:
|
|
7314
|
-
deviceKey:
|
|
7315
|
-
models:
|
|
7316
|
-
warmRuns:
|
|
7317
|
-
confirm_token:
|
|
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:
|
|
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:
|
|
7344
|
-
model:
|
|
7345
|
-
deviceId:
|
|
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, {
|