camstack 1.2.211 → 1.2.213
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/{chunk-NAUJQNCX.js → chunk-MJJWVAEM.js} +966 -175
- package/dist/cli.js +4 -4
- package/dist/{launcher-UXJ4WU3U.js → launcher-6TBFW332.js} +3834 -1129
- package/dist/{server-OYJZ5RCY.js → server-EFXGQ7FD.js} +1926 -705
- package/package.json +1 -1
|
@@ -11,13 +11,15 @@ import {
|
|
|
11
11
|
buildViewerLink,
|
|
12
12
|
capUnitResolver,
|
|
13
13
|
classifyInferenceHardware,
|
|
14
|
+
convertLegacyWhen,
|
|
15
|
+
countWhenLeaves,
|
|
14
16
|
findHubs,
|
|
15
17
|
generateAutomationBlock,
|
|
16
18
|
isReadingQuantity,
|
|
17
19
|
prepareRecipeUnits,
|
|
18
20
|
unitsForQuantity,
|
|
19
21
|
validateRecipeBounds
|
|
20
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-MJJWVAEM.js";
|
|
21
23
|
import "./chunk-LMMQX4CK.js";
|
|
22
24
|
|
|
23
25
|
// src/mcp/server.ts
|
|
@@ -27,10 +29,10 @@ import { MutationGate, tryConnectHub } from "@camstack/develop";
|
|
|
27
29
|
|
|
28
30
|
// src/mcp/configure-tools.ts
|
|
29
31
|
import {
|
|
30
|
-
runGated as
|
|
31
|
-
text as
|
|
32
|
+
runGated as runGated4,
|
|
33
|
+
text as text13
|
|
32
34
|
} from "@camstack/develop";
|
|
33
|
-
import { z as
|
|
35
|
+
import { z as z28 } from "zod";
|
|
34
36
|
|
|
35
37
|
// src/mcp/configure-automations.ts
|
|
36
38
|
import { z as z4 } from "zod";
|
|
@@ -987,7 +989,7 @@ function toActions(actions) {
|
|
|
987
989
|
function quantityNotes(recipe) {
|
|
988
990
|
const notes = [];
|
|
989
991
|
const walk = (c, path) => {
|
|
990
|
-
if (c.kind === "
|
|
992
|
+
if (c.kind === "device-is" && typeof c.value === "number" && c.unit !== void 0) {
|
|
991
993
|
notes.push({
|
|
992
994
|
where: `${path} ${c.cap}.${c.fieldPath}`,
|
|
993
995
|
value: c.value,
|
|
@@ -997,7 +999,7 @@ function quantityNotes(recipe) {
|
|
|
997
999
|
if (c.kind === "not") walk(c.child, path);
|
|
998
1000
|
if (c.kind === "all" || c.kind === "any") c.children.forEach((child) => walk(child, path));
|
|
999
1001
|
};
|
|
1000
|
-
|
|
1002
|
+
walk(recipe.when, "condition");
|
|
1001
1003
|
recipe.actions.forEach((a, i) => {
|
|
1002
1004
|
if (a.kind !== "cap" || a.args === void 0 || a.argUnits === void 0) return;
|
|
1003
1005
|
for (const [arg, unit] of Object.entries(a.argUnits)) {
|
|
@@ -1085,8 +1087,7 @@ async function interviewAutomation(client, answers) {
|
|
|
1085
1087
|
const conditions = answers.conditions ?? [];
|
|
1086
1088
|
const actions = answers.actions ?? [];
|
|
1087
1089
|
const draft = {
|
|
1088
|
-
triggers: [outcome.trigger],
|
|
1089
|
-
...toCondition(conditions) !== void 0 ? { conditions: toCondition(conditions) } : {},
|
|
1090
|
+
when: convertLegacyWhen({ triggers: [outcome.trigger], conditions: toCondition(conditions) }),
|
|
1090
1091
|
actions: toActions(actions),
|
|
1091
1092
|
...answers.cooldownMinutes !== void 0 ? {
|
|
1092
1093
|
runMode: {
|
|
@@ -1184,7 +1185,7 @@ function saveAutomationSpec(client) {
|
|
|
1184
1185
|
runsOn: "the hub",
|
|
1185
1186
|
enabled: args.enabled,
|
|
1186
1187
|
effect: args.enabled ? "it starts running as soon as it is saved" : "it is saved switched off",
|
|
1187
|
-
|
|
1188
|
+
rows: countWhenLeaves(finished.recipe.when),
|
|
1188
1189
|
steps: finished.recipe.actions.length,
|
|
1189
1190
|
quantities: quantityNotes(finished.recipe),
|
|
1190
1191
|
generatedBytes: code.length
|
|
@@ -1217,7 +1218,7 @@ var GATE_HOW = "Two steps: call without confirm_token for a preview and a one-sh
|
|
|
1217
1218
|
function registerAutomationTools(server, ctx) {
|
|
1218
1219
|
server.tool(
|
|
1219
1220
|
"configure_automation_questionnaire",
|
|
1220
|
-
"Interview for a new automation (a recipe:
|
|
1221
|
+
"Interview for a new automation (a recipe: ONE `when` tree \u2014 events that start it, states that must hold \u2014 and the actions). Stateless: send every answer so far in `answers`; it returns the questions still open, with choices read from the hub, or `ready` with a summary and the recipe. A number on a quantity field is { value, unit }. Writes nothing.",
|
|
1221
1222
|
{ answers: AutomationAnswersSchema },
|
|
1222
1223
|
async ({ answers }) => text(await automationQuestionnaire(ctx, answers))
|
|
1223
1224
|
);
|
|
@@ -2345,33 +2346,527 @@ function registerExportTools(server, ctx) {
|
|
|
2345
2346
|
);
|
|
2346
2347
|
}
|
|
2347
2348
|
|
|
2348
|
-
// src/mcp/configure-
|
|
2349
|
-
import { z as z9 } from "zod";
|
|
2349
|
+
// src/mcp/configure-oidc.ts
|
|
2350
2350
|
import { text as text6 } from "@camstack/develop";
|
|
2351
|
+
import { z as z10 } from "zod";
|
|
2352
|
+
|
|
2353
|
+
// src/mcp/configure-oidc-providers.ts
|
|
2354
|
+
import { z as z9 } from "zod";
|
|
2355
|
+
var OidcProviderSchema = z9.enum([
|
|
2356
|
+
"authentik",
|
|
2357
|
+
"pocket-id",
|
|
2358
|
+
"authelia",
|
|
2359
|
+
"keycloak",
|
|
2360
|
+
"zitadel",
|
|
2361
|
+
"kanidm",
|
|
2362
|
+
"google",
|
|
2363
|
+
"microsoft-entra",
|
|
2364
|
+
"auth0",
|
|
2365
|
+
"okta",
|
|
2366
|
+
"generic"
|
|
2367
|
+
]);
|
|
2368
|
+
var PIECE_QUESTION = {
|
|
2369
|
+
host: "host: the provider host name, with or without https:// (no path)",
|
|
2370
|
+
application_slug: "application_slug: the application slug (authentik) or client name (kanidm)",
|
|
2371
|
+
realm: "realm: the Keycloak realm name",
|
|
2372
|
+
tenant_id: "tenant_id: the Entra directory (tenant) id, a GUID or a verified domain"
|
|
2373
|
+
};
|
|
2374
|
+
function pieceQuestion(piece) {
|
|
2375
|
+
return PIECE_QUESTION[piece];
|
|
2376
|
+
}
|
|
2377
|
+
function hostOrigin(host) {
|
|
2378
|
+
const trimmed = host.trim().replace(/\/+$/, "");
|
|
2379
|
+
return /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
2380
|
+
}
|
|
2381
|
+
var COMMON_STEPS = [
|
|
2382
|
+
"Create the client as an OpenID Connect / OAuth2 web application, authorization-code flow.",
|
|
2383
|
+
"Paste the redirect URI shown below as the ONLY allowed redirect URI.",
|
|
2384
|
+
"Allow the scopes listed below."
|
|
2385
|
+
];
|
|
2386
|
+
var OIDC_PROVIDERS = {
|
|
2387
|
+
authentik: {
|
|
2388
|
+
label: "authentik",
|
|
2389
|
+
pieces: ["host", "application_slug"],
|
|
2390
|
+
issuerFormat: "https://<host>/application/o/<application_slug>/",
|
|
2391
|
+
scopes: "openid profile email",
|
|
2392
|
+
publicPkce: true,
|
|
2393
|
+
usernameClaim: "preferred_username",
|
|
2394
|
+
whereToCreate: "Admin interface > Applications > Applications > Create with Provider (OAuth2/OpenID)",
|
|
2395
|
+
steps: [
|
|
2396
|
+
"Create an application with an OAuth2/OpenID provider; its slug is the application_slug.",
|
|
2397
|
+
"Set the client type to Public for PKCE without a secret, or Confidential.",
|
|
2398
|
+
"Add the redirect URI below as a Strict redirect URI."
|
|
2399
|
+
],
|
|
2400
|
+
build: (p) => `${hostOrigin(p.host)}/application/o/${p.application_slug}/`
|
|
2401
|
+
},
|
|
2402
|
+
"pocket-id": {
|
|
2403
|
+
label: "Pocket ID",
|
|
2404
|
+
pieces: ["host"],
|
|
2405
|
+
issuerFormat: "https://<host>",
|
|
2406
|
+
scopes: "openid profile email",
|
|
2407
|
+
publicPkce: true,
|
|
2408
|
+
usernameClaim: "preferred_username",
|
|
2409
|
+
whereToCreate: "Settings > Administration > OIDC Clients > Add OIDC Client",
|
|
2410
|
+
steps: [
|
|
2411
|
+
"Add an OIDC client and paste the redirect URI below as its callback URL.",
|
|
2412
|
+
'Switch on "Public Client" for PKCE without a secret; leave it off for a confidential client.',
|
|
2413
|
+
"Copy the client ID."
|
|
2414
|
+
],
|
|
2415
|
+
build: (p) => hostOrigin(p.host)
|
|
2416
|
+
},
|
|
2417
|
+
authelia: {
|
|
2418
|
+
label: "Authelia",
|
|
2419
|
+
pieces: ["host"],
|
|
2420
|
+
issuerFormat: "https://<host>",
|
|
2421
|
+
scopes: "openid profile email",
|
|
2422
|
+
publicPkce: true,
|
|
2423
|
+
usernameClaim: "preferred_username",
|
|
2424
|
+
whereToCreate: "configuration.yml > identity_providers.oidc.clients (a file edit, then a restart)",
|
|
2425
|
+
steps: [
|
|
2426
|
+
"Add a client with its redirect URI below and `authorization_policy` of your choice.",
|
|
2427
|
+
"For PKCE without a secret set `public: true` and `require_pkce: true`.",
|
|
2428
|
+
"Set `token_endpoint_auth_method: client_secret_post` for a confidential client."
|
|
2429
|
+
],
|
|
2430
|
+
build: (p) => hostOrigin(p.host)
|
|
2431
|
+
},
|
|
2432
|
+
keycloak: {
|
|
2433
|
+
label: "Keycloak",
|
|
2434
|
+
pieces: ["host", "realm"],
|
|
2435
|
+
issuerFormat: "https://<host>/realms/<realm>",
|
|
2436
|
+
scopes: "openid profile email",
|
|
2437
|
+
publicPkce: true,
|
|
2438
|
+
usernameClaim: "preferred_username",
|
|
2439
|
+
whereToCreate: "Admin console > the realm > Clients > Create client",
|
|
2440
|
+
steps: [
|
|
2441
|
+
'Client type OpenID Connect; switch "Client authentication" on for confidential, off for public.',
|
|
2442
|
+
'Under Advanced, set "Proof Key for Code Exchange Code Challenge Method" to S256.',
|
|
2443
|
+
"Add the redirect URI below as a valid redirect URI."
|
|
2444
|
+
],
|
|
2445
|
+
build: (p) => `${hostOrigin(p.host)}/realms/${p.realm}`
|
|
2446
|
+
},
|
|
2447
|
+
zitadel: {
|
|
2448
|
+
label: "ZITADEL",
|
|
2449
|
+
pieces: ["host"],
|
|
2450
|
+
issuerFormat: "https://<host>",
|
|
2451
|
+
scopes: "openid profile email",
|
|
2452
|
+
publicPkce: true,
|
|
2453
|
+
usernameClaim: "preferred_username",
|
|
2454
|
+
whereToCreate: "Console > Project > Applications > New > Web",
|
|
2455
|
+
steps: [
|
|
2456
|
+
"Choose authentication method PKCE (public) or Basic/Post (confidential).",
|
|
2457
|
+
"Add the redirect URI below.",
|
|
2458
|
+
"Copy the client ID."
|
|
2459
|
+
],
|
|
2460
|
+
build: (p) => hostOrigin(p.host)
|
|
2461
|
+
},
|
|
2462
|
+
kanidm: {
|
|
2463
|
+
label: "Kanidm",
|
|
2464
|
+
pieces: ["host", "application_slug"],
|
|
2465
|
+
issuerFormat: "https://<host>/oauth2/openid/<application_slug>",
|
|
2466
|
+
scopes: "openid profile email",
|
|
2467
|
+
publicPkce: true,
|
|
2468
|
+
usernameClaim: "preferred_username",
|
|
2469
|
+
whereToCreate: "`kanidm system oauth2 create(-public) <name> <display> <origin>` on the server",
|
|
2470
|
+
steps: [
|
|
2471
|
+
"Use `create-public` for PKCE without a secret, `create` for a confidential client.",
|
|
2472
|
+
"Add the redirect URI below with `add-redirect-url`.",
|
|
2473
|
+
"Grant scopes with `update-scope-map <name> <group> openid profile email`.",
|
|
2474
|
+
"The application_slug is the client name you created."
|
|
2475
|
+
],
|
|
2476
|
+
build: (p) => `${hostOrigin(p.host)}/oauth2/openid/${p.application_slug}`
|
|
2477
|
+
},
|
|
2478
|
+
google: {
|
|
2479
|
+
label: "Google",
|
|
2480
|
+
pieces: [],
|
|
2481
|
+
issuerFormat: "https://accounts.google.com",
|
|
2482
|
+
scopes: "openid profile email",
|
|
2483
|
+
publicPkce: false,
|
|
2484
|
+
usernameClaim: "email",
|
|
2485
|
+
whereToCreate: "Google Cloud console > APIs & Services > Credentials > Create credentials > OAuth client ID (Web application)",
|
|
2486
|
+
steps: [
|
|
2487
|
+
'Choose "Web application"; Google issues a client secret for it, so the client is confidential.',
|
|
2488
|
+
"Add the redirect URI below as an authorized redirect URI (it must be a public https host)."
|
|
2489
|
+
],
|
|
2490
|
+
build: () => "https://accounts.google.com"
|
|
2491
|
+
},
|
|
2492
|
+
"microsoft-entra": {
|
|
2493
|
+
label: "Microsoft Entra ID",
|
|
2494
|
+
pieces: ["tenant_id"],
|
|
2495
|
+
issuerFormat: "https://login.microsoftonline.com/<tenant_id>/v2.0",
|
|
2496
|
+
scopes: "openid profile email",
|
|
2497
|
+
publicPkce: true,
|
|
2498
|
+
usernameClaim: "preferred_username",
|
|
2499
|
+
whereToCreate: "Entra admin center > App registrations > New registration",
|
|
2500
|
+
steps: [
|
|
2501
|
+
"Register a Web app (confidential, with a client secret) or a Single-page application (public, PKCE).",
|
|
2502
|
+
"Add the redirect URI below under Authentication.",
|
|
2503
|
+
"Copy the Application (client) ID and the Directory (tenant) ID."
|
|
2504
|
+
],
|
|
2505
|
+
build: (p) => `https://login.microsoftonline.com/${p.tenant_id}/v2.0`
|
|
2506
|
+
},
|
|
2507
|
+
auth0: {
|
|
2508
|
+
label: "Auth0",
|
|
2509
|
+
pieces: ["host"],
|
|
2510
|
+
issuerFormat: "https://<host>/",
|
|
2511
|
+
scopes: "openid profile email",
|
|
2512
|
+
publicPkce: true,
|
|
2513
|
+
usernameClaim: "email",
|
|
2514
|
+
whereToCreate: "Dashboard > Applications > Applications > Create Application",
|
|
2515
|
+
steps: [
|
|
2516
|
+
"Regular Web Application for confidential, Single Page Application for public (PKCE).",
|
|
2517
|
+
"Add the redirect URI below under Allowed Callback URLs.",
|
|
2518
|
+
"host is the tenant domain, for example example.eu.auth0.com."
|
|
2519
|
+
],
|
|
2520
|
+
build: (p) => `${hostOrigin(p.host)}/`
|
|
2521
|
+
},
|
|
2522
|
+
okta: {
|
|
2523
|
+
label: "Okta",
|
|
2524
|
+
pieces: ["host"],
|
|
2525
|
+
issuerFormat: "https://<host>/oauth2/default",
|
|
2526
|
+
scopes: "openid profile email",
|
|
2527
|
+
publicPkce: true,
|
|
2528
|
+
usernameClaim: "preferred_username",
|
|
2529
|
+
whereToCreate: "Admin console > Applications > Applications > Create App Integration > OIDC",
|
|
2530
|
+
steps: [
|
|
2531
|
+
"Web Application for confidential, Single-Page Application for public (PKCE).",
|
|
2532
|
+
"Add the redirect URI below as a sign-in redirect URI.",
|
|
2533
|
+
"host is your Okta domain; the default authorization server is used."
|
|
2534
|
+
],
|
|
2535
|
+
build: (p) => `${hostOrigin(p.host)}/oauth2/default`
|
|
2536
|
+
},
|
|
2537
|
+
generic: {
|
|
2538
|
+
label: "Any OpenID Connect provider",
|
|
2539
|
+
pieces: [],
|
|
2540
|
+
issuerFormat: "the `issuer` value of the provider's discovery document",
|
|
2541
|
+
scopes: "openid profile email",
|
|
2542
|
+
publicPkce: true,
|
|
2543
|
+
usernameClaim: "preferred_username",
|
|
2544
|
+
whereToCreate: "the provider's admin interface (create an OIDC web client)",
|
|
2545
|
+
steps: COMMON_STEPS,
|
|
2546
|
+
note: "Pass `issuer` exactly as the provider's /.well-known/openid-configuration reports it.",
|
|
2547
|
+
build: () => ""
|
|
2548
|
+
}
|
|
2549
|
+
};
|
|
2550
|
+
function resolveIssuer(provider, explicit, pieces) {
|
|
2551
|
+
if (explicit !== void 0 && explicit.trim() !== "") {
|
|
2552
|
+
return { issuer: explicit.trim(), missing: [] };
|
|
2553
|
+
}
|
|
2554
|
+
const spec = OIDC_PROVIDERS[provider];
|
|
2555
|
+
const missing = spec.pieces.filter((k) => (pieces[k] ?? "").trim() === "");
|
|
2556
|
+
if (missing.length > 0 || provider === "generic") return { issuer: null, missing };
|
|
2557
|
+
return {
|
|
2558
|
+
issuer: spec.build({
|
|
2559
|
+
host: pieces.host ?? "",
|
|
2560
|
+
application_slug: pieces.application_slug ?? "",
|
|
2561
|
+
realm: pieces.realm ?? "",
|
|
2562
|
+
tenant_id: pieces.tenant_id ?? ""
|
|
2563
|
+
}),
|
|
2564
|
+
missing: []
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
// src/mcp/configure-oidc.ts
|
|
2569
|
+
var OIDC_TOOL = "configure_oidc";
|
|
2570
|
+
var OIDC_ADMIN_ROUTE = "/system/identity?tab=authentication";
|
|
2571
|
+
var DISCOVERY_TIMEOUT_MS = 5e3;
|
|
2572
|
+
var ClientTypeSchema = z10.enum(["public", "confidential"]);
|
|
2573
|
+
var AddressesSchema3 = z10.looseObject({
|
|
2574
|
+
roles: z10.looseObject({
|
|
2575
|
+
publicEndpoint: z10.looseObject({ resolved: z10.string().nullish() }).optional(),
|
|
2576
|
+
appConnections: z10.looseObject({ resolved: z10.array(z10.string()) }).optional()
|
|
2577
|
+
})
|
|
2578
|
+
});
|
|
2579
|
+
var LOOPBACK2 = /^(localhost|127\.|\[?::1\]?$|0\.0\.0\.0)/i;
|
|
2580
|
+
function isLoopback(url) {
|
|
2581
|
+
try {
|
|
2582
|
+
return LOOPBACK2.test(new URL(url).hostname);
|
|
2583
|
+
} catch {
|
|
2584
|
+
return true;
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
async function resolveHubBase(client) {
|
|
2588
|
+
const read = await readQuery(client, "localNetwork.getAddresses", {}, AddressesSchema3);
|
|
2589
|
+
if (!read.ok) return { base: null, reason: read.reason };
|
|
2590
|
+
const pub = read.value.roles.publicEndpoint?.resolved;
|
|
2591
|
+
if (typeof pub === "string" && pub !== "" && !isLoopback(pub)) {
|
|
2592
|
+
return { base: pub.replace(/\/+$/, ""), source: "publicEndpoint" };
|
|
2593
|
+
}
|
|
2594
|
+
const app = read.value.roles.appConnections?.resolved.find((u) => !isLoopback(u));
|
|
2595
|
+
if (app !== void 0) return { base: app.replace(/\/+$/, ""), source: "appConnections" };
|
|
2596
|
+
return {
|
|
2597
|
+
base: null,
|
|
2598
|
+
reason: "no non-loopback public endpoint or app-connections address is set (Network page)"
|
|
2599
|
+
};
|
|
2600
|
+
}
|
|
2601
|
+
function callbackPath(connectionId) {
|
|
2602
|
+
return `/addon/auth-oidc/${connectionId}/callback`;
|
|
2603
|
+
}
|
|
2604
|
+
var DiscoverySchema = z10.looseObject({
|
|
2605
|
+
issuer: z10.string().optional(),
|
|
2606
|
+
authorization_endpoint: z10.string().optional(),
|
|
2607
|
+
token_endpoint: z10.string().optional(),
|
|
2608
|
+
jwks_uri: z10.string().optional(),
|
|
2609
|
+
code_challenge_methods_supported: z10.array(z10.string()).optional(),
|
|
2610
|
+
token_endpoint_auth_methods_supported: z10.array(z10.string()).optional()
|
|
2611
|
+
});
|
|
2612
|
+
var trimSlash = (s) => s.replace(/\/+$/, "");
|
|
2613
|
+
async function checkDiscovery(fetchFn, issuer, clientType) {
|
|
2614
|
+
const url = `${trimSlash(issuer)}/.well-known/openid-configuration`;
|
|
2615
|
+
let body;
|
|
2616
|
+
try {
|
|
2617
|
+
const res = await fetchFn(url, { signal: AbortSignal.timeout(DISCOVERY_TIMEOUT_MS) });
|
|
2618
|
+
if (!res.ok) {
|
|
2619
|
+
return {
|
|
2620
|
+
checked: false,
|
|
2621
|
+
url,
|
|
2622
|
+
warnings: [`discovery answered ${res.status}: check the issuer URL`]
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2625
|
+
body = await res.json();
|
|
2626
|
+
} catch (err) {
|
|
2627
|
+
return {
|
|
2628
|
+
checked: false,
|
|
2629
|
+
url,
|
|
2630
|
+
warnings: [`discovery could not be fetched from this machine (${reasonOf(err)})`]
|
|
2631
|
+
};
|
|
2632
|
+
}
|
|
2633
|
+
const parsed = DiscoverySchema.safeParse(body);
|
|
2634
|
+
if (!parsed.success) {
|
|
2635
|
+
return {
|
|
2636
|
+
checked: false,
|
|
2637
|
+
url,
|
|
2638
|
+
warnings: ["discovery answered something that is not a JSON document"]
|
|
2639
|
+
};
|
|
2640
|
+
}
|
|
2641
|
+
const doc = parsed.data;
|
|
2642
|
+
const warnings = [];
|
|
2643
|
+
const issuerMatches = doc.issuer !== void 0 && trimSlash(doc.issuer) === trimSlash(issuer);
|
|
2644
|
+
if (!issuerMatches) {
|
|
2645
|
+
warnings.push(
|
|
2646
|
+
`the provider reports issuer "${doc.issuer ?? "(none)"}", which differs from "${issuer}"`
|
|
2647
|
+
);
|
|
2648
|
+
}
|
|
2649
|
+
if (doc.authorization_endpoint === void 0 || doc.token_endpoint === void 0) {
|
|
2650
|
+
warnings.push("the discovery document lacks an authorization or token endpoint");
|
|
2651
|
+
}
|
|
2652
|
+
const s256 = doc.code_challenge_methods_supported?.includes("S256") === true;
|
|
2653
|
+
if (!s256)
|
|
2654
|
+
warnings.push("code_challenge_methods_supported does not include S256 (PKCE is always used)");
|
|
2655
|
+
const post = doc.token_endpoint_auth_methods_supported?.includes("client_secret_post") === true;
|
|
2656
|
+
if (clientType === "confidential" && !post) {
|
|
2657
|
+
warnings.push(
|
|
2658
|
+
"token_endpoint_auth_methods_supported does not include client_secret_post, which CamStack uses for a confidential client"
|
|
2659
|
+
);
|
|
2660
|
+
}
|
|
2661
|
+
return {
|
|
2662
|
+
checked: true,
|
|
2663
|
+
url,
|
|
2664
|
+
issuerMatches,
|
|
2665
|
+
...doc.authorization_endpoint !== void 0 ? { authorizationEndpoint: doc.authorization_endpoint } : {},
|
|
2666
|
+
...doc.token_endpoint !== void 0 ? { tokenEndpoint: doc.token_endpoint } : {},
|
|
2667
|
+
s256,
|
|
2668
|
+
clientSecretPost: post,
|
|
2669
|
+
warnings
|
|
2670
|
+
};
|
|
2671
|
+
}
|
|
2672
|
+
var ConnectionListSchema = z10.array(
|
|
2673
|
+
z10.looseObject({ id: z10.string(), displayName: z10.string().optional() })
|
|
2674
|
+
);
|
|
2675
|
+
function connectionIdOf(args) {
|
|
2676
|
+
return args.connection_id ?? args.provider;
|
|
2677
|
+
}
|
|
2678
|
+
function questionnaire(spec, args, redirectUri) {
|
|
2679
|
+
const questions = [
|
|
2680
|
+
...spec.pieces.map(pieceQuestion),
|
|
2681
|
+
...args.provider === "generic" ? ["issuer: the exact issuer from the discovery document"] : [],
|
|
2682
|
+
"client_id: the client id the provider shows after you create the client",
|
|
2683
|
+
`client_type: "public" (PKCE, no secret${spec.publicPkce ? "" : " - NOT supported by this provider"}) or "confidential" (a secret you type into the admin UI)`,
|
|
2684
|
+
"display_name (optional): the text of the login button",
|
|
2685
|
+
"connection_id (optional): to update an existing connection instead of creating one"
|
|
2686
|
+
];
|
|
2687
|
+
return {
|
|
2688
|
+
status: "questions",
|
|
2689
|
+
provider: args.provider,
|
|
2690
|
+
label: spec.label,
|
|
2691
|
+
questions,
|
|
2692
|
+
issuerFormat: spec.issuerFormat,
|
|
2693
|
+
defaultScopes: spec.scopes,
|
|
2694
|
+
publicClientSupported: spec.publicPkce,
|
|
2695
|
+
whereToCreateTheClient: spec.whereToCreate,
|
|
2696
|
+
providerSideSteps: spec.steps,
|
|
2697
|
+
...spec.note !== void 0 ? { note: spec.note } : {},
|
|
2698
|
+
redirectUri,
|
|
2699
|
+
...redirectUri === null ? {
|
|
2700
|
+
redirectUriNote: "the hub address could not be resolved; set a public endpoint on the Network page"
|
|
2701
|
+
} : {},
|
|
2702
|
+
next: `Call ${OIDC_TOOL} again with the answers; it previews the connection and gives a confirm_token.`,
|
|
2703
|
+
secretRule: "A client secret is never a tool argument. For a confidential client you type it in the admin UI."
|
|
2704
|
+
};
|
|
2705
|
+
}
|
|
2706
|
+
function oidcSpec(client, fetchFn) {
|
|
2707
|
+
return {
|
|
2708
|
+
tool: OIDC_TOOL,
|
|
2709
|
+
async dryRun(args) {
|
|
2710
|
+
const spec = OIDC_PROVIDERS[args.provider];
|
|
2711
|
+
const clientType = args.client_type ?? "public";
|
|
2712
|
+
const resolved = resolveIssuer(args.provider, args.issuer, args);
|
|
2713
|
+
if (resolved.issuer === null || args.client_id === void 0 || args.client_id.trim() === "") {
|
|
2714
|
+
return { confirmable: false, describe: { problem: "issuer and client_id are required" } };
|
|
2715
|
+
}
|
|
2716
|
+
if (clientType === "public" && !spec.publicPkce) {
|
|
2717
|
+
return {
|
|
2718
|
+
confirmable: false,
|
|
2719
|
+
describe: {
|
|
2720
|
+
problem: `${spec.label} does not issue public clients; use client_type "confidential"`
|
|
2721
|
+
}
|
|
2722
|
+
};
|
|
2723
|
+
}
|
|
2724
|
+
const id = connectionIdOf(args);
|
|
2725
|
+
const list = await readQuery(
|
|
2726
|
+
client,
|
|
2727
|
+
"oidcConnections.listConnections",
|
|
2728
|
+
{},
|
|
2729
|
+
ConnectionListSchema
|
|
2730
|
+
);
|
|
2731
|
+
if (!list.ok) return { confirmable: false, describe: { problem: list.reason } };
|
|
2732
|
+
const exists = list.value.some((c) => c.id === id);
|
|
2733
|
+
if (exists && args.connection_id === void 0) {
|
|
2734
|
+
return {
|
|
2735
|
+
confirmable: false,
|
|
2736
|
+
describe: {
|
|
2737
|
+
problem: `a connection "${id}" already exists; pass connection_id "${id}" to update it, or another id to create a new one`
|
|
2738
|
+
}
|
|
2739
|
+
};
|
|
2740
|
+
}
|
|
2741
|
+
const base = await resolveHubBase(client);
|
|
2742
|
+
const discovery = await checkDiscovery(fetchFn, resolved.issuer, clientType);
|
|
2743
|
+
const link = await adminLink(client, OIDC_ADMIN_ROUTE);
|
|
2744
|
+
return {
|
|
2745
|
+
confirmable: true,
|
|
2746
|
+
describe: {
|
|
2747
|
+
action: exists ? "update OIDC connection" : "create OIDC connection",
|
|
2748
|
+
connection: {
|
|
2749
|
+
id,
|
|
2750
|
+
displayName: args.display_name ?? spec.label,
|
|
2751
|
+
issuerUrl: resolved.issuer,
|
|
2752
|
+
clientId: args.client_id.trim(),
|
|
2753
|
+
clientType,
|
|
2754
|
+
scopes: spec.scopes,
|
|
2755
|
+
usernameClaim: spec.usernameClaim,
|
|
2756
|
+
clientSecret: clientType === "public" ? "none (public PKCE client)" : "entered in the admin UI"
|
|
2757
|
+
},
|
|
2758
|
+
redirectUri: base.base !== null ? `${base.base}${callbackPath(id)}` : null,
|
|
2759
|
+
...base.base === null ? { redirectUriProblem: base.reason } : {},
|
|
2760
|
+
discovery,
|
|
2761
|
+
warnings: discovery.warnings,
|
|
2762
|
+
...secretStep(clientType, link.url)
|
|
2763
|
+
}
|
|
2764
|
+
};
|
|
2765
|
+
},
|
|
2766
|
+
async execute(args) {
|
|
2767
|
+
const spec = OIDC_PROVIDERS[args.provider];
|
|
2768
|
+
const clientType = args.client_type ?? "public";
|
|
2769
|
+
const resolved = resolveIssuer(args.provider, args.issuer, args);
|
|
2770
|
+
const id = connectionIdOf(args);
|
|
2771
|
+
const displayName = args.display_name ?? spec.label;
|
|
2772
|
+
const list = await readQuery(
|
|
2773
|
+
client,
|
|
2774
|
+
"oidcConnections.listConnections",
|
|
2775
|
+
{},
|
|
2776
|
+
ConnectionListSchema
|
|
2777
|
+
);
|
|
2778
|
+
const exists = list.ok && list.value.some((c) => c.id === id);
|
|
2779
|
+
if (!exists) {
|
|
2780
|
+
await client.call("POST", "oidcConnections.createConnection", { id, displayName });
|
|
2781
|
+
}
|
|
2782
|
+
await client.call("POST", "oidcConnections.updateConnection", {
|
|
2783
|
+
id,
|
|
2784
|
+
displayName,
|
|
2785
|
+
issuerUrl: resolved.issuer ?? "",
|
|
2786
|
+
clientId: (args.client_id ?? "").trim(),
|
|
2787
|
+
scopes: spec.scopes,
|
|
2788
|
+
usernameClaim: spec.usernameClaim,
|
|
2789
|
+
...exists && clientType === "public" ? { clientSecret: { $secretClear: true } } : {}
|
|
2790
|
+
});
|
|
2791
|
+
const link = await adminLink(client, OIDC_ADMIN_ROUTE);
|
|
2792
|
+
return {
|
|
2793
|
+
connectionId: id,
|
|
2794
|
+
applied: exists ? "updated" : "created",
|
|
2795
|
+
...secretStep(clientType, link.url)
|
|
2796
|
+
};
|
|
2797
|
+
}
|
|
2798
|
+
};
|
|
2799
|
+
}
|
|
2800
|
+
function secretStep(clientType, url) {
|
|
2801
|
+
if (clientType === "public") return {};
|
|
2802
|
+
return {
|
|
2803
|
+
secretStep: {
|
|
2804
|
+
required: true,
|
|
2805
|
+
how: url !== null ? `open ${url}, edit the connection and type the client secret in the Client Secret field` : `open ${OIDC_ADMIN_ROUTE} in the admin UI, edit the connection and type the client secret in the Client Secret field`,
|
|
2806
|
+
note: "The secret is never a tool argument and is not shown here."
|
|
2807
|
+
}
|
|
2808
|
+
};
|
|
2809
|
+
}
|
|
2810
|
+
async function configureOidc(ctx, args, confirmToken) {
|
|
2811
|
+
const spec = OIDC_PROVIDERS[args.provider];
|
|
2812
|
+
const resolved = resolveIssuer(args.provider, args.issuer, args);
|
|
2813
|
+
const asking = resolved.issuer === null || (args.client_id ?? "").trim() === "";
|
|
2814
|
+
if (asking) {
|
|
2815
|
+
const base = await withHub(ctx, async ({ client }) => resolveHubBase(client));
|
|
2816
|
+
const redirect = "status" in base || base.base === null ? null : `${base.base}${callbackPath(connectionIdOf(args))}`;
|
|
2817
|
+
return questionnaire(spec, args, redirect);
|
|
2818
|
+
}
|
|
2819
|
+
return adminGated(ctx, (client) => oidcSpec(client, ctx.fetch), args, confirmToken);
|
|
2820
|
+
}
|
|
2821
|
+
var OIDC_TOOL_SHAPE = {
|
|
2822
|
+
provider: OidcProviderSchema,
|
|
2823
|
+
issuer: z10.string().optional(),
|
|
2824
|
+
host: z10.string().optional(),
|
|
2825
|
+
application_slug: z10.string().optional(),
|
|
2826
|
+
realm: z10.string().optional(),
|
|
2827
|
+
tenant_id: z10.string().optional(),
|
|
2828
|
+
client_id: z10.string().optional(),
|
|
2829
|
+
client_type: ClientTypeSchema.optional(),
|
|
2830
|
+
display_name: z10.string().optional(),
|
|
2831
|
+
connection_id: z10.string().optional(),
|
|
2832
|
+
confirm_token: ConfirmTokenSchema
|
|
2833
|
+
};
|
|
2834
|
+
function registerOidcTools(server, ctx) {
|
|
2835
|
+
server.tool(
|
|
2836
|
+
OIDC_TOOL,
|
|
2837
|
+
"Connect an OpenID Connect identity provider (authentik, pocket-id, authelia, keycloak, zitadel, kanidm, google, microsoft-entra, auth0, okta, generic). Call with only `provider` for the questions, the provider-side setup steps and the redirect URI to paste into the provider. With the answers it previews the connection and checks the provider discovery document (warnings, never a crash); call again with the SAME arguments and the confirm_token to create or update it. A client secret is NEVER an argument or output: a confidential client is finished by typing the secret in the admin UI (the route is given); a public PKCE client needs none. Needs an admin `camstack login` session for the write.",
|
|
2838
|
+
OIDC_TOOL_SHAPE,
|
|
2839
|
+
async ({ confirm_token, ...args }) => text6(await configureOidc(ctx, args, confirm_token))
|
|
2840
|
+
);
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
// src/mcp/configure-nc-rules.ts
|
|
2844
|
+
import { z as z11 } from "zod";
|
|
2845
|
+
import { text as text7 } from "@camstack/develop";
|
|
2351
2846
|
var TIME = /^\d{1,2}:\d{2}$/;
|
|
2352
2847
|
var ALL_DAYS = [0, 1, 2, 3, 4, 5, 6];
|
|
2353
|
-
var QuietHoursAnswerSchema =
|
|
2848
|
+
var QuietHoursAnswerSchema = z11.object({
|
|
2354
2849
|
/** Notifications are muted from this time … */
|
|
2355
|
-
from:
|
|
2850
|
+
from: z11.string().regex(TIME),
|
|
2356
2851
|
/** … until this one (HH:MM, hub time zone). A window may cross midnight. */
|
|
2357
|
-
to:
|
|
2852
|
+
to: z11.string().regex(TIME),
|
|
2358
2853
|
/** Days the quiet window STARTS on (0 = Sunday); absent = every day. */
|
|
2359
|
-
days:
|
|
2854
|
+
days: z11.array(z11.number().int().min(0).max(6)).min(1).optional()
|
|
2360
2855
|
});
|
|
2361
|
-
var NcRuleAnswersSchema =
|
|
2362
|
-
name:
|
|
2363
|
-
delivery:
|
|
2856
|
+
var NcRuleAnswersSchema = z11.object({
|
|
2857
|
+
name: z11.string().min(1).max(200).optional(),
|
|
2858
|
+
delivery: z11.enum(["immediate", "track-end"]).optional(),
|
|
2364
2859
|
/** Camera ids; `[]` means every camera. */
|
|
2365
|
-
cameras:
|
|
2366
|
-
classes:
|
|
2860
|
+
cameras: z11.array(z11.number().int().nonnegative()).optional(),
|
|
2861
|
+
classes: z11.array(z11.string().min(1)).optional(),
|
|
2367
2862
|
/** Zone ids or names; `[]` means anywhere in view. */
|
|
2368
|
-
zones:
|
|
2369
|
-
minConfidence:
|
|
2863
|
+
zones: z11.array(z11.string().min(1)).optional(),
|
|
2864
|
+
minConfidence: z11.number().min(0).max(1).optional(),
|
|
2370
2865
|
/** `null` states "no quiet hours"; absent means not asked yet. */
|
|
2371
2866
|
quietHours: QuietHoursAnswerSchema.nullable().optional(),
|
|
2372
2867
|
/** Target ids or names. */
|
|
2373
|
-
targets:
|
|
2374
|
-
cooldownSeconds:
|
|
2868
|
+
targets: z11.array(z11.string().min(1)).optional(),
|
|
2869
|
+
cooldownSeconds: z11.number().int().min(0).max(86400).optional()
|
|
2375
2870
|
});
|
|
2376
2871
|
var SAVE_RULE_TOOL = "configure_notification_rule_save";
|
|
2377
2872
|
var DELIVERY_CHOICES = [
|
|
@@ -2557,11 +3052,11 @@ async function interviewNcRule(client, answers) {
|
|
|
2557
3052
|
async function ncRuleQuestionnaire(ctx, answers) {
|
|
2558
3053
|
return withHub(ctx, ({ client }) => interviewNcRule(client, answers));
|
|
2559
3054
|
}
|
|
2560
|
-
var StoredRuleSchema =
|
|
2561
|
-
id:
|
|
2562
|
-
name:
|
|
2563
|
-
conditions:
|
|
2564
|
-
schedule:
|
|
3055
|
+
var StoredRuleSchema = z11.looseObject({
|
|
3056
|
+
id: z11.string().optional(),
|
|
3057
|
+
name: z11.string().optional(),
|
|
3058
|
+
conditions: z11.record(z11.string(), z11.unknown()).optional(),
|
|
3059
|
+
schedule: z11.unknown().optional()
|
|
2565
3060
|
});
|
|
2566
3061
|
var AUTHORED_CONDITION_KEYS = [
|
|
2567
3062
|
"devices",
|
|
@@ -2693,17 +3188,17 @@ function registerNcRuleTools(server, ctx) {
|
|
|
2693
3188
|
"configure_notification_rule_questionnaire",
|
|
2694
3189
|
"Interview for a detection notification rule (cameras, classes, zones, quiet hours, targets). Stateless: send every answer so far in `answers`; it returns the questions still open, with choices read from the hub, or `ready` with a summary and the rule. Writes nothing.",
|
|
2695
3190
|
{ answers: NcRuleAnswersSchema },
|
|
2696
|
-
async ({ answers }) =>
|
|
3191
|
+
async ({ answers }) => text7(await ncRuleQuestionnaire(ctx, answers))
|
|
2697
3192
|
);
|
|
2698
3193
|
server.tool(
|
|
2699
3194
|
SAVE_RULE_TOOL,
|
|
2700
3195
|
`Create a notification rule (the \`rule\` of a ready questionnaire), or update the one named by ruleId (the full conditions are sent, so keys the interview does not author are kept). ${GATE_HOW4}`,
|
|
2701
3196
|
{
|
|
2702
3197
|
rule: NcRuleInputSchema,
|
|
2703
|
-
ruleId:
|
|
3198
|
+
ruleId: z11.string().optional(),
|
|
2704
3199
|
confirm_token: ConfirmTokenSchema
|
|
2705
3200
|
},
|
|
2706
|
-
async ({ rule, ruleId, confirm_token }) =>
|
|
3201
|
+
async ({ rule, ruleId, confirm_token }) => text7(
|
|
2707
3202
|
await saveRule(ctx, { rule, ...ruleId !== void 0 ? { ruleId } : {} }, confirm_token)
|
|
2708
3203
|
)
|
|
2709
3204
|
);
|
|
@@ -2716,20 +3211,21 @@ function registerConfigureP3bTools(server, ctx) {
|
|
|
2716
3211
|
registerCalibrateTools(server, ctx);
|
|
2717
3212
|
registerExportTools(server, ctx);
|
|
2718
3213
|
registerNetworkTools(server, ctx);
|
|
3214
|
+
registerOidcTools(server, ctx);
|
|
2719
3215
|
registerZonesAlarmTools(server, ctx);
|
|
2720
3216
|
}
|
|
2721
3217
|
|
|
2722
3218
|
// src/mcp/configure-addons.ts
|
|
2723
3219
|
import {
|
|
2724
|
-
text as
|
|
3220
|
+
text as text8
|
|
2725
3221
|
} from "@camstack/develop";
|
|
2726
|
-
import { z as
|
|
3222
|
+
import { z as z13 } from "zod";
|
|
2727
3223
|
|
|
2728
3224
|
// src/mcp/configure-support.ts
|
|
2729
3225
|
import {
|
|
2730
3226
|
runGated as runGated2
|
|
2731
3227
|
} from "@camstack/develop";
|
|
2732
|
-
import { z as
|
|
3228
|
+
import { z as z12 } from "zod";
|
|
2733
3229
|
async function runGatedAdmin(ctx, makeSpec, args, confirmToken) {
|
|
2734
3230
|
const outcome = await ctx.hub();
|
|
2735
3231
|
if (!outcome.ok) return noSessionResult(outcome);
|
|
@@ -2752,28 +3248,28 @@ async function withAdminHub(ctx, body) {
|
|
|
2752
3248
|
}
|
|
2753
3249
|
return body(outcome.connection);
|
|
2754
3250
|
}
|
|
2755
|
-
var FormOptionSchema =
|
|
2756
|
-
value:
|
|
2757
|
-
label:
|
|
3251
|
+
var FormOptionSchema = z12.looseObject({
|
|
3252
|
+
value: z12.union([z12.string(), z12.number(), z12.boolean()]),
|
|
3253
|
+
label: z12.string().optional()
|
|
2758
3254
|
});
|
|
2759
|
-
var FormFieldSchema =
|
|
2760
|
-
type:
|
|
2761
|
-
key:
|
|
2762
|
-
label:
|
|
2763
|
-
description:
|
|
2764
|
-
required:
|
|
2765
|
-
secret:
|
|
2766
|
-
default:
|
|
2767
|
-
options:
|
|
2768
|
-
itemFields:
|
|
2769
|
-
|
|
2770
|
-
type:
|
|
2771
|
-
key:
|
|
3255
|
+
var FormFieldSchema = z12.looseObject({
|
|
3256
|
+
type: z12.string().optional(),
|
|
3257
|
+
key: z12.string().optional(),
|
|
3258
|
+
label: z12.string().optional(),
|
|
3259
|
+
description: z12.string().optional(),
|
|
3260
|
+
required: z12.boolean().optional(),
|
|
3261
|
+
secret: z12.boolean().optional(),
|
|
3262
|
+
default: z12.unknown().optional(),
|
|
3263
|
+
options: z12.array(FormOptionSchema).optional(),
|
|
3264
|
+
itemFields: z12.array(
|
|
3265
|
+
z12.looseObject({
|
|
3266
|
+
type: z12.string().optional(),
|
|
3267
|
+
key: z12.string().optional()
|
|
2772
3268
|
})
|
|
2773
3269
|
).optional()
|
|
2774
3270
|
});
|
|
2775
|
-
var FormSchemaShape =
|
|
2776
|
-
sections:
|
|
3271
|
+
var FormSchemaShape = z12.looseObject({
|
|
3272
|
+
sections: z12.array(z12.looseObject({ fields: z12.array(FormFieldSchema) }))
|
|
2777
3273
|
});
|
|
2778
3274
|
var NON_INPUT_TYPES = /* @__PURE__ */ new Set(["info", "divider", "heading", "button"]);
|
|
2779
3275
|
var CREDENTIAL_NAME = /pass(word|wd|phrase)?|token|secret|api[_-]?key|credential|private[_-]?key/i;
|
|
@@ -2874,21 +3370,21 @@ function splitConfig(fields, config) {
|
|
|
2874
3370
|
function fieldValidator(field) {
|
|
2875
3371
|
switch (field.type) {
|
|
2876
3372
|
case "number":
|
|
2877
|
-
return
|
|
3373
|
+
return z12.number();
|
|
2878
3374
|
case "boolean":
|
|
2879
3375
|
case "switch":
|
|
2880
3376
|
case "checkbox":
|
|
2881
3377
|
case "toggle":
|
|
2882
|
-
return
|
|
3378
|
+
return z12.boolean();
|
|
2883
3379
|
case "tags":
|
|
2884
|
-
return
|
|
3380
|
+
return z12.array(z12.string());
|
|
2885
3381
|
case "editable-array":
|
|
2886
|
-
return
|
|
3382
|
+
return z12.array(z12.record(z12.string(), z12.unknown()));
|
|
2887
3383
|
case "select":
|
|
2888
3384
|
case "radio": {
|
|
2889
|
-
if (field.options === null || field.options.length === 0) return
|
|
3385
|
+
if (field.options === null || field.options.length === 0) return z12.unknown();
|
|
2890
3386
|
const values = field.options.map((o) => o.value);
|
|
2891
|
-
return
|
|
3387
|
+
return z12.custom(
|
|
2892
3388
|
(v) => values.some((allowed) => allowed === v),
|
|
2893
3389
|
`one of: ${values.map(String).join(", ")}`
|
|
2894
3390
|
);
|
|
@@ -2898,9 +3394,9 @@ function fieldValidator(field) {
|
|
|
2898
3394
|
case "textarea":
|
|
2899
3395
|
case "url":
|
|
2900
3396
|
case "email":
|
|
2901
|
-
return
|
|
3397
|
+
return z12.string();
|
|
2902
3398
|
default:
|
|
2903
|
-
return
|
|
3399
|
+
return z12.unknown();
|
|
2904
3400
|
}
|
|
2905
3401
|
}
|
|
2906
3402
|
function formValidator(fields) {
|
|
@@ -2910,7 +3406,7 @@ function formValidator(fields) {
|
|
|
2910
3406
|
const base = fieldValidator(field);
|
|
2911
3407
|
shape[field.key] = field.required ? base : base.optional();
|
|
2912
3408
|
}
|
|
2913
|
-
return
|
|
3409
|
+
return z12.object(shape);
|
|
2914
3410
|
}
|
|
2915
3411
|
function validationProblems(error) {
|
|
2916
3412
|
return error.issues.map((issue) => {
|
|
@@ -2942,64 +3438,64 @@ function viewConfig(fields, config) {
|
|
|
2942
3438
|
var REGISTRY = "https://registry.npmjs.org";
|
|
2943
3439
|
var REGISTRY_TIMEOUT_MS = 5e3;
|
|
2944
3440
|
var MAX_RESULTS = 15;
|
|
2945
|
-
var PackageName =
|
|
2946
|
-
var CatalogShape =
|
|
2947
|
-
name:
|
|
2948
|
-
version:
|
|
2949
|
-
description:
|
|
2950
|
-
keywords:
|
|
2951
|
-
installed:
|
|
2952
|
-
capabilities:
|
|
3441
|
+
var PackageName = z13.string().min(1).regex(/^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/, "not an npm package name");
|
|
3442
|
+
var CatalogShape = z13.looseObject({
|
|
3443
|
+
name: z13.string(),
|
|
3444
|
+
version: z13.string(),
|
|
3445
|
+
description: z13.string(),
|
|
3446
|
+
keywords: z13.array(z13.string()).optional(),
|
|
3447
|
+
installed: z13.boolean(),
|
|
3448
|
+
capabilities: z13.array(z13.string()).nullable()
|
|
2953
3449
|
});
|
|
2954
|
-
var ListedAddonShape =
|
|
2955
|
-
manifest:
|
|
2956
|
-
id:
|
|
2957
|
-
packageName:
|
|
2958
|
-
packageVersion:
|
|
3450
|
+
var ListedAddonShape = z13.looseObject({
|
|
3451
|
+
manifest: z13.looseObject({
|
|
3452
|
+
id: z13.string(),
|
|
3453
|
+
packageName: z13.string(),
|
|
3454
|
+
packageVersion: z13.string(),
|
|
2959
3455
|
// The hub answers null, not absent, for a manifest without the flag.
|
|
2960
|
-
protected:
|
|
2961
|
-
removable:
|
|
3456
|
+
protected: z13.boolean().nullish(),
|
|
3457
|
+
removable: z13.boolean().nullish()
|
|
2962
3458
|
})
|
|
2963
3459
|
});
|
|
2964
|
-
var UpdateShape =
|
|
2965
|
-
name:
|
|
2966
|
-
currentVersion:
|
|
2967
|
-
latestVersion:
|
|
2968
|
-
category:
|
|
2969
|
-
requiresRestart:
|
|
2970
|
-
isSystem:
|
|
3460
|
+
var UpdateShape = z13.looseObject({
|
|
3461
|
+
name: z13.string(),
|
|
3462
|
+
currentVersion: z13.string(),
|
|
3463
|
+
latestVersion: z13.string(),
|
|
3464
|
+
category: z13.enum(["addon", "core"]),
|
|
3465
|
+
requiresRestart: z13.boolean(),
|
|
3466
|
+
isSystem: z13.boolean()
|
|
2971
3467
|
});
|
|
2972
|
-
var InstallationsShape =
|
|
2973
|
-
installations:
|
|
2974
|
-
|
|
2975
|
-
nodeId:
|
|
2976
|
-
isHub:
|
|
2977
|
-
isOnline:
|
|
2978
|
-
version:
|
|
2979
|
-
source:
|
|
3468
|
+
var InstallationsShape = z13.looseObject({
|
|
3469
|
+
installations: z13.array(
|
|
3470
|
+
z13.looseObject({
|
|
3471
|
+
nodeId: z13.string(),
|
|
3472
|
+
isHub: z13.boolean(),
|
|
3473
|
+
isOnline: z13.boolean(),
|
|
3474
|
+
version: z13.string().nullable(),
|
|
3475
|
+
source: z13.string()
|
|
2980
3476
|
})
|
|
2981
3477
|
),
|
|
2982
|
-
unreachable:
|
|
3478
|
+
unreachable: z13.array(z13.looseObject({ nodeId: z13.string(), error: z13.string() }))
|
|
2983
3479
|
});
|
|
2984
|
-
var VersionsShape =
|
|
2985
|
-
|
|
3480
|
+
var VersionsShape = z13.array(
|
|
3481
|
+
z13.looseObject({ version: z13.string(), deprecated: z13.string().optional() })
|
|
2986
3482
|
);
|
|
2987
|
-
var UninstallShape =
|
|
2988
|
-
success:
|
|
2989
|
-
packageName:
|
|
2990
|
-
results:
|
|
2991
|
-
|
|
2992
|
-
nodeId:
|
|
2993
|
-
outcome:
|
|
2994
|
-
error:
|
|
3483
|
+
var UninstallShape = z13.looseObject({
|
|
3484
|
+
success: z13.boolean(),
|
|
3485
|
+
packageName: z13.string(),
|
|
3486
|
+
results: z13.array(
|
|
3487
|
+
z13.looseObject({
|
|
3488
|
+
nodeId: z13.string(),
|
|
3489
|
+
outcome: z13.string(),
|
|
3490
|
+
error: z13.string().optional()
|
|
2995
3491
|
})
|
|
2996
3492
|
)
|
|
2997
3493
|
});
|
|
2998
|
-
var ServerStatusShape =
|
|
2999
|
-
runningVersion:
|
|
3494
|
+
var ServerStatusShape = z13.looseObject({
|
|
3495
|
+
runningVersion: z13.string().nullable().optional()
|
|
3000
3496
|
});
|
|
3001
3497
|
async function readProtection(client, packageName) {
|
|
3002
|
-
const list = await readQuery(client, "addons.list", {},
|
|
3498
|
+
const list = await readQuery(client, "addons.list", {}, z13.array(ListedAddonShape));
|
|
3003
3499
|
if (!list.ok) return null;
|
|
3004
3500
|
const mine = list.value.filter((a) => a.manifest.packageName === packageName);
|
|
3005
3501
|
return {
|
|
@@ -3010,13 +3506,13 @@ async function readProtection(client, packageName) {
|
|
|
3010
3506
|
}
|
|
3011
3507
|
var PROTECTED_REASON = (name) => `${name} is a protected package (manifest protected / removable:false): the kernel needs it, so it is never installed over, updated or removed here`;
|
|
3012
3508
|
var UNREADABLE_PROTECTION_REASON = (name) => `could not read whether ${name} is protected (addons.list did not answer with the expected shape); nothing is changed until it does`;
|
|
3013
|
-
var PackumentShape =
|
|
3014
|
-
peerDependencies:
|
|
3015
|
-
engines:
|
|
3016
|
-
camstack:
|
|
3017
|
-
addons:
|
|
3018
|
-
|
|
3019
|
-
execution:
|
|
3509
|
+
var PackumentShape = z13.looseObject({
|
|
3510
|
+
peerDependencies: z13.record(z13.string(), z13.string()).optional(),
|
|
3511
|
+
engines: z13.record(z13.string(), z13.string()).optional(),
|
|
3512
|
+
camstack: z13.looseObject({
|
|
3513
|
+
addons: z13.array(
|
|
3514
|
+
z13.looseObject({
|
|
3515
|
+
execution: z13.looseObject({ placement: z13.string().optional() }).optional()
|
|
3020
3516
|
})
|
|
3021
3517
|
).optional()
|
|
3022
3518
|
}).optional()
|
|
@@ -3068,10 +3564,10 @@ async function configureAddonsSearch(ctx, args) {
|
|
|
3068
3564
|
...args.query !== void 0 ? { query: args.query } : {},
|
|
3069
3565
|
...args.capability !== void 0 ? { capability: args.capability } : {}
|
|
3070
3566
|
},
|
|
3071
|
-
|
|
3567
|
+
z13.array(CatalogShape)
|
|
3072
3568
|
),
|
|
3073
|
-
readQuery(client, "addons.list", {},
|
|
3074
|
-
readQuery(client, "addons.listUpdates", {},
|
|
3569
|
+
readQuery(client, "addons.list", {}, z13.array(ListedAddonShape)),
|
|
3570
|
+
readQuery(client, "addons.listUpdates", {}, z13.array(UpdateShape)),
|
|
3075
3571
|
readQuery(client, "serverManagement.getServerPackageStatus", void 0, ServerStatusShape)
|
|
3076
3572
|
]);
|
|
3077
3573
|
if (!catalog.ok) return { status: "unavailable", reason: catalog.reason };
|
|
@@ -3150,7 +3646,7 @@ function installAddonSpec(client, fetchFn) {
|
|
|
3150
3646
|
client,
|
|
3151
3647
|
"addons.searchAvailable",
|
|
3152
3648
|
{ query: args.packageName },
|
|
3153
|
-
|
|
3649
|
+
z13.array(CatalogShape)
|
|
3154
3650
|
);
|
|
3155
3651
|
if (!catalog.ok) return { confirmable: false, describe: { problem: catalog.reason } };
|
|
3156
3652
|
const entry = catalog.value.find((c) => c.name === args.packageName);
|
|
@@ -3253,7 +3749,7 @@ function updateAddonSpec(client) {
|
|
|
3253
3749
|
client,
|
|
3254
3750
|
"addons.listUpdates",
|
|
3255
3751
|
args.nodeId !== void 0 ? { nodeId: args.nodeId } : {},
|
|
3256
|
-
|
|
3752
|
+
z13.array(UpdateShape)
|
|
3257
3753
|
);
|
|
3258
3754
|
if (!updates.ok) return { confirmable: false, describe: { problem: updates.reason } };
|
|
3259
3755
|
const entry = updates.value.find((u) => u.name === args.packageName);
|
|
@@ -3374,10 +3870,10 @@ function registerAddonTools(server, ctx) {
|
|
|
3374
3870
|
"configure_addons_search",
|
|
3375
3871
|
"Search the addon store (the catalog the admin UI uses) with the installed version, an available update, whether the package is protected, and compatibility (placement, @camstack/types peer range, node engine; no addon declares a minimum server version).",
|
|
3376
3872
|
{
|
|
3377
|
-
query:
|
|
3378
|
-
capability:
|
|
3873
|
+
query: z13.string().max(100).optional(),
|
|
3874
|
+
capability: z13.string().max(100).optional()
|
|
3379
3875
|
},
|
|
3380
|
-
async ({ query, capability }) =>
|
|
3876
|
+
async ({ query, capability }) => text8(
|
|
3381
3877
|
await configureAddonsSearch(ctx, {
|
|
3382
3878
|
...query !== void 0 ? { query } : {},
|
|
3383
3879
|
...capability !== void 0 ? { capability } : {}
|
|
@@ -3389,10 +3885,10 @@ function registerAddonTools(server, ctx) {
|
|
|
3389
3885
|
`Install an addon package on the hub from the store. ${TWO_STEP} Agents reconcile later; the result says so.`,
|
|
3390
3886
|
{
|
|
3391
3887
|
packageName: PackageName,
|
|
3392
|
-
version:
|
|
3393
|
-
confirm_token:
|
|
3888
|
+
version: z13.string().max(64).optional(),
|
|
3889
|
+
confirm_token: z13.string().optional()
|
|
3394
3890
|
},
|
|
3395
|
-
async ({ packageName, version, confirm_token }) =>
|
|
3891
|
+
async ({ packageName, version, confirm_token }) => text8(
|
|
3396
3892
|
await installAddon(
|
|
3397
3893
|
ctx,
|
|
3398
3894
|
{ packageName, ...version !== void 0 ? { version } : {} },
|
|
@@ -3405,11 +3901,11 @@ function registerAddonTools(server, ctx) {
|
|
|
3405
3901
|
`Update an installed addon package (hub by default, or one node). Protected and framework packages are refused in the preview. ${TWO_STEP}`,
|
|
3406
3902
|
{
|
|
3407
3903
|
packageName: PackageName,
|
|
3408
|
-
version:
|
|
3409
|
-
nodeId:
|
|
3410
|
-
confirm_token:
|
|
3904
|
+
version: z13.string().max(64).optional(),
|
|
3905
|
+
nodeId: z13.string().max(100).optional(),
|
|
3906
|
+
confirm_token: z13.string().optional()
|
|
3411
3907
|
},
|
|
3412
|
-
async ({ packageName, version, nodeId, confirm_token }) =>
|
|
3908
|
+
async ({ packageName, version, nodeId, confirm_token }) => text8(
|
|
3413
3909
|
await updateAddon(
|
|
3414
3910
|
ctx,
|
|
3415
3911
|
{
|
|
@@ -3426,11 +3922,11 @@ function registerAddonTools(server, ctx) {
|
|
|
3426
3922
|
`Uninstall an addon package from the hub, one node, or everywhere. Protected packages (manifest protected / removable:false) are refused in the preview with the reason. ${TWO_STEP}`,
|
|
3427
3923
|
{
|
|
3428
3924
|
packageName: PackageName,
|
|
3429
|
-
nodeId:
|
|
3430
|
-
everywhere:
|
|
3431
|
-
confirm_token:
|
|
3925
|
+
nodeId: z13.string().max(100).optional(),
|
|
3926
|
+
everywhere: z13.boolean().optional(),
|
|
3927
|
+
confirm_token: z13.string().optional()
|
|
3432
3928
|
},
|
|
3433
|
-
async ({ packageName, nodeId, everywhere, confirm_token }) =>
|
|
3929
|
+
async ({ packageName, nodeId, everywhere, confirm_token }) => text8(
|
|
3434
3930
|
await uninstallAddon(
|
|
3435
3931
|
ctx,
|
|
3436
3932
|
{
|
|
@@ -3446,9 +3942,9 @@ function registerAddonTools(server, ctx) {
|
|
|
3446
3942
|
|
|
3447
3943
|
// src/mcp/configure-cameras.ts
|
|
3448
3944
|
import {
|
|
3449
|
-
text as
|
|
3945
|
+
text as text9
|
|
3450
3946
|
} from "@camstack/develop";
|
|
3451
|
-
import { z as
|
|
3947
|
+
import { z as z14 } from "zod";
|
|
3452
3948
|
var UNSUPPORTED_HINT = "use support_diagnose_camera / support_draft_issue";
|
|
3453
3949
|
var MAX_PROVIDERS = 40;
|
|
3454
3950
|
var MAX_CANDIDATES = 50;
|
|
@@ -3461,34 +3957,34 @@ function unsupported(reason, provider) {
|
|
|
3461
3957
|
hint: UNSUPPORTED_HINT
|
|
3462
3958
|
};
|
|
3463
3959
|
}
|
|
3464
|
-
var CapabilityEntry =
|
|
3465
|
-
var InstalledAddonShape =
|
|
3466
|
-
manifest:
|
|
3467
|
-
id:
|
|
3468
|
-
name:
|
|
3469
|
-
description:
|
|
3470
|
-
packageName:
|
|
3471
|
-
packageVersion:
|
|
3472
|
-
capabilities:
|
|
3960
|
+
var CapabilityEntry = z14.union([z14.string(), z14.looseObject({ name: z14.string() })]);
|
|
3961
|
+
var InstalledAddonShape = z14.looseObject({
|
|
3962
|
+
manifest: z14.looseObject({
|
|
3963
|
+
id: z14.string(),
|
|
3964
|
+
name: z14.string().optional(),
|
|
3965
|
+
description: z14.string().optional(),
|
|
3966
|
+
packageName: z14.string(),
|
|
3967
|
+
packageVersion: z14.string(),
|
|
3968
|
+
capabilities: z14.array(CapabilityEntry).optional()
|
|
3473
3969
|
})
|
|
3474
3970
|
});
|
|
3475
|
-
var CatalogShape2 =
|
|
3476
|
-
name:
|
|
3477
|
-
version:
|
|
3478
|
-
description:
|
|
3479
|
-
keywords:
|
|
3480
|
-
installed:
|
|
3481
|
-
capabilities:
|
|
3971
|
+
var CatalogShape2 = z14.looseObject({
|
|
3972
|
+
name: z14.string(),
|
|
3973
|
+
version: z14.string(),
|
|
3974
|
+
description: z14.string(),
|
|
3975
|
+
keywords: z14.array(z14.string()),
|
|
3976
|
+
installed: z14.boolean(),
|
|
3977
|
+
capabilities: z14.array(z14.string()).nullable()
|
|
3482
3978
|
});
|
|
3483
|
-
var DiscoveryProvidersShape =
|
|
3484
|
-
providers:
|
|
3979
|
+
var DiscoveryProvidersShape = z14.looseObject({
|
|
3980
|
+
providers: z14.array(z14.looseObject({ addonId: z14.string(), label: z14.string() }))
|
|
3485
3981
|
});
|
|
3486
3982
|
function capabilityName(entry) {
|
|
3487
3983
|
return typeof entry === "string" ? entry : entry.name;
|
|
3488
3984
|
}
|
|
3489
3985
|
async function readInstalledProviders(client) {
|
|
3490
3986
|
const [list, discovery] = await Promise.all([
|
|
3491
|
-
readQuery(client, "addons.list", {},
|
|
3987
|
+
readQuery(client, "addons.list", {}, z14.array(InstalledAddonShape)),
|
|
3492
3988
|
readQuery(client, "deviceManager.discoveryProviders", {}, DiscoveryProvidersShape)
|
|
3493
3989
|
]);
|
|
3494
3990
|
if (!list.ok) return list;
|
|
@@ -3519,7 +4015,7 @@ async function configureCameraProviders(ctx, query) {
|
|
|
3519
4015
|
client,
|
|
3520
4016
|
"addons.searchAvailable",
|
|
3521
4017
|
{ capability: DEVICE_PROVIDER },
|
|
3522
|
-
|
|
4018
|
+
z14.array(CatalogShape2)
|
|
3523
4019
|
)
|
|
3524
4020
|
]);
|
|
3525
4021
|
if (!installed.ok) return { status: "unavailable", reason: installed.reason };
|
|
@@ -3553,13 +4049,13 @@ async function configureCameraProviders(ctx, query) {
|
|
|
3553
4049
|
};
|
|
3554
4050
|
});
|
|
3555
4051
|
}
|
|
3556
|
-
var CandidateShape =
|
|
3557
|
-
stableId:
|
|
3558
|
-
type:
|
|
3559
|
-
suggestedName:
|
|
3560
|
-
prefilledConfig:
|
|
4052
|
+
var CandidateShape = z14.looseObject({
|
|
4053
|
+
stableId: z14.string(),
|
|
4054
|
+
type: z14.string(),
|
|
4055
|
+
suggestedName: z14.string(),
|
|
4056
|
+
prefilledConfig: z14.record(z14.string(), z14.unknown())
|
|
3561
4057
|
});
|
|
3562
|
-
var DiscoverShape =
|
|
4058
|
+
var DiscoverShape = z14.looseObject({ candidates: z14.array(CandidateShape) });
|
|
3563
4059
|
async function configureCameraDiscover(ctx, args) {
|
|
3564
4060
|
return withAdminHub(ctx, async ({ client }) => {
|
|
3565
4061
|
const providers = await readQuery(
|
|
@@ -3645,7 +4141,7 @@ async function planAdd(client, args) {
|
|
|
3645
4141
|
client,
|
|
3646
4142
|
"deviceManager.getCreationSchema",
|
|
3647
4143
|
{ addonId: args.addonId, type: "camera" },
|
|
3648
|
-
|
|
4144
|
+
z14.unknown()
|
|
3649
4145
|
);
|
|
3650
4146
|
if (!schema.ok) return { ok: false, problems: [schema.reason] };
|
|
3651
4147
|
if (schema.value === null || schema.value === void 0) {
|
|
@@ -3696,7 +4192,7 @@ async function providerPage(client, addonId) {
|
|
|
3696
4192
|
const link = await resolveLinkBase(client);
|
|
3697
4193
|
return link.base === null ? null : `${link.base}/integrations/${addonId}`;
|
|
3698
4194
|
}
|
|
3699
|
-
var CreatedShape =
|
|
4195
|
+
var CreatedShape = z14.looseObject({ id: z14.number(), name: z14.string() });
|
|
3700
4196
|
function addCameraSpec(client) {
|
|
3701
4197
|
return {
|
|
3702
4198
|
tool: ADD_CAMERA_TOOL,
|
|
@@ -3730,7 +4226,7 @@ function addCameraSpec(client) {
|
|
|
3730
4226
|
const plan = await planAdd(client, args);
|
|
3731
4227
|
if (!plan.ok) throw new Error(plan.problems.join("; "));
|
|
3732
4228
|
const link = await providerPage(client, args.addonId);
|
|
3733
|
-
const
|
|
4229
|
+
const needs2 = plan.secretNeeds.map((f) => secretRequired(f, link));
|
|
3734
4230
|
let raw;
|
|
3735
4231
|
try {
|
|
3736
4232
|
raw = await client.call("POST", "deviceManager.createDevice", {
|
|
@@ -3742,7 +4238,7 @@ function addCameraSpec(client) {
|
|
|
3742
4238
|
return {
|
|
3743
4239
|
created: false,
|
|
3744
4240
|
adminUi: link,
|
|
3745
|
-
incompleteUntilSecretSet:
|
|
4241
|
+
incompleteUntilSecretSet: needs2,
|
|
3746
4242
|
error: reasonOf(err)
|
|
3747
4243
|
};
|
|
3748
4244
|
}
|
|
@@ -3751,7 +4247,7 @@ function addCameraSpec(client) {
|
|
|
3751
4247
|
return {
|
|
3752
4248
|
created: false,
|
|
3753
4249
|
adminUi: link,
|
|
3754
|
-
incompleteUntilSecretSet:
|
|
4250
|
+
incompleteUntilSecretSet: needs2,
|
|
3755
4251
|
error: "the hub answered in an unexpected shape; check the Devices page before retrying"
|
|
3756
4252
|
};
|
|
3757
4253
|
}
|
|
@@ -3761,7 +4257,7 @@ function addCameraSpec(client) {
|
|
|
3761
4257
|
deviceId: created.data.id,
|
|
3762
4258
|
name: created.data.name,
|
|
3763
4259
|
adminUi: base.base === null ? null : `${base.base}/devices/${String(created.data.id)}`,
|
|
3764
|
-
incompleteUntilSecretSet:
|
|
4260
|
+
incompleteUntilSecretSet: needs2
|
|
3765
4261
|
};
|
|
3766
4262
|
}
|
|
3767
4263
|
};
|
|
@@ -3773,17 +4269,17 @@ function registerCameraTools(server, ctx) {
|
|
|
3773
4269
|
server.tool(
|
|
3774
4270
|
"configure_camera_providers",
|
|
3775
4271
|
"Installed camera-provider addons and the published ones not yet installed. A manifest declares no brand/model list, so `query` matches on name, description and keywords; a brand nothing matches is `unsupported` with a pointer to support_diagnose_camera / support_draft_issue.",
|
|
3776
|
-
{ query:
|
|
3777
|
-
async ({ query }) =>
|
|
4272
|
+
{ query: z14.string().max(100).optional() },
|
|
4273
|
+
async ({ query }) => text9(await configureCameraProviders(ctx, query))
|
|
3778
4274
|
);
|
|
3779
4275
|
server.tool(
|
|
3780
4276
|
"configure_camera_discover",
|
|
3781
4277
|
'Scan the network through one camera provider (the admin UI "add camera" discovery) and list the candidates with the non-secret fields to pass to configure_camera_add. Changes nothing on the hub. A credential in `params` is refused (`secret-required`). Needs an admin `camstack login`.',
|
|
3782
4278
|
{
|
|
3783
|
-
addonId:
|
|
3784
|
-
params:
|
|
4279
|
+
addonId: z14.string().min(1),
|
|
4280
|
+
params: z14.record(z14.string(), z14.unknown()).optional()
|
|
3785
4281
|
},
|
|
3786
|
-
async ({ addonId, params }) =>
|
|
4282
|
+
async ({ addonId, params }) => text9(
|
|
3787
4283
|
await configureCameraDiscover(ctx, {
|
|
3788
4284
|
addonId,
|
|
3789
4285
|
...params !== void 0 ? { params } : {}
|
|
@@ -3794,46 +4290,46 @@ function registerCameraTools(server, ctx) {
|
|
|
3794
4290
|
ADD_CAMERA_TOOL,
|
|
3795
4291
|
"Add a camera through a provider's creation form (the fields are the provider's own schema). Two steps: call without confirm_token for a preview and a one-shot token, then again with the SAME arguments and that token. `config` takes NON-secret fields only; a password is refused and the answer carries the admin-UI link where the operator types it (`secret-required`). A provider that is not installed or has no manual form is `unsupported`.",
|
|
3796
4292
|
{
|
|
3797
|
-
addonId:
|
|
3798
|
-
config:
|
|
3799
|
-
confirm_token:
|
|
4293
|
+
addonId: z14.string().min(1),
|
|
4294
|
+
config: z14.record(z14.string(), z14.unknown()),
|
|
4295
|
+
confirm_token: z14.string().optional()
|
|
3800
4296
|
},
|
|
3801
|
-
async ({ addonId, config, confirm_token }) =>
|
|
4297
|
+
async ({ addonId, config, confirm_token }) => text9(await addCamera(ctx, { addonId, config }, confirm_token))
|
|
3802
4298
|
);
|
|
3803
4299
|
}
|
|
3804
4300
|
|
|
3805
4301
|
// src/mcp/configure-notifiers.ts
|
|
3806
4302
|
import {
|
|
3807
|
-
text as
|
|
4303
|
+
text as text10
|
|
3808
4304
|
} from "@camstack/develop";
|
|
3809
|
-
import { z as
|
|
4305
|
+
import { z as z15 } from "zod";
|
|
3810
4306
|
var MAX_TARGETS = 50;
|
|
3811
|
-
var TargetKindShape =
|
|
3812
|
-
kind:
|
|
3813
|
-
label:
|
|
3814
|
-
addonId:
|
|
3815
|
-
supportsDiscovery:
|
|
3816
|
-
configSchema:
|
|
3817
|
-
destinationKeys:
|
|
4307
|
+
var TargetKindShape = z15.looseObject({
|
|
4308
|
+
kind: z15.string(),
|
|
4309
|
+
label: z15.string(),
|
|
4310
|
+
addonId: z15.string(),
|
|
4311
|
+
supportsDiscovery: z15.boolean().optional(),
|
|
4312
|
+
configSchema: z15.unknown(),
|
|
4313
|
+
destinationKeys: z15.array(z15.string()).optional()
|
|
3818
4314
|
});
|
|
3819
|
-
var TargetShape =
|
|
3820
|
-
id:
|
|
3821
|
-
name:
|
|
3822
|
-
kind:
|
|
3823
|
-
addonId:
|
|
3824
|
-
enabled:
|
|
3825
|
-
config:
|
|
4315
|
+
var TargetShape = z15.looseObject({
|
|
4316
|
+
id: z15.string(),
|
|
4317
|
+
name: z15.string(),
|
|
4318
|
+
kind: z15.string(),
|
|
4319
|
+
addonId: z15.string(),
|
|
4320
|
+
enabled: z15.boolean(),
|
|
4321
|
+
config: z15.record(z15.string(), z15.unknown())
|
|
3826
4322
|
});
|
|
3827
|
-
var SendResultShape =
|
|
3828
|
-
success:
|
|
3829
|
-
error:
|
|
4323
|
+
var SendResultShape = z15.looseObject({
|
|
4324
|
+
success: z15.boolean(),
|
|
4325
|
+
error: z15.string().optional()
|
|
3830
4326
|
});
|
|
3831
4327
|
async function readKinds(client) {
|
|
3832
4328
|
const read = await readQuery(
|
|
3833
4329
|
client,
|
|
3834
4330
|
"notificationOutput.listTargetKinds",
|
|
3835
4331
|
{},
|
|
3836
|
-
|
|
4332
|
+
z15.array(TargetKindShape)
|
|
3837
4333
|
);
|
|
3838
4334
|
if (!read.ok) return read;
|
|
3839
4335
|
return {
|
|
@@ -3888,7 +4384,7 @@ async function configureNotificationTargets(ctx) {
|
|
|
3888
4384
|
return withAdminHub(ctx, async ({ client }) => {
|
|
3889
4385
|
const [kinds, targets] = await Promise.all([
|
|
3890
4386
|
readKinds(client),
|
|
3891
|
-
readQuery(client, "notificationOutput.listTargets", {},
|
|
4387
|
+
readQuery(client, "notificationOutput.listTargets", {}, z15.array(TargetShape))
|
|
3892
4388
|
]);
|
|
3893
4389
|
if (!targets.ok) return { status: "unavailable", reason: targets.reason };
|
|
3894
4390
|
const forms = kinds.ok ? kinds.value : [];
|
|
@@ -3929,7 +4425,7 @@ async function planUpsert(client, args) {
|
|
|
3929
4425
|
const fields = form.fields;
|
|
3930
4426
|
let existing = null;
|
|
3931
4427
|
if (args.targetId !== void 0) {
|
|
3932
|
-
const list = await readQuery(client, "notificationOutput.listTargets", {},
|
|
4428
|
+
const list = await readQuery(client, "notificationOutput.listTargets", {}, z15.array(TargetShape));
|
|
3933
4429
|
if (!list.ok) return { ok: false, problems: [list.reason] };
|
|
3934
4430
|
existing = list.value.find((t) => t.id === args.targetId) ?? null;
|
|
3935
4431
|
if (existing === null) return { ok: false, problems: [`target ${args.targetId} was not found`] };
|
|
@@ -4036,7 +4532,7 @@ function testTargetSpec(client) {
|
|
|
4036
4532
|
async dryRun(args) {
|
|
4037
4533
|
const [kinds, targets] = await Promise.all([
|
|
4038
4534
|
readKinds(client),
|
|
4039
|
-
readQuery(client, "notificationOutput.listTargets", {},
|
|
4535
|
+
readQuery(client, "notificationOutput.listTargets", {}, z15.array(TargetShape))
|
|
4040
4536
|
]);
|
|
4041
4537
|
if (!targets.ok) return { confirmable: false, describe: { problem: targets.reason } };
|
|
4042
4538
|
const target = targets.value.find((t) => t.id === args.targetId);
|
|
@@ -4050,12 +4546,12 @@ function testTargetSpec(client) {
|
|
|
4050
4546
|
if (view.incomplete.length > 0) {
|
|
4051
4547
|
const form = kinds.ok ? kinds.value.find((k) => k.kind === target.kind) : void 0;
|
|
4052
4548
|
const link = await notificationsPage(client);
|
|
4053
|
-
const
|
|
4549
|
+
const needs2 = (form?.fields ?? []).filter((f) => view.incomplete.includes(f.key)).map((f) => secretRequired(f, link));
|
|
4054
4550
|
return {
|
|
4055
4551
|
confirmable: false,
|
|
4056
4552
|
describe: {
|
|
4057
4553
|
problem: `target ${target.id} is incomplete until its secret is set`,
|
|
4058
|
-
secretRequired:
|
|
4554
|
+
secretRequired: needs2
|
|
4059
4555
|
}
|
|
4060
4556
|
};
|
|
4061
4557
|
}
|
|
@@ -4108,26 +4604,26 @@ function registerNotifierTools(server, ctx) {
|
|
|
4108
4604
|
"configure_notifiers",
|
|
4109
4605
|
"The notifier kinds a target can be created from (ntfy, telegram, email, \u2026), each with its settings fields, which are required and which are SECRETS. Secrets are never tool arguments. Needs an admin `camstack login`.",
|
|
4110
4606
|
{},
|
|
4111
|
-
async () =>
|
|
4607
|
+
async () => text10(await configureNotifiers(ctx))
|
|
4112
4608
|
);
|
|
4113
4609
|
server.tool(
|
|
4114
4610
|
"configure_notification_targets",
|
|
4115
4611
|
"Existing notification targets with their non-secret settings. A secret is shown only as set/unset, and a target missing a required secret is listed under `incomplete`. Needs an admin `camstack login`.",
|
|
4116
4612
|
{},
|
|
4117
|
-
async () =>
|
|
4613
|
+
async () => text10(await configureNotificationTargets(ctx))
|
|
4118
4614
|
);
|
|
4119
4615
|
server.tool(
|
|
4120
4616
|
UPSERT_TARGET_TOOL,
|
|
4121
4617
|
"Create (no targetId) or edit (targetId) a notification target from its notifier schema (see configure_notifiers). Two steps: call without confirm_token for a preview and a one-shot token, then again with the SAME arguments and that token. `config` takes NON-secret fields only; a secret field is refused, and the target is saved incomplete with the admin-UI link where the operator types the secret (`secret-required`).",
|
|
4122
4618
|
{
|
|
4123
|
-
targetId:
|
|
4124
|
-
kind:
|
|
4125
|
-
name:
|
|
4126
|
-
config:
|
|
4127
|
-
enabled:
|
|
4128
|
-
confirm_token:
|
|
4619
|
+
targetId: z15.string().min(1).optional(),
|
|
4620
|
+
kind: z15.string().min(1),
|
|
4621
|
+
name: z15.string().min(1),
|
|
4622
|
+
config: z15.record(z15.string(), z15.unknown()),
|
|
4623
|
+
enabled: z15.boolean().optional(),
|
|
4624
|
+
confirm_token: z15.string().optional()
|
|
4129
4625
|
},
|
|
4130
|
-
async ({ targetId, kind, name, config, enabled, confirm_token }) =>
|
|
4626
|
+
async ({ targetId, kind, name, config, enabled, confirm_token }) => text10(
|
|
4131
4627
|
await upsertNotificationTarget(
|
|
4132
4628
|
ctx,
|
|
4133
4629
|
{
|
|
@@ -4144,51 +4640,51 @@ function registerNotifierTools(server, ctx) {
|
|
|
4144
4640
|
server.tool(
|
|
4145
4641
|
TEST_TARGET_TOOL,
|
|
4146
4642
|
"Send a test notification to one target. Two steps: the preview checks the target exists and has every required secret, and only the confirmed call delivers a real message.",
|
|
4147
|
-
{ targetId:
|
|
4148
|
-
async ({ targetId, confirm_token }) =>
|
|
4643
|
+
{ targetId: z15.string().min(1), confirm_token: z15.string().optional() },
|
|
4644
|
+
async ({ targetId, confirm_token }) => text10(await testNotificationTarget(ctx, { targetId }, confirm_token))
|
|
4149
4645
|
);
|
|
4150
4646
|
}
|
|
4151
4647
|
|
|
4152
4648
|
// src/mcp/ops-tools.ts
|
|
4153
|
-
import { z as
|
|
4154
|
-
import { jwtClaims, text as
|
|
4649
|
+
import { z as z27 } from "zod";
|
|
4650
|
+
import { jwtClaims, text as text12 } from "@camstack/develop";
|
|
4155
4651
|
|
|
4156
4652
|
// src/mcp/ops-accelerators.ts
|
|
4157
|
-
import { z as
|
|
4158
|
-
var NodesSchema =
|
|
4159
|
-
|
|
4160
|
-
id:
|
|
4161
|
-
isOnline:
|
|
4162
|
-
name:
|
|
4653
|
+
import { z as z16 } from "zod";
|
|
4654
|
+
var NodesSchema = z16.array(
|
|
4655
|
+
z16.looseObject({
|
|
4656
|
+
id: z16.string(),
|
|
4657
|
+
isOnline: z16.boolean(),
|
|
4658
|
+
name: z16.string().optional()
|
|
4163
4659
|
})
|
|
4164
4660
|
);
|
|
4165
|
-
var HardwareSchema =
|
|
4166
|
-
platform:
|
|
4167
|
-
arch:
|
|
4168
|
-
cpuModel:
|
|
4169
|
-
cpuCores:
|
|
4170
|
-
totalRAM_MB:
|
|
4171
|
-
availableRAM_MB:
|
|
4172
|
-
gpu:
|
|
4173
|
-
type:
|
|
4174
|
-
name:
|
|
4661
|
+
var HardwareSchema = z16.looseObject({
|
|
4662
|
+
platform: z16.enum(["darwin", "linux", "win32"]),
|
|
4663
|
+
arch: z16.enum(["arm64", "x64"]),
|
|
4664
|
+
cpuModel: z16.string(),
|
|
4665
|
+
cpuCores: z16.number(),
|
|
4666
|
+
totalRAM_MB: z16.number(),
|
|
4667
|
+
availableRAM_MB: z16.number(),
|
|
4668
|
+
gpu: z16.looseObject({
|
|
4669
|
+
type: z16.enum(["nvidia", "amd", "intel", "apple"]),
|
|
4670
|
+
name: z16.string()
|
|
4175
4671
|
}).nullable(),
|
|
4176
|
-
npu:
|
|
4177
|
-
type:
|
|
4672
|
+
npu: z16.looseObject({
|
|
4673
|
+
type: z16.enum(["apple-ane", "intel-npu", "amd-xdna"])
|
|
4178
4674
|
}).nullable()
|
|
4179
4675
|
});
|
|
4180
|
-
var InferenceDevicesSchema =
|
|
4181
|
-
reachable:
|
|
4182
|
-
devices:
|
|
4183
|
-
|
|
4184
|
-
key:
|
|
4185
|
-
backend:
|
|
4186
|
-
device:
|
|
4187
|
-
available:
|
|
4188
|
-
presence:
|
|
4189
|
-
enabled:
|
|
4190
|
-
exclusion:
|
|
4191
|
-
defaultModelId:
|
|
4676
|
+
var InferenceDevicesSchema = z16.looseObject({
|
|
4677
|
+
reachable: z16.boolean(),
|
|
4678
|
+
devices: z16.array(
|
|
4679
|
+
z16.looseObject({
|
|
4680
|
+
key: z16.string(),
|
|
4681
|
+
backend: z16.string(),
|
|
4682
|
+
device: z16.string(),
|
|
4683
|
+
available: z16.boolean(),
|
|
4684
|
+
presence: z16.enum(["present", "absent", "unknown"]).optional(),
|
|
4685
|
+
enabled: z16.boolean(),
|
|
4686
|
+
exclusion: z16.string().nullable(),
|
|
4687
|
+
defaultModelId: z16.string()
|
|
4192
4688
|
})
|
|
4193
4689
|
)
|
|
4194
4690
|
});
|
|
@@ -4312,81 +4808,81 @@ async function opsAccelerators(ctx) {
|
|
|
4312
4808
|
}
|
|
4313
4809
|
|
|
4314
4810
|
// src/mcp/ops-device.ts
|
|
4315
|
-
import { z as
|
|
4316
|
-
var DeviceSchema =
|
|
4317
|
-
id:
|
|
4318
|
-
name:
|
|
4319
|
-
type:
|
|
4320
|
-
addonId:
|
|
4321
|
-
location:
|
|
4322
|
-
disabled:
|
|
4323
|
-
online:
|
|
4324
|
-
isCamera:
|
|
4325
|
-
features:
|
|
4811
|
+
import { z as z17 } from "zod";
|
|
4812
|
+
var DeviceSchema = z17.looseObject({
|
|
4813
|
+
id: z17.number(),
|
|
4814
|
+
name: z17.string(),
|
|
4815
|
+
type: z17.string().optional(),
|
|
4816
|
+
addonId: z17.string().optional(),
|
|
4817
|
+
location: z17.string().nullable().optional(),
|
|
4818
|
+
disabled: z17.boolean().optional(),
|
|
4819
|
+
online: z17.boolean().optional(),
|
|
4820
|
+
isCamera: z17.boolean().optional(),
|
|
4821
|
+
features: z17.array(z17.string()).optional()
|
|
4326
4822
|
});
|
|
4327
4823
|
var NullableDeviceSchema = DeviceSchema.nullable();
|
|
4328
|
-
var DeviceListSchema =
|
|
4329
|
-
var BindingsSchema2 =
|
|
4330
|
-
entries:
|
|
4331
|
-
|
|
4332
|
-
capName:
|
|
4333
|
-
kind:
|
|
4334
|
-
providerAddonId:
|
|
4335
|
-
providerNodeId:
|
|
4824
|
+
var DeviceListSchema = z17.array(DeviceSchema);
|
|
4825
|
+
var BindingsSchema2 = z17.looseObject({
|
|
4826
|
+
entries: z17.array(
|
|
4827
|
+
z17.looseObject({
|
|
4828
|
+
capName: z17.string(),
|
|
4829
|
+
kind: z17.string(),
|
|
4830
|
+
providerAddonId: z17.string(),
|
|
4831
|
+
providerNodeId: z17.string()
|
|
4336
4832
|
})
|
|
4337
4833
|
)
|
|
4338
4834
|
});
|
|
4339
|
-
var StatusAggregateSchema =
|
|
4340
|
-
var DegradationSchema =
|
|
4341
|
-
stage:
|
|
4342
|
-
reason:
|
|
4343
|
-
elapsedMs:
|
|
4835
|
+
var StatusAggregateSchema = z17.record(z17.string(), z17.unknown());
|
|
4836
|
+
var DegradationSchema = z17.looseObject({
|
|
4837
|
+
stage: z17.string(),
|
|
4838
|
+
reason: z17.string(),
|
|
4839
|
+
elapsedMs: z17.number().optional()
|
|
4344
4840
|
});
|
|
4345
|
-
var CameraStatusSchema =
|
|
4346
|
-
deviceId:
|
|
4347
|
-
assignment:
|
|
4348
|
-
detectionNodeId:
|
|
4349
|
-
decoderNodeId:
|
|
4350
|
-
sourceNodeId:
|
|
4841
|
+
var CameraStatusSchema = z17.looseObject({
|
|
4842
|
+
deviceId: z17.number(),
|
|
4843
|
+
assignment: z17.looseObject({
|
|
4844
|
+
detectionNodeId: z17.string().nullable(),
|
|
4845
|
+
decoderNodeId: z17.string().nullable(),
|
|
4846
|
+
sourceNodeId: z17.string().nullable(),
|
|
4351
4847
|
/** Why a stage sits where it does; `armed:on-motion` means idle by design. */
|
|
4352
|
-
reasons:
|
|
4848
|
+
reasons: z17.record(z17.string(), z17.string().optional()).optional()
|
|
4353
4849
|
}),
|
|
4354
|
-
broker:
|
|
4355
|
-
profiles:
|
|
4356
|
-
|
|
4357
|
-
profile:
|
|
4358
|
-
status:
|
|
4359
|
-
subscribers:
|
|
4850
|
+
broker: z17.looseObject({
|
|
4851
|
+
profiles: z17.array(
|
|
4852
|
+
z17.looseObject({
|
|
4853
|
+
profile: z17.string(),
|
|
4854
|
+
status: z17.string(),
|
|
4855
|
+
subscribers: z17.number()
|
|
4360
4856
|
})
|
|
4361
4857
|
)
|
|
4362
4858
|
}).nullable(),
|
|
4363
|
-
detection:
|
|
4364
|
-
nodeId:
|
|
4365
|
-
phase:
|
|
4366
|
-
actualFps:
|
|
4367
|
-
configuredFps:
|
|
4368
|
-
provisioning:
|
|
4369
|
-
state:
|
|
4370
|
-
error:
|
|
4859
|
+
detection: z17.looseObject({
|
|
4860
|
+
nodeId: z17.string(),
|
|
4861
|
+
phase: z17.string(),
|
|
4862
|
+
actualFps: z17.number(),
|
|
4863
|
+
configuredFps: z17.number(),
|
|
4864
|
+
provisioning: z17.looseObject({
|
|
4865
|
+
state: z17.string(),
|
|
4866
|
+
error: z17.string().optional()
|
|
4371
4867
|
})
|
|
4372
4868
|
}).nullable(),
|
|
4373
|
-
recording:
|
|
4374
|
-
mode:
|
|
4375
|
-
active:
|
|
4376
|
-
storageBytes:
|
|
4869
|
+
recording: z17.looseObject({
|
|
4870
|
+
mode: z17.string(),
|
|
4871
|
+
active: z17.boolean(),
|
|
4872
|
+
storageBytes: z17.number()
|
|
4377
4873
|
}).nullable(),
|
|
4378
|
-
switchedOff:
|
|
4379
|
-
degraded:
|
|
4380
|
-
fetchedAt:
|
|
4874
|
+
switchedOff: z17.array(z17.string()),
|
|
4875
|
+
degraded: z17.array(DegradationSchema),
|
|
4876
|
+
fetchedAt: z17.number()
|
|
4381
4877
|
});
|
|
4382
|
-
var CameraStatusListSchema =
|
|
4383
|
-
var ObjectEventSchema =
|
|
4384
|
-
id:
|
|
4385
|
-
timestamp:
|
|
4386
|
-
className:
|
|
4387
|
-
trackId:
|
|
4878
|
+
var CameraStatusListSchema = z17.array(CameraStatusSchema);
|
|
4879
|
+
var ObjectEventSchema = z17.looseObject({
|
|
4880
|
+
id: z17.number(),
|
|
4881
|
+
timestamp: z17.number(),
|
|
4882
|
+
className: z17.string().optional(),
|
|
4883
|
+
trackId: z17.string().optional()
|
|
4388
4884
|
});
|
|
4389
|
-
var ObjectEventsSchema2 =
|
|
4885
|
+
var ObjectEventsSchema2 = z17.array(ObjectEventSchema);
|
|
4390
4886
|
var BINDING_LIMIT = 40;
|
|
4391
4887
|
var EVENT_LIMIT = 5;
|
|
4392
4888
|
var UNHEALTHY_LIMIT = 50;
|
|
@@ -4394,9 +4890,9 @@ function freshness(aggregate, now) {
|
|
|
4394
4890
|
const fetched = [];
|
|
4395
4891
|
let changed = null;
|
|
4396
4892
|
for (const [capName, value] of Object.entries(aggregate)) {
|
|
4397
|
-
const parsed =
|
|
4398
|
-
lastFetchedAt:
|
|
4399
|
-
lastChangedAt:
|
|
4893
|
+
const parsed = z17.looseObject({
|
|
4894
|
+
lastFetchedAt: z17.number().optional(),
|
|
4895
|
+
lastChangedAt: z17.number().optional()
|
|
4400
4896
|
}).safeParse(value);
|
|
4401
4897
|
if (!parsed.success) continue;
|
|
4402
4898
|
const at = parsed.data.lastFetchedAt;
|
|
@@ -4647,26 +5143,26 @@ async function opsUnhealthyDevices(ctx) {
|
|
|
4647
5143
|
}
|
|
4648
5144
|
|
|
4649
5145
|
// src/mcp/ops-logs.ts
|
|
4650
|
-
import { z as
|
|
4651
|
-
var LevelSchema =
|
|
4652
|
-
var LogEntrySchema =
|
|
4653
|
-
timestamp:
|
|
4654
|
-
level:
|
|
4655
|
-
scope:
|
|
4656
|
-
message:
|
|
4657
|
-
tags:
|
|
4658
|
-
meta:
|
|
5146
|
+
import { z as z18 } from "zod";
|
|
5147
|
+
var LevelSchema = z18.enum(["debug", "info", "warn", "error"]);
|
|
5148
|
+
var LogEntrySchema = z18.looseObject({
|
|
5149
|
+
timestamp: z18.string(),
|
|
5150
|
+
level: z18.string(),
|
|
5151
|
+
scope: z18.string().optional(),
|
|
5152
|
+
message: z18.string(),
|
|
5153
|
+
tags: z18.record(z18.string(), z18.string().optional()).optional(),
|
|
5154
|
+
meta: z18.record(z18.string(), z18.unknown()).optional()
|
|
4659
5155
|
});
|
|
4660
|
-
var LogPageSchema =
|
|
4661
|
-
entries:
|
|
4662
|
-
done:
|
|
4663
|
-
source:
|
|
5156
|
+
var LogPageSchema = z18.looseObject({
|
|
5157
|
+
entries: z18.array(LogEntrySchema),
|
|
5158
|
+
done: z18.boolean(),
|
|
5159
|
+
source: z18.string()
|
|
4664
5160
|
});
|
|
4665
|
-
var LogSourcesSchema =
|
|
4666
|
-
|
|
4667
|
-
addonId:
|
|
4668
|
-
readable:
|
|
4669
|
-
isDefault:
|
|
5161
|
+
var LogSourcesSchema = z18.array(
|
|
5162
|
+
z18.looseObject({
|
|
5163
|
+
addonId: z18.string(),
|
|
5164
|
+
readable: z18.boolean(),
|
|
5165
|
+
isDefault: z18.boolean()
|
|
4670
5166
|
})
|
|
4671
5167
|
);
|
|
4672
5168
|
var RING_SOURCE = "ring";
|
|
@@ -4881,37 +5377,37 @@ async function opsLogs(ctx, args, now = Date.now()) {
|
|
|
4881
5377
|
}
|
|
4882
5378
|
|
|
4883
5379
|
// src/mcp/ops-status.ts
|
|
4884
|
-
import { z as
|
|
4885
|
-
var TopologyNodeSchema =
|
|
4886
|
-
id:
|
|
4887
|
-
name:
|
|
4888
|
-
platform:
|
|
4889
|
-
isHub:
|
|
4890
|
-
isOnline:
|
|
4891
|
-
addons:
|
|
5380
|
+
import { z as z19 } from "zod";
|
|
5381
|
+
var TopologyNodeSchema = z19.looseObject({
|
|
5382
|
+
id: z19.string(),
|
|
5383
|
+
name: z19.string().optional(),
|
|
5384
|
+
platform: z19.string().optional(),
|
|
5385
|
+
isHub: z19.boolean().optional(),
|
|
5386
|
+
isOnline: z19.boolean(),
|
|
5387
|
+
addons: z19.array(z19.looseObject({ capabilities: z19.array(z19.string()) })).optional(),
|
|
4892
5388
|
/** Agents report it; the hub does not. Absent means "not claimed". */
|
|
4893
|
-
capabilitiesRegistered:
|
|
5389
|
+
capabilitiesRegistered: z19.boolean().optional()
|
|
4894
5390
|
});
|
|
4895
|
-
var TopologySchema =
|
|
4896
|
-
var ServerStatusSchema =
|
|
4897
|
-
runningVersion:
|
|
4898
|
-
restartRequired:
|
|
4899
|
-
updateAvailable:
|
|
4900
|
-
updateState:
|
|
5391
|
+
var TopologySchema = z19.array(TopologyNodeSchema);
|
|
5392
|
+
var ServerStatusSchema = z19.looseObject({
|
|
5393
|
+
runningVersion: z19.string(),
|
|
5394
|
+
restartRequired: z19.boolean().optional(),
|
|
5395
|
+
updateAvailable: z19.boolean().optional(),
|
|
5396
|
+
updateState: z19.string().optional()
|
|
4901
5397
|
});
|
|
4902
|
-
var AlertSchema =
|
|
4903
|
-
severity:
|
|
4904
|
-
title:
|
|
4905
|
-
message:
|
|
4906
|
-
status:
|
|
5398
|
+
var AlertSchema = z19.looseObject({
|
|
5399
|
+
severity: z19.string(),
|
|
5400
|
+
title: z19.string(),
|
|
5401
|
+
message: z19.string().optional(),
|
|
5402
|
+
status: z19.string()
|
|
4907
5403
|
});
|
|
4908
|
-
var AlertsSchema =
|
|
4909
|
-
var AddonUpdateSchema =
|
|
4910
|
-
name:
|
|
4911
|
-
currentVersion:
|
|
4912
|
-
latestVersion:
|
|
5404
|
+
var AlertsSchema = z19.array(AlertSchema);
|
|
5405
|
+
var AddonUpdateSchema = z19.looseObject({
|
|
5406
|
+
name: z19.string(),
|
|
5407
|
+
currentVersion: z19.string(),
|
|
5408
|
+
latestVersion: z19.string()
|
|
4913
5409
|
});
|
|
4914
|
-
var AddonUpdatesSchema =
|
|
5410
|
+
var AddonUpdatesSchema = z19.array(AddonUpdateSchema);
|
|
4915
5411
|
var NODE_LIMIT2 = 40;
|
|
4916
5412
|
var ALERT_LIMIT = 10;
|
|
4917
5413
|
var UPDATE_LIMIT = 20;
|
|
@@ -5040,195 +5536,189 @@ async function opsStatus(ctx) {
|
|
|
5040
5536
|
});
|
|
5041
5537
|
}
|
|
5042
5538
|
|
|
5043
|
-
// src/mcp/ops-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5539
|
+
// src/mcp/ops-update-apply.ts
|
|
5540
|
+
import {
|
|
5541
|
+
runGated as runGated3
|
|
5542
|
+
} from "@camstack/develop";
|
|
5543
|
+
import { z as z25 } from "zod";
|
|
5544
|
+
|
|
5545
|
+
// src/mcp/ops-update-model.ts
|
|
5546
|
+
import { z as z20 } from "zod";
|
|
5547
|
+
var ImageContractShape = z20.looseObject({
|
|
5548
|
+
state: z20.enum(["in-sync", "behind-patch", "behind-series", "ahead", "unknown"]),
|
|
5549
|
+
seedVersion: z20.string().nullable(),
|
|
5550
|
+
contractVersion: z20.string().nullable(),
|
|
5551
|
+
message: z20.string().optional()
|
|
5552
|
+
});
|
|
5553
|
+
var WrapperShape = z20.looseObject({
|
|
5554
|
+
kind: z20.enum(["docker", "electron", "native", "contradictory", "unknown"]),
|
|
5555
|
+
currentVersion: z20.string().nullable().optional()
|
|
5556
|
+
});
|
|
5557
|
+
var ServerStatusShape2 = z20.looseObject({
|
|
5558
|
+
runningVersion: z20.string().nullable(),
|
|
5559
|
+
installedVersion: z20.string().nullable().optional(),
|
|
5560
|
+
latestVersion: z20.string().nullable().optional(),
|
|
5561
|
+
pendingVersion: z20.string().nullable().optional(),
|
|
5562
|
+
restartRequired: z20.boolean().optional(),
|
|
5563
|
+
updateAvailable: z20.boolean().optional(),
|
|
5564
|
+
updateState: z20.string().optional(),
|
|
5565
|
+
lastCheckedAtMs: z20.number().nullable().optional(),
|
|
5566
|
+
rolledBack: z20.looseObject({
|
|
5567
|
+
fromVersion: z20.string(),
|
|
5568
|
+
toVersion: z20.string().nullable(),
|
|
5569
|
+
reason: z20.string()
|
|
5570
|
+
}).nullable().optional(),
|
|
5571
|
+
imageContract: ImageContractShape.optional(),
|
|
5572
|
+
wrapper: WrapperShape.optional()
|
|
5573
|
+
});
|
|
5574
|
+
var AddonUpdateShape = z20.looseObject({
|
|
5575
|
+
name: z20.string(),
|
|
5576
|
+
currentVersion: z20.string(),
|
|
5577
|
+
latestVersion: z20.string(),
|
|
5578
|
+
category: z20.enum(["addon", "core"]),
|
|
5579
|
+
requiresRestart: z20.boolean().optional(),
|
|
5580
|
+
isSystem: z20.boolean()
|
|
5581
|
+
});
|
|
5582
|
+
var InstallationsShape2 = z20.looseObject({
|
|
5583
|
+
installations: z20.array(
|
|
5584
|
+
z20.looseObject({
|
|
5585
|
+
packageName: z20.string(),
|
|
5586
|
+
nodeId: z20.string(),
|
|
5587
|
+
isHub: z20.boolean(),
|
|
5588
|
+
isOnline: z20.boolean(),
|
|
5589
|
+
version: z20.string().nullable(),
|
|
5590
|
+
onHub: z20.boolean().optional()
|
|
5591
|
+
})
|
|
5592
|
+
),
|
|
5593
|
+
unreachable: z20.array(z20.looseObject({ nodeId: z20.string(), error: z20.string() }))
|
|
5594
|
+
});
|
|
5595
|
+
var ServerActionShape = z20.looseObject({
|
|
5596
|
+
accepted: z20.boolean().optional(),
|
|
5597
|
+
outcome: z20.enum(["staged", "restart-scheduled", "already-current", "refused", "failed"]).optional(),
|
|
5598
|
+
reason: z20.string().nullable().optional(),
|
|
5599
|
+
error: z20.string().nullable().optional(),
|
|
5600
|
+
message: z20.string().optional(),
|
|
5601
|
+
blockers: z20.array(z20.looseObject({ addonId: z20.string(), reason: z20.string() })).optional()
|
|
5602
|
+
});
|
|
5603
|
+
function statusInput(node) {
|
|
5604
|
+
return node.isHub === true ? void 0 : { nodeId: node.id };
|
|
5605
|
+
}
|
|
5606
|
+
function readTopology(client) {
|
|
5607
|
+
return readQuery(client, "nodes.topology", void 0, TopologySchema);
|
|
5608
|
+
}
|
|
5609
|
+
function readNodeStatus(client, node) {
|
|
5610
|
+
return readQuery(
|
|
5611
|
+
client,
|
|
5612
|
+
"serverManagement.getServerPackageStatus",
|
|
5613
|
+
statusInput(node),
|
|
5614
|
+
ServerStatusShape2
|
|
5615
|
+
);
|
|
5616
|
+
}
|
|
5617
|
+
function readInstallations(client) {
|
|
5618
|
+
return readQuery(client, "addons.listInstallations", {}, InstallationsShape2);
|
|
5619
|
+
}
|
|
5620
|
+
function serverVerdict(status) {
|
|
5621
|
+
return {
|
|
5622
|
+
running: status.runningVersion,
|
|
5623
|
+
installed: status.installedVersion ?? null,
|
|
5624
|
+
latest: status.latestVersion ?? null,
|
|
5625
|
+
pending: status.pendingVersion ?? null,
|
|
5626
|
+
updateAvailable: status.updateAvailable ?? null,
|
|
5627
|
+
restartRequired: status.restartRequired ?? null,
|
|
5628
|
+
updateState: status.updateState ?? null,
|
|
5629
|
+
rolledBack: status.rolledBack ? {
|
|
5630
|
+
from: status.rolledBack.fromVersion,
|
|
5631
|
+
to: status.rolledBack.toVersion,
|
|
5632
|
+
reason: status.rolledBack.reason
|
|
5633
|
+
} : null,
|
|
5634
|
+
latestCheckedAt: status.lastCheckedAtMs !== void 0 && status.lastCheckedAtMs !== null ? new Date(status.lastCheckedAtMs).toISOString() : null
|
|
5635
|
+
};
|
|
5636
|
+
}
|
|
5637
|
+
function wrapperInstructions(kind) {
|
|
5638
|
+
switch (kind) {
|
|
5639
|
+
case "docker":
|
|
5640
|
+
return [
|
|
5641
|
+
"The image is replaced, not the closure: pull the new image and RECREATE the container (a plain restart keeps the old image).",
|
|
5642
|
+
"Docker Compose: `docker compose pull` then `docker compose up -d`.",
|
|
5643
|
+
'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).',
|
|
5644
|
+
'Proxmox LXC: recreate the container the way it was installed; `setup_install_guide` with hostKind "proxmox-lxc" returns the official steps.',
|
|
5645
|
+
"Back up the data volume first; no tool here does it."
|
|
5646
|
+
];
|
|
5647
|
+
case "electron":
|
|
5648
|
+
return [
|
|
5649
|
+
"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.",
|
|
5650
|
+
"The server closure update does not change the app bundle."
|
|
5651
|
+
];
|
|
5652
|
+
case "native":
|
|
5653
|
+
return [
|
|
5654
|
+
"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."
|
|
5655
|
+
];
|
|
5656
|
+
default:
|
|
5657
|
+
return [
|
|
5658
|
+
"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."
|
|
5659
|
+
];
|
|
5052
5660
|
}
|
|
5053
|
-
return out;
|
|
5054
5661
|
}
|
|
5055
|
-
|
|
5056
|
-
const
|
|
5057
|
-
|
|
5058
|
-
|
|
5662
|
+
function wrapperVerdict(status) {
|
|
5663
|
+
const kind = status.wrapper?.kind ?? null;
|
|
5664
|
+
const contract = status.imageContract;
|
|
5665
|
+
if (contract === void 0) {
|
|
5666
|
+
return {
|
|
5667
|
+
kind,
|
|
5668
|
+
behind: null,
|
|
5669
|
+
state: null,
|
|
5670
|
+
from: null,
|
|
5671
|
+
to: null,
|
|
5672
|
+
message: "this node reports no image contract (its server predates it); the shell is unverified",
|
|
5673
|
+
instructions: []
|
|
5674
|
+
};
|
|
5675
|
+
}
|
|
5676
|
+
const behind = contract.state === "behind-patch" || contract.state === "behind-series" ? true : contract.state === "in-sync" ? false : null;
|
|
5059
5677
|
return {
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5678
|
+
kind,
|
|
5679
|
+
behind,
|
|
5680
|
+
state: contract.state,
|
|
5681
|
+
from: contract.seedVersion,
|
|
5682
|
+
to: contract.contractVersion,
|
|
5683
|
+
message: contract.message ?? null,
|
|
5684
|
+
instructions: behind === true ? wrapperInstructions(kind) : []
|
|
5065
5685
|
};
|
|
5066
5686
|
}
|
|
5067
|
-
function
|
|
5068
|
-
|
|
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))
|
|
5151
|
-
);
|
|
5687
|
+
function needsServerUpdate(verdict) {
|
|
5688
|
+
return verdict.updateAvailable === true || verdict.restartRequired === true;
|
|
5152
5689
|
}
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
function
|
|
5158
|
-
|
|
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
|
|
5690
|
+
function capState(node) {
|
|
5691
|
+
const count = distinctCaps(node);
|
|
5692
|
+
return { count, capless: isCapless(node, count) };
|
|
5693
|
+
}
|
|
5694
|
+
function addonDrift(installations) {
|
|
5695
|
+
const hubVersion = /* @__PURE__ */ new Map();
|
|
5696
|
+
for (const row of installations.installations) {
|
|
5697
|
+
if (row.isHub && row.version !== null) hubVersion.set(row.packageName, row.version);
|
|
5698
|
+
}
|
|
5699
|
+
const drift = [];
|
|
5700
|
+
for (const row of installations.installations) {
|
|
5701
|
+
if (row.isHub) continue;
|
|
5702
|
+
const hub = hubVersion.get(row.packageName);
|
|
5703
|
+
if (hub === void 0) continue;
|
|
5704
|
+
if (row.version !== hub) {
|
|
5705
|
+
drift.push({
|
|
5706
|
+
packageName: row.packageName,
|
|
5707
|
+
nodeId: row.nodeId,
|
|
5708
|
+
hubVersion: hub,
|
|
5709
|
+
nodeVersion: row.version
|
|
5196
5710
|
});
|
|
5197
|
-
return { deviceId: args.deviceId, name: args.name };
|
|
5198
5711
|
}
|
|
5199
|
-
};
|
|
5200
|
-
}
|
|
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") {
|
|
5205
|
-
return {
|
|
5206
|
-
status: "refused",
|
|
5207
|
-
reason: outcome.connection.adminHint ?? "this credential is not admin-scoped"
|
|
5208
|
-
};
|
|
5209
5712
|
}
|
|
5210
|
-
return
|
|
5211
|
-
}
|
|
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);
|
|
5713
|
+
return drift;
|
|
5227
5714
|
}
|
|
5228
5715
|
|
|
5716
|
+
// src/mcp/ops-update-cli.ts
|
|
5717
|
+
import { execFile } from "child_process";
|
|
5718
|
+
|
|
5229
5719
|
// src/mcp/setup-tools.ts
|
|
5230
|
-
import { text as
|
|
5231
|
-
import { z as
|
|
5720
|
+
import { text as text11 } from "@camstack/develop";
|
|
5721
|
+
import { z as z24 } from "zod";
|
|
5232
5722
|
|
|
5233
5723
|
// src/mcp/setup-hubs.ts
|
|
5234
5724
|
import * as https from "https";
|
|
@@ -5241,13 +5731,13 @@ import {
|
|
|
5241
5731
|
normalizeHubAddress,
|
|
5242
5732
|
resolveHubTls
|
|
5243
5733
|
} from "@camstack/develop";
|
|
5244
|
-
import { z as
|
|
5734
|
+
import { z as z23 } from "zod";
|
|
5245
5735
|
|
|
5246
5736
|
// src/mcp/setup-install.ts
|
|
5247
|
-
import { z as
|
|
5737
|
+
import { z as z22 } from "zod";
|
|
5248
5738
|
|
|
5249
5739
|
// src/mcp/setup-install-rules.ts
|
|
5250
|
-
import { z as
|
|
5740
|
+
import { z as z21 } from "zod";
|
|
5251
5741
|
var HOST_KINDS = [
|
|
5252
5742
|
"docker-compose",
|
|
5253
5743
|
"docker",
|
|
@@ -5255,24 +5745,24 @@ var HOST_KINDS = [
|
|
|
5255
5745
|
"proxmox-lxc",
|
|
5256
5746
|
"macos-desktop"
|
|
5257
5747
|
];
|
|
5258
|
-
var HostKindSchema =
|
|
5748
|
+
var HostKindSchema = z21.enum(HOST_KINDS);
|
|
5259
5749
|
var ROLES = ["hub", "agent"];
|
|
5260
|
-
var RoleSchema =
|
|
5750
|
+
var RoleSchema = z21.enum(ROLES);
|
|
5261
5751
|
var ACCELERATORS = ["intel", "amd", "coral", "apple", "cpu"];
|
|
5262
|
-
var AcceleratorSchema =
|
|
5263
|
-
var InstallPathsSchema =
|
|
5264
|
-
data:
|
|
5265
|
-
recordings:
|
|
5266
|
-
config:
|
|
5267
|
-
backups:
|
|
5752
|
+
var AcceleratorSchema = z21.enum(ACCELERATORS);
|
|
5753
|
+
var InstallPathsSchema = z21.object({
|
|
5754
|
+
data: z21.string().min(1),
|
|
5755
|
+
recordings: z21.string().min(1),
|
|
5756
|
+
config: z21.string().min(1).optional(),
|
|
5757
|
+
backups: z21.string().min(1).optional()
|
|
5268
5758
|
});
|
|
5269
|
-
var InstallGuideInputSchema =
|
|
5759
|
+
var InstallGuideInputSchema = z21.object({
|
|
5270
5760
|
hostKind: HostKindSchema.optional(),
|
|
5271
5761
|
role: RoleSchema.optional(),
|
|
5272
5762
|
accelerator: AcceleratorSchema.optional(),
|
|
5273
5763
|
paths: InstallPathsSchema.optional(),
|
|
5274
5764
|
/** Agents only: the hub the agent should dial (host or IP). */
|
|
5275
|
-
hubAddress:
|
|
5765
|
+
hubAddress: z21.string().min(1).optional()
|
|
5276
5766
|
});
|
|
5277
5767
|
var RAW = "https://raw.githubusercontent.com/camstack";
|
|
5278
5768
|
var TEMPLATE_URLS = {
|
|
@@ -5411,20 +5901,20 @@ function readService(body) {
|
|
|
5411
5901
|
if (raw.trim() === "" || raw.trim().startsWith("#")) continue;
|
|
5412
5902
|
const line = stripComment(raw);
|
|
5413
5903
|
const indent = line.length - line.trimStart().length;
|
|
5414
|
-
const
|
|
5904
|
+
const text15 = line.trim();
|
|
5415
5905
|
if (indent === 4) {
|
|
5416
5906
|
sub = null;
|
|
5417
|
-
const m = /^([A-Za-z_]+):\s*(.*)$/.exec(
|
|
5907
|
+
const m = /^([A-Za-z_]+):\s*(.*)$/.exec(text15);
|
|
5418
5908
|
if (m === null) continue;
|
|
5419
5909
|
key = m[1] ?? null;
|
|
5420
5910
|
if (key !== null && (m[2] ?? "") !== "") fields.scalars.set(key, unquote(m[2] ?? ""));
|
|
5421
5911
|
} else if (indent === 6 && key !== null) {
|
|
5422
|
-
if (
|
|
5912
|
+
if (text15.startsWith("- ")) {
|
|
5423
5913
|
const list = fields.lists.get(key) ?? [];
|
|
5424
|
-
list.push(unquote(
|
|
5914
|
+
list.push(unquote(text15.slice(2)));
|
|
5425
5915
|
fields.lists.set(key, list);
|
|
5426
5916
|
} else {
|
|
5427
|
-
const m = /^([A-Za-z_-]+):\s*(.*)$/.exec(
|
|
5917
|
+
const m = /^([A-Za-z_-]+):\s*(.*)$/.exec(text15);
|
|
5428
5918
|
if (m === null) continue;
|
|
5429
5919
|
sub = m[1] ?? null;
|
|
5430
5920
|
const map = fields.maps.get(key) ?? /* @__PURE__ */ new Map();
|
|
@@ -5432,7 +5922,7 @@ function readService(body) {
|
|
|
5432
5922
|
fields.maps.set(key, map);
|
|
5433
5923
|
}
|
|
5434
5924
|
} else if (indent === 8 && key !== null && sub !== null) {
|
|
5435
|
-
const m = /^([A-Za-z_-]+):\s*(.*)$/.exec(
|
|
5925
|
+
const m = /^([A-Za-z_-]+):\s*(.*)$/.exec(text15);
|
|
5436
5926
|
if (m === null) continue;
|
|
5437
5927
|
const map = fields.maps.get(`${key}.${sub}`) ?? /* @__PURE__ */ new Map();
|
|
5438
5928
|
map.set(m[1] ?? "", unquote(m[2] ?? ""));
|
|
@@ -5532,9 +6022,9 @@ async function fetchText(url, fetchFn) {
|
|
|
5532
6022
|
return { ok: false, failure: { status: "fetch-failed", url, error } };
|
|
5533
6023
|
}
|
|
5534
6024
|
}
|
|
5535
|
-
var ReleaseSchema =
|
|
5536
|
-
tag_name:
|
|
5537
|
-
assets:
|
|
6025
|
+
var ReleaseSchema = z22.object({
|
|
6026
|
+
tag_name: z22.string(),
|
|
6027
|
+
assets: z22.array(z22.object({ name: z22.string(), browser_download_url: z22.string() }))
|
|
5538
6028
|
});
|
|
5539
6029
|
var DMG = /^CamStack-[0-9][0-9A-Za-z.-]*-(arm64|x64)\.dmg$/;
|
|
5540
6030
|
function acceleratorEdits(accelerator, role) {
|
|
@@ -5825,15 +6315,15 @@ async function connectTool(server, deps) {
|
|
|
5825
6315
|
next: session === null ? LOGIN(url) : "a session exists: the ops tools can use it"
|
|
5826
6316
|
};
|
|
5827
6317
|
}
|
|
5828
|
-
var DiscoveredAgentsSchema =
|
|
5829
|
-
agents:
|
|
5830
|
-
|
|
5831
|
-
nodeId:
|
|
5832
|
-
host:
|
|
5833
|
-
port:
|
|
5834
|
-
version:
|
|
5835
|
-
name:
|
|
5836
|
-
state:
|
|
6318
|
+
var DiscoveredAgentsSchema = z23.object({
|
|
6319
|
+
agents: z23.array(
|
|
6320
|
+
z23.object({
|
|
6321
|
+
nodeId: z23.string(),
|
|
6322
|
+
host: z23.string(),
|
|
6323
|
+
port: z23.number(),
|
|
6324
|
+
version: z23.string().nullable(),
|
|
6325
|
+
name: z23.string().nullable(),
|
|
6326
|
+
state: z23.enum(["unpaired", "pending", "paired"])
|
|
5837
6327
|
})
|
|
5838
6328
|
)
|
|
5839
6329
|
});
|
|
@@ -5880,7 +6370,7 @@ async function addNodeTool(input, ctx) {
|
|
|
5880
6370
|
var SUPPORTED_HOST_KINDS = HOST_KINDS;
|
|
5881
6371
|
var REGISTRY2 = "https://registry.npmjs.org";
|
|
5882
6372
|
var REGISTRY_TIMEOUT_MS2 = 5e3;
|
|
5883
|
-
var RegistryLatestSchema =
|
|
6373
|
+
var RegistryLatestSchema = z24.object({ version: z24.string() });
|
|
5884
6374
|
async function latestOnNpm(name, fetchFn) {
|
|
5885
6375
|
const url = `${REGISTRY2}/${name.replace("/", "%2f")}/latest`;
|
|
5886
6376
|
try {
|
|
@@ -5914,7 +6404,7 @@ function registerSetupTools(server, ctx, deps = {}) {
|
|
|
5914
6404
|
"setup_about",
|
|
5915
6405
|
"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
6406
|
{},
|
|
5917
|
-
async () =>
|
|
6407
|
+
async () => text11(await about(ctx))
|
|
5918
6408
|
);
|
|
5919
6409
|
server.tool(
|
|
5920
6410
|
"setup_install_guide",
|
|
@@ -5924,21 +6414,21 @@ function registerSetupTools(server, ctx, deps = {}) {
|
|
|
5924
6414
|
role: RoleSchema.optional(),
|
|
5925
6415
|
accelerator: AcceleratorSchema.optional(),
|
|
5926
6416
|
paths: InstallPathsSchema.optional(),
|
|
5927
|
-
hubAddress:
|
|
6417
|
+
hubAddress: z24.string().min(1).optional().describe("Agents only: the hub to dial.")
|
|
5928
6418
|
},
|
|
5929
|
-
async (input) =>
|
|
6419
|
+
async (input) => text11(await installGuide(input, ctx.fetch))
|
|
5930
6420
|
);
|
|
5931
6421
|
server.tool(
|
|
5932
6422
|
"setup_discover_hubs",
|
|
5933
6423
|
"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 }) =>
|
|
6424
|
+
{ timeoutMs: z24.number().int().min(200).max(15e3).optional() },
|
|
6425
|
+
async ({ timeoutMs }) => text11(await discoverHubsTool(timeoutMs, deps))
|
|
5936
6426
|
);
|
|
5937
6427
|
server.tool(
|
|
5938
6428
|
"setup_connect",
|
|
5939
6429
|
"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 }) =>
|
|
6430
|
+
{ server: z24.string().min(1) },
|
|
6431
|
+
async ({ server: address }) => text11(await connectTool(address, deps))
|
|
5942
6432
|
);
|
|
5943
6433
|
server.tool(
|
|
5944
6434
|
"setup_add_node",
|
|
@@ -5947,18 +6437,749 @@ function registerSetupTools(server, ctx, deps = {}) {
|
|
|
5947
6437
|
hostKind: HostKindSchema.optional(),
|
|
5948
6438
|
accelerator: AcceleratorSchema.optional(),
|
|
5949
6439
|
paths: InstallPathsSchema.optional(),
|
|
5950
|
-
hubAddress:
|
|
6440
|
+
hubAddress: z24.string().min(1).optional()
|
|
5951
6441
|
},
|
|
5952
|
-
async (input) =>
|
|
6442
|
+
async (input) => text11(await addNodeTool(input, ctx))
|
|
5953
6443
|
);
|
|
5954
6444
|
}
|
|
5955
6445
|
|
|
6446
|
+
// src/mcp/ops-update-cli.ts
|
|
6447
|
+
var CLI_PACKAGE = "camstack";
|
|
6448
|
+
var NPM_GLOBAL = /[\\/]lib[\\/]node_modules[\\/]camstack[\\/]|[\\/]npm[\\/]node_modules[\\/]camstack[\\/]/;
|
|
6449
|
+
var OTHER_MANAGER = /[\\/]\.bun[\\/]|[\\/]pnpm[\\/]|[\\/]\.yarn[\\/]|[\\/]yarn[\\/]|[\\/]\.volta[\\/]/;
|
|
6450
|
+
function detectCliInstall(entryPath) {
|
|
6451
|
+
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).`;
|
|
6452
|
+
if (/[\\/]_npx[\\/]/.test(entryPath)) {
|
|
6453
|
+
return {
|
|
6454
|
+
method: "npx",
|
|
6455
|
+
command: null,
|
|
6456
|
+
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.`
|
|
6457
|
+
};
|
|
6458
|
+
}
|
|
6459
|
+
if (OTHER_MANAGER.test(entryPath) || !NPM_GLOBAL.test(entryPath)) {
|
|
6460
|
+
return { method: "unknown", command: null, instructions: manual };
|
|
6461
|
+
}
|
|
6462
|
+
return {
|
|
6463
|
+
method: "npm-global",
|
|
6464
|
+
command: ["npm", "install", "-g", `${CLI_PACKAGE}@latest`],
|
|
6465
|
+
instructions: `npm install -g ${CLI_PACKAGE}@latest`
|
|
6466
|
+
};
|
|
6467
|
+
}
|
|
6468
|
+
var runCommand = (file, args) => new Promise((resolve) => {
|
|
6469
|
+
execFile(file, [...args], { timeout: 5 * 6e4 }, (err, stdout, stderr) => {
|
|
6470
|
+
const out = `${stdout}${stderr}`.trim().slice(-600);
|
|
6471
|
+
resolve({
|
|
6472
|
+
ok: err === null,
|
|
6473
|
+
output: err ? `${err.message} ${out}`.trim() : out
|
|
6474
|
+
});
|
|
6475
|
+
});
|
|
6476
|
+
});
|
|
6477
|
+
function parts(v) {
|
|
6478
|
+
return v.split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
6479
|
+
}
|
|
6480
|
+
function isOlder(a, b) {
|
|
6481
|
+
const x = parts(a);
|
|
6482
|
+
const y = parts(b);
|
|
6483
|
+
for (let i = 0; i < 3; i++) {
|
|
6484
|
+
const d = (x[i] ?? 0) - (y[i] ?? 0);
|
|
6485
|
+
if (d !== 0) return d < 0;
|
|
6486
|
+
}
|
|
6487
|
+
return false;
|
|
6488
|
+
}
|
|
6489
|
+
async function cliVerdict(installed, fetchFn, entryPath) {
|
|
6490
|
+
const latest = await latestOnNpm(CLI_PACKAGE, fetchFn);
|
|
6491
|
+
return {
|
|
6492
|
+
installed,
|
|
6493
|
+
latest: latest.latest,
|
|
6494
|
+
updateAvailable: latest.latest === null ? null : isOlder(installed, latest.latest),
|
|
6495
|
+
...latest.error !== void 0 ? { error: latest.error } : {},
|
|
6496
|
+
install: detectCliInstall(entryPath)
|
|
6497
|
+
};
|
|
6498
|
+
}
|
|
6499
|
+
|
|
6500
|
+
// src/mcp/ops-update-apply.ts
|
|
6501
|
+
var UPDATE_APPLY_TOOL = "ops_update_apply";
|
|
6502
|
+
function timing(deps) {
|
|
6503
|
+
return {
|
|
6504
|
+
sleep: deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms))),
|
|
6505
|
+
now: deps.now ?? Date.now,
|
|
6506
|
+
pollMs: deps.pollMs ?? 5e3,
|
|
6507
|
+
hubTimeoutMs: deps.hubTimeoutMs ?? 10 * 6e4,
|
|
6508
|
+
agentTimeoutMs: deps.agentTimeoutMs ?? 10 * 6e4,
|
|
6509
|
+
addonTimeoutMs: deps.addonTimeoutMs ?? 5 * 6e4
|
|
6510
|
+
};
|
|
6511
|
+
}
|
|
6512
|
+
function needs(p, target, restart) {
|
|
6513
|
+
return restart || p.running !== null && isOlder(p.running, target);
|
|
6514
|
+
}
|
|
6515
|
+
async function readPlan(client, args, ctx, deps) {
|
|
6516
|
+
const topology = await readTopology(client);
|
|
6517
|
+
if (!topology.ok) return { ok: false, problem: topology.reason };
|
|
6518
|
+
const hubNode = topology.value.find((n) => n.isHub === true);
|
|
6519
|
+
if (hubNode === void 0) return { ok: false, problem: "the topology lists no hub node" };
|
|
6520
|
+
const hubStatus = await readNodeStatus(client, hubNode);
|
|
6521
|
+
if (!hubStatus.ok) return { ok: false, problem: hubStatus.reason };
|
|
6522
|
+
const target = args.version ?? hubStatus.value.latestVersion ?? null;
|
|
6523
|
+
if (target === null) {
|
|
6524
|
+
return {
|
|
6525
|
+
ok: false,
|
|
6526
|
+
problem: "the hub has no registry check yet (latestVersion is null); run a check in the admin UI Cluster page or pass `version`"
|
|
6527
|
+
};
|
|
6528
|
+
}
|
|
6529
|
+
const restartFlags = /* @__PURE__ */ new Map();
|
|
6530
|
+
const plans = [];
|
|
6531
|
+
const offline = [];
|
|
6532
|
+
const unreadable = [];
|
|
6533
|
+
for (const node of topology.value) {
|
|
6534
|
+
if (!node.isOnline) {
|
|
6535
|
+
offline.push(node.id);
|
|
6536
|
+
continue;
|
|
6537
|
+
}
|
|
6538
|
+
const status = node.isHub === true ? hubStatus : await readNodeStatus(client, node);
|
|
6539
|
+
if (!status.ok) {
|
|
6540
|
+
unreadable.push({ nodeId: node.id, reason: status.reason });
|
|
6541
|
+
continue;
|
|
6542
|
+
}
|
|
6543
|
+
const v = serverVerdict(status.value);
|
|
6544
|
+
restartFlags.set(node.id, v.restartRequired === true);
|
|
6545
|
+
plans.push({
|
|
6546
|
+
node,
|
|
6547
|
+
running: v.running,
|
|
6548
|
+
busy: v.updateState !== null && v.updateState !== "idle" ? v.updateState : null,
|
|
6549
|
+
wrapper: wrapperVerdict(status.value)
|
|
6550
|
+
});
|
|
6551
|
+
}
|
|
6552
|
+
const updates = args.addons === false ? null : await readQuery(client, "addons.listUpdates", {}, z25.array(AddonUpdateShape));
|
|
6553
|
+
const cli = args.cli === false ? null : await cliVerdict(ctx.cliVersion, ctx.fetch, deps.entryPath ?? process.argv[1] ?? "");
|
|
6554
|
+
const hub = plans.find((p) => p.node.isHub === true) ?? null;
|
|
6555
|
+
return {
|
|
6556
|
+
ok: true,
|
|
6557
|
+
plan: {
|
|
6558
|
+
target,
|
|
6559
|
+
hub,
|
|
6560
|
+
agents: plans.filter((p) => p.node.isHub !== true),
|
|
6561
|
+
offline,
|
|
6562
|
+
unreadable,
|
|
6563
|
+
addons: updates !== null && updates.ok ? updates.value.filter((u) => !u.isSystem && u.category === "addon") : [],
|
|
6564
|
+
cli
|
|
6565
|
+
}
|
|
6566
|
+
};
|
|
6567
|
+
}
|
|
6568
|
+
function serverSteps(plan, restartOf) {
|
|
6569
|
+
const out = [];
|
|
6570
|
+
if (plan.hub !== null && needs(plan.hub, plan.target, restartOf(plan.hub))) out.push(plan.hub);
|
|
6571
|
+
for (const a of plan.agents) if (needs(a, plan.target, restartOf(a))) out.push(a);
|
|
6572
|
+
return out;
|
|
6573
|
+
}
|
|
6574
|
+
async function waitUntil(t, timeoutMs, check) {
|
|
6575
|
+
const deadline = t.now() + timeoutMs;
|
|
6576
|
+
let last = "not checked";
|
|
6577
|
+
for (; ; ) {
|
|
6578
|
+
const why = await check();
|
|
6579
|
+
if (why === null) return { ok: true };
|
|
6580
|
+
last = why;
|
|
6581
|
+
if (t.now() >= deadline)
|
|
6582
|
+
return {
|
|
6583
|
+
ok: false,
|
|
6584
|
+
why: `timed out after ${Math.round(timeoutMs / 1e3)}s: ${last}`
|
|
6585
|
+
};
|
|
6586
|
+
await t.sleep(t.pollMs);
|
|
6587
|
+
}
|
|
6588
|
+
}
|
|
6589
|
+
async function nodeReady(client, nodeId, isHub, target) {
|
|
6590
|
+
const status = await readNodeStatus(client, { id: nodeId, isHub });
|
|
6591
|
+
if (!status.ok) return `status unreadable (${status.reason})`;
|
|
6592
|
+
const v = serverVerdict(status.value);
|
|
6593
|
+
if (v.running !== target) return `still running ${v.running ?? "unknown"}, target ${target}`;
|
|
6594
|
+
if (v.restartRequired === true) return "restart still required";
|
|
6595
|
+
if (v.updateState !== "idle") return `update state ${v.updateState ?? "unknown"}`;
|
|
6596
|
+
const topology = await readTopology(client);
|
|
6597
|
+
if (!topology.ok) return `topology unreadable (${topology.reason})`;
|
|
6598
|
+
const node = topology.value.find((n) => n.id === nodeId);
|
|
6599
|
+
if (node === void 0) return "node absent from the topology";
|
|
6600
|
+
if (!node.isOnline) return "node is not online yet";
|
|
6601
|
+
if (capState(node).capless) return "node is online but registered no capabilities";
|
|
6602
|
+
return null;
|
|
6603
|
+
}
|
|
6604
|
+
async function applyServer(client, p, target, t) {
|
|
6605
|
+
const isHub = p.node.isHub === true;
|
|
6606
|
+
const step = isHub ? "hub-server" : "agent-server";
|
|
6607
|
+
const nodeId = p.node.id;
|
|
6608
|
+
let note = "";
|
|
6609
|
+
try {
|
|
6610
|
+
const raw = await client.call("POST", "serverManagement.applyServerUpdate", {
|
|
6611
|
+
version: target,
|
|
6612
|
+
...statusInput(p.node)
|
|
6613
|
+
});
|
|
6614
|
+
const parsed = ServerActionShape.safeParse(raw);
|
|
6615
|
+
if (parsed.success) {
|
|
6616
|
+
const r = parsed.data;
|
|
6617
|
+
if (r.outcome === "refused" || r.outcome === "failed" || r.accepted === false && r.outcome !== "already-current") {
|
|
6618
|
+
const blockers = (r.blockers ?? []).map((b) => `${b.addonId}: ${b.reason}`).join("; ");
|
|
6619
|
+
return {
|
|
6620
|
+
step,
|
|
6621
|
+
nodeId,
|
|
6622
|
+
status: "failed",
|
|
6623
|
+
detail: `${nodeId} refused the update (${r.outcome ?? "not accepted"}): ${r.reason ?? r.error ?? r.message ?? "no reason given"}${blockers ? ` [would refuse to start: ${blockers}]` : ""}`
|
|
6624
|
+
};
|
|
6625
|
+
}
|
|
6626
|
+
note = r.outcome ?? "";
|
|
6627
|
+
}
|
|
6628
|
+
} catch (err) {
|
|
6629
|
+
note = `no reply (${reasonOf(err)}); expected while the node restarts`;
|
|
6630
|
+
}
|
|
6631
|
+
const wait = await waitUntil(
|
|
6632
|
+
t,
|
|
6633
|
+
isHub ? t.hubTimeoutMs : t.agentTimeoutMs,
|
|
6634
|
+
() => nodeReady(client, nodeId, isHub, target)
|
|
6635
|
+
);
|
|
6636
|
+
if (!wait.ok) {
|
|
6637
|
+
return {
|
|
6638
|
+
step,
|
|
6639
|
+
nodeId,
|
|
6640
|
+
status: "failed",
|
|
6641
|
+
detail: `${nodeId} did not reach ${target}: ${wait.why}`
|
|
6642
|
+
};
|
|
6643
|
+
}
|
|
6644
|
+
return {
|
|
6645
|
+
step,
|
|
6646
|
+
nodeId,
|
|
6647
|
+
status: "ok",
|
|
6648
|
+
detail: `${nodeId} runs ${target}, capabilities registered${note ? ` (${note})` : ""}`
|
|
6649
|
+
};
|
|
6650
|
+
}
|
|
6651
|
+
async function applyAddons(client, addons, t) {
|
|
6652
|
+
const done = [];
|
|
6653
|
+
for (const u of addons) {
|
|
6654
|
+
try {
|
|
6655
|
+
await client.call("POST", "addons.updatePackage", {
|
|
6656
|
+
name: u.name,
|
|
6657
|
+
version: u.latestVersion
|
|
6658
|
+
});
|
|
6659
|
+
done.push(u);
|
|
6660
|
+
} catch (err) {
|
|
6661
|
+
return [
|
|
6662
|
+
{
|
|
6663
|
+
step: "addons",
|
|
6664
|
+
status: "failed",
|
|
6665
|
+
detail: `hub failed to update ${u.name} to ${u.latestVersion}: ${reasonOf(err)}${done.length > 0 ? `; already updated: ${done.map((d) => d.name).join(", ")}` : ""}`
|
|
6666
|
+
}
|
|
6667
|
+
];
|
|
6668
|
+
}
|
|
6669
|
+
}
|
|
6670
|
+
const names = new Set(done.map((d) => d.name));
|
|
6671
|
+
const wait = await waitUntil(t, t.addonTimeoutMs, async () => {
|
|
6672
|
+
const inst = await readInstallations(client);
|
|
6673
|
+
if (!inst.ok) return `installations unreadable (${inst.reason})`;
|
|
6674
|
+
for (const u of done) {
|
|
6675
|
+
const hub = inst.value.installations.find((i) => i.isHub && i.packageName === u.name);
|
|
6676
|
+
if (hub === void 0 || hub.version !== u.latestVersion) {
|
|
6677
|
+
return `${u.name} is ${hub?.version ?? "absent"} on the hub, expected ${u.latestVersion}`;
|
|
6678
|
+
}
|
|
6679
|
+
}
|
|
6680
|
+
const behind = addonDrift({
|
|
6681
|
+
...inst.value,
|
|
6682
|
+
installations: inst.value.installations.filter((i) => names.has(i.packageName) && i.isOnline)
|
|
6683
|
+
});
|
|
6684
|
+
if (behind.length > 0) {
|
|
6685
|
+
return `agents behind the hub: ${behind.map((d) => `${d.nodeId}/${d.packageName}=${d.nodeVersion ?? "absent"}`).join(", ")}`;
|
|
6686
|
+
}
|
|
6687
|
+
return null;
|
|
6688
|
+
});
|
|
6689
|
+
return [
|
|
6690
|
+
{
|
|
6691
|
+
step: "addons",
|
|
6692
|
+
status: wait.ok ? "ok" : "unverified",
|
|
6693
|
+
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}`
|
|
6694
|
+
}
|
|
6695
|
+
];
|
|
6696
|
+
}
|
|
6697
|
+
async function applyCli(cli, run) {
|
|
6698
|
+
const restart = "Restart the MCP server in your client to load the new version; a running stdio MCP cannot swap itself.";
|
|
6699
|
+
if (cli.install.command === null) {
|
|
6700
|
+
return {
|
|
6701
|
+
step: "cli",
|
|
6702
|
+
status: "instructions",
|
|
6703
|
+
detail: `${cli.install.instructions} ${restart}`
|
|
6704
|
+
};
|
|
6705
|
+
}
|
|
6706
|
+
const [file, ...rest] = cli.install.command;
|
|
6707
|
+
const result = await run(file ?? "", rest);
|
|
6708
|
+
if (!result.ok) {
|
|
6709
|
+
return {
|
|
6710
|
+
step: "cli",
|
|
6711
|
+
status: "failed",
|
|
6712
|
+
detail: `\`${cli.install.command.join(" ")}\` failed: ${result.output}`
|
|
6713
|
+
};
|
|
6714
|
+
}
|
|
6715
|
+
return {
|
|
6716
|
+
step: "cli",
|
|
6717
|
+
status: "ok",
|
|
6718
|
+
detail: `ran \`${cli.install.command.join(" ")}\` (${cli.installed} to ${cli.latest ?? "latest"}). ${restart}`
|
|
6719
|
+
};
|
|
6720
|
+
}
|
|
6721
|
+
function updateApplySpec(client, ctx, deps = {}) {
|
|
6722
|
+
const t = timing(deps);
|
|
6723
|
+
return {
|
|
6724
|
+
tool: UPDATE_APPLY_TOOL,
|
|
6725
|
+
async dryRun(args) {
|
|
6726
|
+
const read = await readPlan(client, args, ctx, deps);
|
|
6727
|
+
if (!read.ok) return { confirmable: false, describe: { problem: read.problem } };
|
|
6728
|
+
const plan = read.plan;
|
|
6729
|
+
const steps = serverSteps(plan, () => false);
|
|
6730
|
+
const busy = [plan.hub, ...plan.agents].filter(
|
|
6731
|
+
(p) => p !== null && p.busy !== null
|
|
6732
|
+
);
|
|
6733
|
+
if (busy.length > 0) {
|
|
6734
|
+
return {
|
|
6735
|
+
confirmable: false,
|
|
6736
|
+
describe: {
|
|
6737
|
+
problem: `a node is mid-update: ${busy.map((p) => `${p.node.id} (${p.busy})`).join(", ")}; wait for it to settle`
|
|
6738
|
+
}
|
|
6739
|
+
};
|
|
6740
|
+
}
|
|
6741
|
+
const cliNeeded = plan.cli?.updateAvailable === true;
|
|
6742
|
+
if (steps.length === 0 && plan.addons.length === 0 && !cliNeeded) {
|
|
6743
|
+
return {
|
|
6744
|
+
confirmable: false,
|
|
6745
|
+
describe: {
|
|
6746
|
+
problem: "nothing to apply: every online node runs the target, no addon update is pending and the CLI is current",
|
|
6747
|
+
target: plan.target,
|
|
6748
|
+
wrapperUpdates: wrapperReport(plan)
|
|
6749
|
+
}
|
|
6750
|
+
};
|
|
6751
|
+
}
|
|
6752
|
+
return {
|
|
6753
|
+
confirmable: true,
|
|
6754
|
+
describe: {
|
|
6755
|
+
action: "update the cluster, in this order, verifying each step",
|
|
6756
|
+
target: plan.target,
|
|
6757
|
+
order: [
|
|
6758
|
+
...steps.map((p, i) => ({
|
|
6759
|
+
order: i + 1,
|
|
6760
|
+
step: p.node.isHub === true ? "hub-server" : "agent-server",
|
|
6761
|
+
nodeId: p.node.id,
|
|
6762
|
+
from: p.running,
|
|
6763
|
+
to: plan.target
|
|
6764
|
+
})),
|
|
6765
|
+
...args.addons === false ? [] : [
|
|
6766
|
+
{
|
|
6767
|
+
step: "addons",
|
|
6768
|
+
packages: plan.addons.map(
|
|
6769
|
+
(a) => `${a.name} ${a.currentVersion} -> ${a.latestVersion}`
|
|
6770
|
+
)
|
|
6771
|
+
}
|
|
6772
|
+
],
|
|
6773
|
+
...cliNeeded && plan.cli ? [
|
|
6774
|
+
{
|
|
6775
|
+
step: "cli",
|
|
6776
|
+
from: plan.cli.installed,
|
|
6777
|
+
to: plan.cli.latest,
|
|
6778
|
+
method: plan.cli.install.method,
|
|
6779
|
+
command: plan.cli.install.command?.join(" ") ?? null,
|
|
6780
|
+
instructions: plan.cli.install.command === null ? plan.cli.install.instructions : null
|
|
6781
|
+
}
|
|
6782
|
+
] : []
|
|
6783
|
+
],
|
|
6784
|
+
skippedOffline: plan.offline,
|
|
6785
|
+
skippedUnreadable: plan.unreadable,
|
|
6786
|
+
wrapperUpdates: wrapperReport(plan),
|
|
6787
|
+
timeouts: {
|
|
6788
|
+
hubS: t.hubTimeoutMs / 1e3,
|
|
6789
|
+
agentS: t.agentTimeoutMs / 1e3,
|
|
6790
|
+
addonsS: t.addonTimeoutMs / 1e3
|
|
6791
|
+
},
|
|
6792
|
+
notes: [
|
|
6793
|
+
"The hub restarts during step 1: this connection drops for a minute or two; that is expected.",
|
|
6794
|
+
"A failed step stops the chain and says which node and why; addons are not touched if a server step failed.",
|
|
6795
|
+
"Wrapper (image / app) updates are NEVER performed here; the instructions are listed.",
|
|
6796
|
+
...cliNeeded ? ["The CLI update is the last step; afterwards restart the MCP in your client."] : []
|
|
6797
|
+
]
|
|
6798
|
+
}
|
|
6799
|
+
};
|
|
6800
|
+
},
|
|
6801
|
+
async execute(args) {
|
|
6802
|
+
const read = await readPlan(client, args, ctx, deps);
|
|
6803
|
+
if (!read.ok) return { outcome: "failed", problem: read.problem, steps: [] };
|
|
6804
|
+
const plan = read.plan;
|
|
6805
|
+
const results = [];
|
|
6806
|
+
const ok = () => results.every((r) => r.status === "ok" || r.status === "skipped");
|
|
6807
|
+
for (const p of serverSteps(plan, () => false)) {
|
|
6808
|
+
if (!ok()) break;
|
|
6809
|
+
results.push(await applyServer(client, p, plan.target, t));
|
|
6810
|
+
}
|
|
6811
|
+
const serverOk = ok();
|
|
6812
|
+
if (serverOk && args.addons !== false && plan.addons.length > 0) {
|
|
6813
|
+
results.push(...await applyAddons(client, plan.addons, t));
|
|
6814
|
+
}
|
|
6815
|
+
const clusterOk = results.every((r) => r.status === "ok");
|
|
6816
|
+
if (clusterOk && plan.cli?.updateAvailable === true) {
|
|
6817
|
+
results.push(await applyCli(plan.cli, deps.runCommand ?? runCommand));
|
|
6818
|
+
} else if (plan.cli?.updateAvailable === true) {
|
|
6819
|
+
results.push({
|
|
6820
|
+
step: "cli",
|
|
6821
|
+
status: "skipped",
|
|
6822
|
+
detail: "not run: a cluster step did not succeed"
|
|
6823
|
+
});
|
|
6824
|
+
}
|
|
6825
|
+
const failed = results.find((r) => r.status === "failed");
|
|
6826
|
+
return {
|
|
6827
|
+
outcome: failed ? "failed" : results.every((r) => r.status === "ok") ? "complete" : "partial",
|
|
6828
|
+
target: plan.target,
|
|
6829
|
+
steps: results,
|
|
6830
|
+
...failed ? { stoppedAt: failed.nodeId ?? failed.step, reason: failed.detail } : {},
|
|
6831
|
+
skippedOffline: plan.offline,
|
|
6832
|
+
wrapperUpdates: wrapperReport(plan),
|
|
6833
|
+
mcpRestartNeeded: results.some(
|
|
6834
|
+
(r) => r.step === "cli" && (r.status === "ok" || r.status === "instructions")
|
|
6835
|
+
),
|
|
6836
|
+
next: "Run ops_update_check to confirm; nodes still offline were not touched."
|
|
6837
|
+
};
|
|
6838
|
+
}
|
|
6839
|
+
};
|
|
6840
|
+
}
|
|
6841
|
+
function wrapperReport(plan) {
|
|
6842
|
+
return [plan.hub, ...plan.agents].filter((p) => p !== null && p.wrapper.behind === true).map((p) => ({
|
|
6843
|
+
nodeId: p.node.id,
|
|
6844
|
+
kind: p.wrapper.kind,
|
|
6845
|
+
from: p.wrapper.from,
|
|
6846
|
+
to: p.wrapper.to,
|
|
6847
|
+
instructions: p.wrapper.instructions
|
|
6848
|
+
}));
|
|
6849
|
+
}
|
|
6850
|
+
async function opsUpdateApply(ctx, args, confirmToken, deps = {}) {
|
|
6851
|
+
const outcome = await ctx.hub();
|
|
6852
|
+
if (!outcome.ok) return noSessionResult(outcome);
|
|
6853
|
+
if (outcome.connection.tier !== "admin") {
|
|
6854
|
+
return {
|
|
6855
|
+
status: "refused",
|
|
6856
|
+
reason: outcome.connection.adminHint ?? "this credential is not admin-scoped"
|
|
6857
|
+
};
|
|
6858
|
+
}
|
|
6859
|
+
return runGated3(
|
|
6860
|
+
ctx.gate,
|
|
6861
|
+
updateApplySpec(outcome.connection.client, ctx, deps),
|
|
6862
|
+
args,
|
|
6863
|
+
confirmToken
|
|
6864
|
+
);
|
|
6865
|
+
}
|
|
6866
|
+
|
|
6867
|
+
// src/mcp/ops-update-check.ts
|
|
6868
|
+
import { z as z26 } from "zod";
|
|
6869
|
+
var ADDON_LIMIT = 30;
|
|
6870
|
+
async function opsUpdateCheck(ctx, deps = {}) {
|
|
6871
|
+
return withAdminHub(ctx, async ({ client, server }) => {
|
|
6872
|
+
const unavailable = {};
|
|
6873
|
+
const [topology, updates, installs, cli] = await Promise.all([
|
|
6874
|
+
readTopology(client),
|
|
6875
|
+
readQuery(client, "addons.listUpdates", {}, z26.array(AddonUpdateShape)),
|
|
6876
|
+
readInstallations(client),
|
|
6877
|
+
cliVerdict(ctx.cliVersion, ctx.fetch, deps.entryPath ?? process.argv[1] ?? "")
|
|
6878
|
+
]);
|
|
6879
|
+
if (!topology.ok) {
|
|
6880
|
+
return { status: "unavailable", reason: topology.reason, cli };
|
|
6881
|
+
}
|
|
6882
|
+
const drift = installs.ok ? addonDrift(installs.value) : [];
|
|
6883
|
+
if (!installs.ok) unavailable["addonVersionsPerNode"] = installs.reason;
|
|
6884
|
+
else if (installs.value.unreachable.length > 0) {
|
|
6885
|
+
unavailable["addonVersionsPerNode"] = `no answer from ${installs.value.unreachable.map((u) => u.nodeId).join(", ")}`;
|
|
6886
|
+
}
|
|
6887
|
+
const nodes = await Promise.all(topology.value.map((n) => checkNode(client, n, drift)));
|
|
6888
|
+
let hubAddons = null;
|
|
6889
|
+
if (updates.ok) {
|
|
6890
|
+
const capped = cap(updates.value, ADDON_LIMIT);
|
|
6891
|
+
hubAddons = {
|
|
6892
|
+
items: capped.items.map((u) => ({
|
|
6893
|
+
name: u.name,
|
|
6894
|
+
from: u.currentVersion,
|
|
6895
|
+
to: u.latestVersion,
|
|
6896
|
+
framework: u.isSystem || u.category === "core"
|
|
6897
|
+
})),
|
|
6898
|
+
truncated: capped.truncated
|
|
6899
|
+
};
|
|
6900
|
+
} else unavailable["hubAddonUpdates"] = updates.reason;
|
|
6901
|
+
return {
|
|
6902
|
+
status: "ok",
|
|
6903
|
+
hub: server,
|
|
6904
|
+
nodes,
|
|
6905
|
+
hubAddons,
|
|
6906
|
+
cli,
|
|
6907
|
+
viewer: {
|
|
6908
|
+
status: "not-exposed",
|
|
6909
|
+
note: "No hub API reports the viewer version; the web viewer is served by the hub and the mobile apps update through their stores."
|
|
6910
|
+
},
|
|
6911
|
+
summary: {
|
|
6912
|
+
serverUpdateNeeded: nodes.filter((n) => n.serverNeedsUpdate === true).map((n) => n.id),
|
|
6913
|
+
wrapperUpdateNeeded: nodes.filter((n) => n.wrapper?.behind === true).map((n) => n.id),
|
|
6914
|
+
offline: nodes.filter((n) => !n.online).map((n) => n.id),
|
|
6915
|
+
capless: nodes.filter((n) => n.flags.length > 0).map((n) => n.id),
|
|
6916
|
+
nodesWithAddonDrift: [...new Set(drift.map((d) => d.nodeId))],
|
|
6917
|
+
cliUpdateNeeded: cli.updateAvailable
|
|
6918
|
+
},
|
|
6919
|
+
unavailable,
|
|
6920
|
+
note: "Latest versions come from the hub's last registry check (`latestCheckedAt`), not a fresh one; a version just published may not show yet."
|
|
6921
|
+
};
|
|
6922
|
+
});
|
|
6923
|
+
}
|
|
6924
|
+
async function checkNode(client, node, drift) {
|
|
6925
|
+
const caps = capState(node);
|
|
6926
|
+
const base = {
|
|
6927
|
+
id: node.id,
|
|
6928
|
+
name: node.name ?? null,
|
|
6929
|
+
isHub: node.isHub === true,
|
|
6930
|
+
online: node.isOnline,
|
|
6931
|
+
platform: node.platform ?? null,
|
|
6932
|
+
capabilityCount: caps.count,
|
|
6933
|
+
flags: caps.capless && node.isOnline ? ["online-but-capless"] : [],
|
|
6934
|
+
addonsBehindHub: drift.filter((d) => d.nodeId === node.id)
|
|
6935
|
+
};
|
|
6936
|
+
if (!node.isOnline) {
|
|
6937
|
+
return {
|
|
6938
|
+
...base,
|
|
6939
|
+
server: null,
|
|
6940
|
+
serverNeedsUpdate: null,
|
|
6941
|
+
wrapper: null,
|
|
6942
|
+
unreadable: "node is offline"
|
|
6943
|
+
};
|
|
6944
|
+
}
|
|
6945
|
+
const status = await readNodeStatus(client, node);
|
|
6946
|
+
if (!status.ok) {
|
|
6947
|
+
return {
|
|
6948
|
+
...base,
|
|
6949
|
+
server: null,
|
|
6950
|
+
serverNeedsUpdate: null,
|
|
6951
|
+
wrapper: null,
|
|
6952
|
+
unreadable: status.reason
|
|
6953
|
+
};
|
|
6954
|
+
}
|
|
6955
|
+
const server = serverVerdict(status.value);
|
|
6956
|
+
return {
|
|
6957
|
+
...base,
|
|
6958
|
+
server,
|
|
6959
|
+
serverNeedsUpdate: needsServerUpdate(server),
|
|
6960
|
+
wrapper: wrapperVerdict(status.value)
|
|
6961
|
+
};
|
|
6962
|
+
}
|
|
6963
|
+
|
|
6964
|
+
// src/mcp/ops-tools.ts
|
|
6965
|
+
var IDENTITY_CLAIMS = ["sub", "username", "name", "role"];
|
|
6966
|
+
function identityOf(token) {
|
|
6967
|
+
const claims = jwtClaims(token);
|
|
6968
|
+
const out = {};
|
|
6969
|
+
if (claims === null) return out;
|
|
6970
|
+
for (const key of IDENTITY_CLAIMS) {
|
|
6971
|
+
const value = claims[key];
|
|
6972
|
+
if (typeof value === "string" && value !== "") out[key] = value;
|
|
6973
|
+
}
|
|
6974
|
+
return out;
|
|
6975
|
+
}
|
|
6976
|
+
async function whoami(ctx) {
|
|
6977
|
+
const outcome = await ctx.hub();
|
|
6978
|
+
if (!outcome.ok) return noSessionResult(outcome);
|
|
6979
|
+
const c = outcome.connection;
|
|
6980
|
+
return {
|
|
6981
|
+
status: "ok",
|
|
6982
|
+
hub: c.server,
|
|
6983
|
+
tier: c.tier,
|
|
6984
|
+
source: c.source,
|
|
6985
|
+
identity: identityOf(c.token)
|
|
6986
|
+
};
|
|
6987
|
+
}
|
|
6988
|
+
function registerOpsTools(server, ctx) {
|
|
6989
|
+
server.tool(
|
|
6990
|
+
"ops_whoami",
|
|
6991
|
+
"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.",
|
|
6992
|
+
{},
|
|
6993
|
+
async () => text12(await whoami(ctx))
|
|
6994
|
+
);
|
|
6995
|
+
server.tool(
|
|
6996
|
+
"ops_status",
|
|
6997
|
+
'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`.',
|
|
6998
|
+
{},
|
|
6999
|
+
async () => text12(await opsStatus(ctx))
|
|
7000
|
+
);
|
|
7001
|
+
server.tool(
|
|
7002
|
+
"ops_device",
|
|
7003
|
+
"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.",
|
|
7004
|
+
{ deviceId: z27.number().int() },
|
|
7005
|
+
async ({ deviceId }) => text12(await opsDevice(ctx, deviceId))
|
|
7006
|
+
);
|
|
7007
|
+
server.tool(
|
|
7008
|
+
"ops_unhealthy_devices",
|
|
7009
|
+
"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`.",
|
|
7010
|
+
{},
|
|
7011
|
+
async () => text12(await opsUnhealthyDevices(ctx))
|
|
7012
|
+
);
|
|
7013
|
+
server.tool(
|
|
7014
|
+
"ops_logs",
|
|
7015
|
+
"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`.",
|
|
7016
|
+
{
|
|
7017
|
+
deviceId: z27.number().int().optional(),
|
|
7018
|
+
addonId: z27.string().optional(),
|
|
7019
|
+
nodeId: z27.string().optional(),
|
|
7020
|
+
level: z27.enum(["debug", "info", "warn", "error"]).optional(),
|
|
7021
|
+
windowMinutes: z27.number().int().min(1).max(1440).optional(),
|
|
7022
|
+
text: z27.string().max(200).optional(),
|
|
7023
|
+
around: z27.string().max(200).optional(),
|
|
7024
|
+
contextLines: z27.number().int().min(1).max(50).optional()
|
|
7025
|
+
},
|
|
7026
|
+
async (args) => text12(
|
|
7027
|
+
await opsLogs(ctx, {
|
|
7028
|
+
...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
|
|
7029
|
+
...args.addonId ? { addonId: args.addonId } : {},
|
|
7030
|
+
...args.nodeId ? { nodeId: args.nodeId } : {},
|
|
7031
|
+
...args.level ? { level: args.level } : {},
|
|
7032
|
+
...args.windowMinutes !== void 0 ? { windowMinutes: args.windowMinutes } : {},
|
|
7033
|
+
...args.text ? { text: args.text } : {},
|
|
7034
|
+
...args.around ? { around: args.around } : {},
|
|
7035
|
+
...args.contextLines !== void 0 ? { contextLines: args.contextLines } : {}
|
|
7036
|
+
})
|
|
7037
|
+
)
|
|
7038
|
+
);
|
|
7039
|
+
server.tool(
|
|
7040
|
+
"ops_events",
|
|
7041
|
+
"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.",
|
|
7042
|
+
{
|
|
7043
|
+
deviceId: z27.number().int().optional(),
|
|
7044
|
+
className: z27.string().optional(),
|
|
7045
|
+
zone: z27.string().optional(),
|
|
7046
|
+
text: z27.string().max(200).optional(),
|
|
7047
|
+
windowMinutes: z27.number().int().min(1).max(10080).optional(),
|
|
7048
|
+
limit: z27.number().int().min(1).max(50).optional()
|
|
7049
|
+
},
|
|
7050
|
+
async (args) => text12(
|
|
7051
|
+
await opsEvents(ctx, {
|
|
7052
|
+
...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
|
|
7053
|
+
...args.className ? { className: args.className } : {},
|
|
7054
|
+
...args.zone ? { zone: args.zone } : {},
|
|
7055
|
+
...args.text ? { text: args.text } : {},
|
|
7056
|
+
...args.windowMinutes !== void 0 ? { windowMinutes: args.windowMinutes } : {},
|
|
7057
|
+
...args.limit !== void 0 ? { limit: args.limit } : {}
|
|
7058
|
+
})
|
|
7059
|
+
)
|
|
7060
|
+
);
|
|
7061
|
+
server.tool(
|
|
7062
|
+
"ops_track",
|
|
7063
|
+
"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.",
|
|
7064
|
+
{ deviceId: z27.number().int(), trackId: z27.string().min(1) },
|
|
7065
|
+
async ({ deviceId, trackId }) => text12(await opsTrack(ctx, { deviceId, trackId }))
|
|
7066
|
+
);
|
|
7067
|
+
server.tool(
|
|
7068
|
+
"ops_accelerators",
|
|
7069
|
+
'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`.',
|
|
7070
|
+
{},
|
|
7071
|
+
async () => text12(await opsAccelerators(ctx))
|
|
7072
|
+
);
|
|
7073
|
+
server.tool(
|
|
7074
|
+
"ops_update_check",
|
|
7075
|
+
'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`.',
|
|
7076
|
+
{},
|
|
7077
|
+
async () => text12(await opsUpdateCheck(ctx))
|
|
7078
|
+
);
|
|
7079
|
+
server.tool(
|
|
7080
|
+
UPDATE_APPLY_TOOL,
|
|
7081
|
+
"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`.",
|
|
7082
|
+
{
|
|
7083
|
+
version: z27.string().min(1).optional().describe("Server target; default = the hub's latest."),
|
|
7084
|
+
addons: z27.boolean().optional().describe("Update addons too (default true)."),
|
|
7085
|
+
cli: z27.boolean().optional().describe("Update this CLI last (default true)."),
|
|
7086
|
+
confirm_token: z27.string().optional()
|
|
7087
|
+
},
|
|
7088
|
+
async ({ version, addons, cli, confirm_token }) => text12(
|
|
7089
|
+
await opsUpdateApply(
|
|
7090
|
+
ctx,
|
|
7091
|
+
{
|
|
7092
|
+
...version !== void 0 ? { version } : {},
|
|
7093
|
+
...addons !== void 0 ? { addons } : {},
|
|
7094
|
+
...cli !== void 0 ? { cli } : {}
|
|
7095
|
+
},
|
|
7096
|
+
confirm_token
|
|
7097
|
+
)
|
|
7098
|
+
)
|
|
7099
|
+
);
|
|
7100
|
+
}
|
|
7101
|
+
|
|
7102
|
+
// src/mcp/configure-tools.ts
|
|
7103
|
+
var CurrentDeviceSchema = z28.object({ name: z28.string() }).nullable();
|
|
7104
|
+
var RENAME_DEVICE_TOOL = "configure_rename_device";
|
|
7105
|
+
function renameDeviceSpec(client) {
|
|
7106
|
+
return {
|
|
7107
|
+
tool: RENAME_DEVICE_TOOL,
|
|
7108
|
+
async dryRun(args) {
|
|
7109
|
+
const current = CurrentDeviceSchema.safeParse(
|
|
7110
|
+
await client.call("GET", "deviceManager.getDevice", {
|
|
7111
|
+
deviceId: args.deviceId
|
|
7112
|
+
})
|
|
7113
|
+
);
|
|
7114
|
+
if (!current.success || current.data === null) {
|
|
7115
|
+
return {
|
|
7116
|
+
confirmable: false,
|
|
7117
|
+
describe: {
|
|
7118
|
+
problem: `device ${args.deviceId} was not found on the hub`
|
|
7119
|
+
}
|
|
7120
|
+
};
|
|
7121
|
+
}
|
|
7122
|
+
if (current.data.name === args.name) {
|
|
7123
|
+
return {
|
|
7124
|
+
confirmable: false,
|
|
7125
|
+
describe: {
|
|
7126
|
+
problem: `device ${args.deviceId} is already named "${args.name}"`
|
|
7127
|
+
}
|
|
7128
|
+
};
|
|
7129
|
+
}
|
|
7130
|
+
return {
|
|
7131
|
+
confirmable: true,
|
|
7132
|
+
describe: {
|
|
7133
|
+
action: "rename device",
|
|
7134
|
+
deviceId: args.deviceId,
|
|
7135
|
+
from: current.data.name,
|
|
7136
|
+
to: args.name
|
|
7137
|
+
}
|
|
7138
|
+
};
|
|
7139
|
+
},
|
|
7140
|
+
async execute(args) {
|
|
7141
|
+
await client.call("POST", "deviceManager.setName", {
|
|
7142
|
+
deviceId: args.deviceId,
|
|
7143
|
+
name: args.name
|
|
7144
|
+
});
|
|
7145
|
+
return { deviceId: args.deviceId, name: args.name };
|
|
7146
|
+
}
|
|
7147
|
+
};
|
|
7148
|
+
}
|
|
7149
|
+
async function renameDevice(ctx, args, confirmToken) {
|
|
7150
|
+
const outcome = await ctx.hub();
|
|
7151
|
+
if (!outcome.ok) return noSessionResult(outcome);
|
|
7152
|
+
if (outcome.connection.tier !== "admin") {
|
|
7153
|
+
return {
|
|
7154
|
+
status: "refused",
|
|
7155
|
+
reason: outcome.connection.adminHint ?? "this credential is not admin-scoped"
|
|
7156
|
+
};
|
|
7157
|
+
}
|
|
7158
|
+
return runGated4(ctx.gate, renameDeviceSpec(outcome.connection.client), args, confirmToken);
|
|
7159
|
+
}
|
|
7160
|
+
function registerConfigureTools(server, ctx) {
|
|
7161
|
+
server.tool(
|
|
7162
|
+
RENAME_DEVICE_TOOL,
|
|
7163
|
+
"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.",
|
|
7164
|
+
{
|
|
7165
|
+
deviceId: z28.number().int(),
|
|
7166
|
+
name: z28.string().min(1),
|
|
7167
|
+
confirm_token: z28.string().optional()
|
|
7168
|
+
},
|
|
7169
|
+
async ({ deviceId, name, confirm_token }) => text13(await renameDevice(ctx, { deviceId, name }, confirm_token))
|
|
7170
|
+
);
|
|
7171
|
+
registerAddonTools(server, ctx);
|
|
7172
|
+
registerCameraTools(server, ctx);
|
|
7173
|
+
registerNotifierTools(server, ctx);
|
|
7174
|
+
registerConfigureP3bTools(server, ctx);
|
|
7175
|
+
}
|
|
7176
|
+
|
|
5956
7177
|
// src/mcp/support-tools.ts
|
|
5957
|
-
import { text as
|
|
5958
|
-
import { z as
|
|
7178
|
+
import { text as text14 } from "@camstack/develop";
|
|
7179
|
+
import { z as z33 } from "zod";
|
|
5959
7180
|
|
|
5960
7181
|
// src/mcp/support-accelerator.ts
|
|
5961
|
-
import { z as
|
|
7182
|
+
import { z as z29 } from "zod";
|
|
5962
7183
|
var EXPECTED_ACCELERATORS = [
|
|
5963
7184
|
"intel-igpu",
|
|
5964
7185
|
"intel-npu",
|
|
@@ -5970,23 +7191,23 @@ var EXPECTED_ACCELERATORS = [
|
|
|
5970
7191
|
"apple-ane",
|
|
5971
7192
|
"nvidia"
|
|
5972
7193
|
];
|
|
5973
|
-
var ExpectedAcceleratorSchema =
|
|
5974
|
-
var HardwareSchema2 =
|
|
5975
|
-
platform:
|
|
5976
|
-
arch:
|
|
5977
|
-
cpuModel:
|
|
5978
|
-
gpu:
|
|
5979
|
-
npu:
|
|
7194
|
+
var ExpectedAcceleratorSchema = z29.enum(EXPECTED_ACCELERATORS);
|
|
7195
|
+
var HardwareSchema2 = z29.looseObject({
|
|
7196
|
+
platform: z29.enum(["darwin", "linux", "win32"]),
|
|
7197
|
+
arch: z29.enum(["arm64", "x64"]),
|
|
7198
|
+
cpuModel: z29.string(),
|
|
7199
|
+
gpu: z29.looseObject({ type: z29.string(), name: z29.string() }).nullable(),
|
|
7200
|
+
npu: z29.looseObject({ type: z29.string() }).nullable()
|
|
5980
7201
|
});
|
|
5981
|
-
var InferenceDevicesSchema2 =
|
|
5982
|
-
reachable:
|
|
5983
|
-
devices:
|
|
5984
|
-
|
|
5985
|
-
key:
|
|
5986
|
-
available:
|
|
5987
|
-
presence:
|
|
5988
|
-
enabled:
|
|
5989
|
-
exclusion:
|
|
7202
|
+
var InferenceDevicesSchema2 = z29.looseObject({
|
|
7203
|
+
reachable: z29.boolean(),
|
|
7204
|
+
devices: z29.array(
|
|
7205
|
+
z29.looseObject({
|
|
7206
|
+
key: z29.string(),
|
|
7207
|
+
available: z29.boolean(),
|
|
7208
|
+
presence: z29.enum(["present", "absent", "unknown"]).optional(),
|
|
7209
|
+
enabled: z29.boolean(),
|
|
7210
|
+
exclusion: z29.string().nullable()
|
|
5990
7211
|
})
|
|
5991
7212
|
)
|
|
5992
7213
|
});
|
|
@@ -6263,9 +7484,9 @@ async function supportDiagnoseAccelerator(ctx, args) {
|
|
|
6263
7484
|
|
|
6264
7485
|
// src/mcp/support-benchmark.ts
|
|
6265
7486
|
import {
|
|
6266
|
-
runGated as
|
|
7487
|
+
runGated as runGated5
|
|
6267
7488
|
} from "@camstack/develop";
|
|
6268
|
-
import { z as
|
|
7489
|
+
import { z as z30 } from "zod";
|
|
6269
7490
|
var BENCHMARK_RUN_TOOL = "support_benchmark_run";
|
|
6270
7491
|
var AUTO_TUNE_MODELS = [
|
|
6271
7492
|
"yolov9t-320",
|
|
@@ -6287,64 +7508,64 @@ var CONTRIBUTE_HOW = [
|
|
|
6287
7508
|
"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
7509
|
'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
7510
|
];
|
|
6290
|
-
var HardwareSchema3 =
|
|
6291
|
-
platform:
|
|
6292
|
-
arch:
|
|
6293
|
-
cpuModel:
|
|
6294
|
-
cpuCores:
|
|
6295
|
-
totalRAM_MB:
|
|
6296
|
-
availableRAM_MB:
|
|
6297
|
-
gpu:
|
|
6298
|
-
type:
|
|
6299
|
-
name:
|
|
7511
|
+
var HardwareSchema3 = z30.looseObject({
|
|
7512
|
+
platform: z30.enum(["darwin", "linux", "win32"]),
|
|
7513
|
+
arch: z30.enum(["arm64", "x64"]),
|
|
7514
|
+
cpuModel: z30.string(),
|
|
7515
|
+
cpuCores: z30.number(),
|
|
7516
|
+
totalRAM_MB: z30.number(),
|
|
7517
|
+
availableRAM_MB: z30.number(),
|
|
7518
|
+
gpu: z30.looseObject({
|
|
7519
|
+
type: z30.enum(["nvidia", "amd", "intel", "apple"]),
|
|
7520
|
+
name: z30.string()
|
|
6300
7521
|
}).nullable(),
|
|
6301
|
-
npu:
|
|
7522
|
+
npu: z30.looseObject({ type: z30.enum(["apple-ane", "intel-npu", "amd-xdna"]) }).nullable()
|
|
6302
7523
|
});
|
|
6303
|
-
var InferenceDevicesSchema3 =
|
|
6304
|
-
reachable:
|
|
6305
|
-
devices:
|
|
6306
|
-
|
|
6307
|
-
key:
|
|
6308
|
-
backend:
|
|
6309
|
-
available:
|
|
6310
|
-
enabled:
|
|
7524
|
+
var InferenceDevicesSchema3 = z30.looseObject({
|
|
7525
|
+
reachable: z30.boolean(),
|
|
7526
|
+
devices: z30.array(
|
|
7527
|
+
z30.looseObject({
|
|
7528
|
+
key: z30.string(),
|
|
7529
|
+
backend: z30.string(),
|
|
7530
|
+
available: z30.boolean(),
|
|
7531
|
+
enabled: z30.boolean()
|
|
6311
7532
|
})
|
|
6312
7533
|
)
|
|
6313
7534
|
});
|
|
6314
|
-
var RunStatusSchema =
|
|
6315
|
-
run:
|
|
6316
|
-
runId:
|
|
6317
|
-
nodeId:
|
|
6318
|
-
state:
|
|
6319
|
-
endReason:
|
|
6320
|
-
error:
|
|
7535
|
+
var RunStatusSchema = z30.looseObject({
|
|
7536
|
+
run: z30.looseObject({
|
|
7537
|
+
runId: z30.string(),
|
|
7538
|
+
nodeId: z30.string(),
|
|
7539
|
+
state: z30.string(),
|
|
7540
|
+
endReason: z30.string().nullable().optional(),
|
|
7541
|
+
error: z30.string().nullable().optional()
|
|
6321
7542
|
}).nullable()
|
|
6322
7543
|
});
|
|
6323
|
-
var ModelListSchema =
|
|
6324
|
-
|
|
6325
|
-
id:
|
|
6326
|
-
formats:
|
|
7544
|
+
var ModelListSchema = z30.array(
|
|
7545
|
+
z30.looseObject({
|
|
7546
|
+
id: z30.string(),
|
|
7547
|
+
formats: z30.record(z30.string(), z30.looseObject({ downloaded: z30.boolean().optional() })).optional()
|
|
6327
7548
|
})
|
|
6328
7549
|
);
|
|
6329
|
-
var PipelineTestSchema =
|
|
6330
|
-
kind:
|
|
6331
|
-
detections:
|
|
6332
|
-
|
|
6333
|
-
debug:
|
|
6334
|
-
stepLatencyMs:
|
|
7550
|
+
var PipelineTestSchema = z30.looseObject({
|
|
7551
|
+
kind: z30.string().optional(),
|
|
7552
|
+
detections: z30.array(
|
|
7553
|
+
z30.looseObject({
|
|
7554
|
+
debug: z30.looseObject({
|
|
7555
|
+
stepLatencyMs: z30.record(z30.string(), z30.number()).optional()
|
|
6335
7556
|
}).optional()
|
|
6336
7557
|
})
|
|
6337
7558
|
).optional(),
|
|
6338
|
-
discarded:
|
|
6339
|
-
debug:
|
|
6340
|
-
stepTimings:
|
|
6341
|
-
|
|
6342
|
-
source:
|
|
6343
|
-
ms:
|
|
7559
|
+
discarded: z30.array(z30.unknown()).optional(),
|
|
7560
|
+
debug: z30.looseObject({
|
|
7561
|
+
stepTimings: z30.array(
|
|
7562
|
+
z30.looseObject({
|
|
7563
|
+
source: z30.string().optional(),
|
|
7564
|
+
ms: z30.number().optional()
|
|
6344
7565
|
})
|
|
6345
7566
|
).optional(),
|
|
6346
|
-
predictMs:
|
|
6347
|
-
totalInferenceMs:
|
|
7567
|
+
predictMs: z30.number().optional(),
|
|
7568
|
+
totalInferenceMs: z30.number().optional()
|
|
6348
7569
|
}).optional()
|
|
6349
7570
|
});
|
|
6350
7571
|
var FORMAT_BY_BACKEND = {
|
|
@@ -6371,10 +7592,10 @@ function inferMsOf(raw) {
|
|
|
6371
7592
|
if (!parsed.success) return { inferMs: null, shed: false };
|
|
6372
7593
|
const r = parsed.data;
|
|
6373
7594
|
const fromDetection = r.detections?.[0]?.debug?.stepLatencyMs?.[DETECTOR_STEP];
|
|
6374
|
-
const
|
|
7595
|
+
const timing2 = r.debug?.stepTimings?.find(
|
|
6375
7596
|
(t) => t.source === DETECTOR_STEP && typeof t.ms === "number"
|
|
6376
7597
|
);
|
|
6377
|
-
const candidates = [fromDetection,
|
|
7598
|
+
const candidates = [fromDetection, timing2?.ms, r.debug?.predictMs, r.debug?.totalInferenceMs];
|
|
6378
7599
|
const inferMs = candidates.find((c) => typeof c === "number" && c > 0) ?? null;
|
|
6379
7600
|
const shedReason = (r.discarded ?? []).some(
|
|
6380
7601
|
(d) => /deadline|shed/i.test(String(JSON.stringify(d)))
|
|
@@ -6629,17 +7850,17 @@ async function supportBenchmarkRun(ctx, deps, args, confirmToken) {
|
|
|
6629
7850
|
reason: outcome.connection.adminHint ?? "this credential is not admin-scoped"
|
|
6630
7851
|
};
|
|
6631
7852
|
}
|
|
6632
|
-
return
|
|
6633
|
-
}
|
|
6634
|
-
var ResultsSchema =
|
|
6635
|
-
|
|
6636
|
-
nodeId:
|
|
6637
|
-
cell:
|
|
6638
|
-
phase:
|
|
6639
|
-
deviceKey:
|
|
6640
|
-
modelId:
|
|
6641
|
-
p50Ms:
|
|
6642
|
-
fps:
|
|
7853
|
+
return runGated5(ctx.gate, benchmarkRunSpec(outcome.connection.client, deps), args, confirmToken);
|
|
7854
|
+
}
|
|
7855
|
+
var ResultsSchema = z30.array(
|
|
7856
|
+
z30.looseObject({
|
|
7857
|
+
nodeId: z30.string(),
|
|
7858
|
+
cell: z30.looseObject({
|
|
7859
|
+
phase: z30.string(),
|
|
7860
|
+
deviceKey: z30.string(),
|
|
7861
|
+
modelId: z30.string(),
|
|
7862
|
+
p50Ms: z30.number().optional(),
|
|
7863
|
+
fps: z30.number().optional()
|
|
6643
7864
|
})
|
|
6644
7865
|
})
|
|
6645
7866
|
);
|
|
@@ -6692,20 +7913,20 @@ async function supportBenchmarkStatus(ctx, args) {
|
|
|
6692
7913
|
}
|
|
6693
7914
|
|
|
6694
7915
|
// src/mcp/support-camera.ts
|
|
6695
|
-
import { z as
|
|
6696
|
-
var DeviceSchema2 =
|
|
6697
|
-
id:
|
|
6698
|
-
name:
|
|
6699
|
-
addonId:
|
|
6700
|
-
disabled:
|
|
6701
|
-
online:
|
|
7916
|
+
import { z as z31 } from "zod";
|
|
7917
|
+
var DeviceSchema2 = z31.looseObject({
|
|
7918
|
+
id: z31.number(),
|
|
7919
|
+
name: z31.string(),
|
|
7920
|
+
addonId: z31.string().optional(),
|
|
7921
|
+
disabled: z31.boolean().optional(),
|
|
7922
|
+
online: z31.boolean().optional()
|
|
6702
7923
|
}).nullable();
|
|
6703
7924
|
var GENERIC = /onvif|rtsp/i;
|
|
6704
|
-
function words(
|
|
6705
|
-
return
|
|
7925
|
+
function words(text15) {
|
|
7926
|
+
return text15.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length >= 3);
|
|
6706
7927
|
}
|
|
6707
|
-
function isGeneric(
|
|
6708
|
-
return
|
|
7928
|
+
function isGeneric(parts2) {
|
|
7929
|
+
return parts2.some((p) => GENERIC.test(p));
|
|
6709
7930
|
}
|
|
6710
7931
|
function installedPick(p) {
|
|
6711
7932
|
return {
|
|
@@ -6726,9 +7947,9 @@ function availablePick(p) {
|
|
|
6726
7947
|
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
7948
|
function matchIn(pools, tokens) {
|
|
6728
7949
|
if (tokens.length === 0) return null;
|
|
6729
|
-
const hit = (
|
|
6730
|
-
if (isGeneric(
|
|
6731
|
-
const joined =
|
|
7950
|
+
const hit = (parts2) => {
|
|
7951
|
+
if (isGeneric(parts2)) return false;
|
|
7952
|
+
const joined = parts2.join(" ").toLowerCase();
|
|
6732
7953
|
return tokens.some((t) => joined.includes(t));
|
|
6733
7954
|
};
|
|
6734
7955
|
const installed = pools.installed.find(
|
|
@@ -6864,10 +8085,10 @@ async function supportDiagnoseCamera(ctx, args) {
|
|
|
6864
8085
|
}
|
|
6865
8086
|
|
|
6866
8087
|
// src/mcp/support-deps.ts
|
|
6867
|
-
import { execFile } from "child_process";
|
|
8088
|
+
import { execFile as execFile2 } from "child_process";
|
|
6868
8089
|
var RUN_TIMEOUT_MS = 6e4;
|
|
6869
8090
|
var spawnRunner = (command, args) => new Promise((resolve) => {
|
|
6870
|
-
|
|
8091
|
+
execFile2(
|
|
6871
8092
|
command,
|
|
6872
8093
|
[...args],
|
|
6873
8094
|
{ timeout: RUN_TIMEOUT_MS, maxBuffer: 1e6 },
|
|
@@ -6897,9 +8118,9 @@ import { tmpdir } from "os";
|
|
|
6897
8118
|
import { join } from "path";
|
|
6898
8119
|
import {
|
|
6899
8120
|
digestArgs,
|
|
6900
|
-
runGated as
|
|
8121
|
+
runGated as runGated6
|
|
6901
8122
|
} from "@camstack/develop";
|
|
6902
|
-
import { z as
|
|
8123
|
+
import { z as z32 } from "zod";
|
|
6903
8124
|
|
|
6904
8125
|
// src/mcp/support-scrub.ts
|
|
6905
8126
|
import { redactToolArgs, scrubSecrets } from "@camstack/develop";
|
|
@@ -6908,10 +8129,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
8129
|
var MAC = /\b(?:[0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}\b/g;
|
|
6909
8130
|
var HOME_DIR = /(\/Users|\/home)\/[^/\s'"]+/g;
|
|
6910
8131
|
function octets(ip) {
|
|
6911
|
-
const
|
|
6912
|
-
if (
|
|
6913
|
-
if (
|
|
6914
|
-
return
|
|
8132
|
+
const parts2 = ip.split(".").map((p) => Number(p));
|
|
8133
|
+
if (parts2.length !== 4) return null;
|
|
8134
|
+
if (parts2.some((p) => !Number.isInteger(p) || p < 0 || p > 255)) return null;
|
|
8135
|
+
return parts2;
|
|
6915
8136
|
}
|
|
6916
8137
|
function maskIpv4(ip) {
|
|
6917
8138
|
const o = octets(ip);
|
|
@@ -6952,16 +8173,16 @@ var INFERENCE_PACKAGE = "@camstack/addon-pipeline";
|
|
|
6952
8173
|
var TITLE_MAX = 140;
|
|
6953
8174
|
var DIAGNOSIS_MAX = 4e3;
|
|
6954
8175
|
var DRAFT_KINDS = ["accelerator", "camera", "benchmark", "other"];
|
|
6955
|
-
var DraftKindSchema =
|
|
6956
|
-
var BenchmarkCellsSchema =
|
|
6957
|
-
|
|
6958
|
-
|
|
8176
|
+
var DraftKindSchema = z32.enum(DRAFT_KINDS);
|
|
8177
|
+
var BenchmarkCellsSchema = z32.record(
|
|
8178
|
+
z32.string().regex(/^[a-z0-9][a-z0-9._-]{1,63}$/),
|
|
8179
|
+
z32.object({ p50Ms: z32.number().positive(), fps: z32.number().positive() })
|
|
6959
8180
|
);
|
|
6960
8181
|
var PackageNameRe = /^(@[a-z0-9._-]+\/)?[a-z0-9._-]+$/;
|
|
6961
8182
|
var GithubRepoRe = /github\.com[:/]([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?(?:[/#?].*)?$/;
|
|
6962
8183
|
var ShorthandRe = /^(?:github:)?([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/;
|
|
6963
|
-
var RegistryDocSchema =
|
|
6964
|
-
repository:
|
|
8184
|
+
var RegistryDocSchema = z32.looseObject({
|
|
8185
|
+
repository: z32.union([z32.string(), z32.looseObject({ url: z32.string() })]).optional()
|
|
6965
8186
|
});
|
|
6966
8187
|
function githubRepoOf(value) {
|
|
6967
8188
|
const url = GithubRepoRe.exec(value);
|
|
@@ -7003,7 +8224,7 @@ async function resolveRepo(ctx, packageName) {
|
|
|
7003
8224
|
reason: null
|
|
7004
8225
|
};
|
|
7005
8226
|
}
|
|
7006
|
-
var ServerStatusSchema2 =
|
|
8227
|
+
var ServerStatusSchema2 = z32.looseObject({ runningVersion: z32.string() });
|
|
7007
8228
|
async function serverVersion(client, nodeId) {
|
|
7008
8229
|
const status = await readQuery(
|
|
7009
8230
|
client,
|
|
@@ -7205,8 +8426,8 @@ var BENCHMARK_FILES = [
|
|
|
7205
8426
|
"docs/benchmarks/detector-latency.json",
|
|
7206
8427
|
"packages/types/src/pipeline/auto-tune/detector-performance.ts"
|
|
7207
8428
|
];
|
|
7208
|
-
function slug(
|
|
7209
|
-
const s =
|
|
8429
|
+
function slug(text15) {
|
|
8430
|
+
const s = text15.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
7210
8431
|
return s === "" ? "change" : s;
|
|
7211
8432
|
}
|
|
7212
8433
|
function prProblem(args) {
|
|
@@ -7262,27 +8483,27 @@ function prSpec(ctx) {
|
|
|
7262
8483
|
};
|
|
7263
8484
|
}
|
|
7264
8485
|
async function supportDraftIssue(ctx, deps, args, confirmToken) {
|
|
7265
|
-
return
|
|
8486
|
+
return runGated6(ctx.gate, issueSpec({ ctx, deps }), args, confirmToken);
|
|
7266
8487
|
}
|
|
7267
8488
|
async function supportDraftPr(ctx, args, confirmToken) {
|
|
7268
|
-
return
|
|
8489
|
+
return runGated6(ctx.gate, prSpec(ctx), args, confirmToken);
|
|
7269
8490
|
}
|
|
7270
8491
|
|
|
7271
8492
|
// src/mcp/support-tools.ts
|
|
7272
|
-
var ModelId =
|
|
8493
|
+
var ModelId = z33.string().regex(/^[a-z0-9][a-z0-9._-]{1,63}$/);
|
|
7273
8494
|
var DraftShape = {
|
|
7274
8495
|
kind: DraftKindSchema,
|
|
7275
|
-
title:
|
|
7276
|
-
summary:
|
|
7277
|
-
nodeId:
|
|
8496
|
+
title: z33.string().min(3).max(300),
|
|
8497
|
+
summary: z33.string().min(3).max(4e3),
|
|
8498
|
+
nodeId: z33.string().max(100).optional(),
|
|
7278
8499
|
expected: ExpectedAcceleratorSchema.optional(),
|
|
7279
|
-
brand:
|
|
7280
|
-
model:
|
|
7281
|
-
deviceId:
|
|
7282
|
-
packageName:
|
|
7283
|
-
column:
|
|
8500
|
+
brand: z33.string().max(100).optional(),
|
|
8501
|
+
model: z33.string().max(100).optional(),
|
|
8502
|
+
deviceId: z33.number().int().optional(),
|
|
8503
|
+
packageName: z33.string().max(214).optional(),
|
|
8504
|
+
column: z33.string().max(100).optional(),
|
|
7284
8505
|
cells: BenchmarkCellsSchema.optional(),
|
|
7285
|
-
confirm_token:
|
|
8506
|
+
confirm_token: z33.string().optional()
|
|
7286
8507
|
};
|
|
7287
8508
|
function draftArgs(input) {
|
|
7288
8509
|
return {
|
|
@@ -7303,20 +8524,20 @@ function registerSupportTools(server, ctx, deps = defaultSupportDeps) {
|
|
|
7303
8524
|
server.tool(
|
|
7304
8525
|
"support_benchmark_status",
|
|
7305
8526
|
"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:
|
|
7307
|
-
async ({ nodeId }) =>
|
|
8527
|
+
{ nodeId: z33.string().max(100).optional() },
|
|
8528
|
+
async ({ nodeId }) => text14(await supportBenchmarkStatus(ctx, nodeId !== void 0 ? { nodeId } : {}))
|
|
7308
8529
|
);
|
|
7309
8530
|
server.tool(
|
|
7310
8531
|
BENCHMARK_RUN_TOOL,
|
|
7311
8532
|
"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
8533
|
{
|
|
7313
|
-
nodeId:
|
|
7314
|
-
deviceKey:
|
|
7315
|
-
models:
|
|
7316
|
-
warmRuns:
|
|
7317
|
-
confirm_token:
|
|
8534
|
+
nodeId: z33.string().min(1).max(100),
|
|
8535
|
+
deviceKey: z33.string().min(1).max(60),
|
|
8536
|
+
models: z33.array(ModelId).min(1).max(MAX_MODELS).optional(),
|
|
8537
|
+
warmRuns: z33.number().int().min(1).max(MAX_WARM_RUNS).optional(),
|
|
8538
|
+
confirm_token: z33.string().optional()
|
|
7318
8539
|
},
|
|
7319
|
-
async ({ nodeId, deviceKey, models, warmRuns, confirm_token }) =>
|
|
8540
|
+
async ({ nodeId, deviceKey, models, warmRuns, confirm_token }) => text14(
|
|
7320
8541
|
await supportBenchmarkRun(
|
|
7321
8542
|
ctx,
|
|
7322
8543
|
deps,
|
|
@@ -7333,18 +8554,18 @@ function registerSupportTools(server, ctx, deps = defaultSupportDeps) {
|
|
|
7333
8554
|
server.tool(
|
|
7334
8555
|
"support_diagnose_accelerator",
|
|
7335
8556
|
"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:
|
|
7337
|
-
async ({ nodeId, expected }) =>
|
|
8557
|
+
{ nodeId: z33.string().min(1).max(100), expected: ExpectedAcceleratorSchema },
|
|
8558
|
+
async ({ nodeId, expected }) => text14(await supportDiagnoseAccelerator(ctx, { nodeId, expected }))
|
|
7338
8559
|
);
|
|
7339
8560
|
server.tool(
|
|
7340
8561
|
"support_diagnose_camera",
|
|
7341
8562
|
"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
8563
|
{
|
|
7343
|
-
brand:
|
|
7344
|
-
model:
|
|
7345
|
-
deviceId:
|
|
8564
|
+
brand: z33.string().max(100).optional(),
|
|
8565
|
+
model: z33.string().max(100).optional(),
|
|
8566
|
+
deviceId: z33.number().int().optional()
|
|
7346
8567
|
},
|
|
7347
|
-
async ({ brand, model, deviceId }) =>
|
|
8568
|
+
async ({ brand, model, deviceId }) => text14(
|
|
7348
8569
|
await supportDiagnoseCamera(ctx, {
|
|
7349
8570
|
...brand !== void 0 ? { brand } : {},
|
|
7350
8571
|
...model !== void 0 ? { model } : {},
|
|
@@ -7356,13 +8577,13 @@ function registerSupportTools(server, ctx, deps = defaultSupportDeps) {
|
|
|
7356
8577
|
DRAFT_ISSUE_TOOL,
|
|
7357
8578
|
"Draft a GitHub issue for the public repository that owns the addon (the package's `repository` field; camstack/server when it names none): title, an environment block (server and CLI version, node platform, accelerator or camera facts from the diagnose tools), secrets and private addresses removed. Two steps: the dry run is the full draft and the target; the confirmed call runs YOUR own `gh issue create` (only if `gh auth status` succeeds, otherwise it returns the draft and a ready command). Never uses a token of ours.",
|
|
7358
8579
|
DraftShape,
|
|
7359
|
-
async (input) =>
|
|
8580
|
+
async (input) => text14(await supportDraftIssue(ctx, deps, draftArgs(input), input.confirm_token))
|
|
7360
8581
|
);
|
|
7361
8582
|
server.tool(
|
|
7362
8583
|
DRAFT_PR_TOOL,
|
|
7363
8584
|
'Plan a pull request for the public repository that owns the addon: branch, files to change, steps and the `gh pr create` command, with a scrubbed body. For kind "benchmark" give column and cells from support_benchmark_run. Two steps like every gated tool; it pushes no code and calls no GitHub API, you run the plan with your own git and gh.',
|
|
7364
8585
|
DraftShape,
|
|
7365
|
-
async (input) =>
|
|
8586
|
+
async (input) => text14(await supportDraftPr(ctx, draftArgs(input), input.confirm_token))
|
|
7366
8587
|
);
|
|
7367
8588
|
}
|
|
7368
8589
|
|