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.
@@ -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-NAUJQNCX.js";
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 runGated3,
31
- text as text11
32
+ runGated as runGated4,
33
+ text as text13
32
34
  } from "@camstack/develop";
33
- import { z as z19 } from "zod";
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 === "condition" && typeof c.value === "number" && c.unit !== void 0) {
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
- if (recipe.conditions !== void 0) walk(recipe.conditions, "condition");
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
- triggers: finished.recipe.triggers.length,
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: trigger, conditions, 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
+ "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-nc-rules.ts
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 = z9.object({
2848
+ var QuietHoursAnswerSchema = z11.object({
2354
2849
  /** Notifications are muted from this time … */
2355
- from: z9.string().regex(TIME),
2850
+ from: z11.string().regex(TIME),
2356
2851
  /** … until this one (HH:MM, hub time zone). A window may cross midnight. */
2357
- to: z9.string().regex(TIME),
2852
+ to: z11.string().regex(TIME),
2358
2853
  /** Days the quiet window STARTS on (0 = Sunday); absent = every day. */
2359
- days: z9.array(z9.number().int().min(0).max(6)).min(1).optional()
2854
+ days: z11.array(z11.number().int().min(0).max(6)).min(1).optional()
2360
2855
  });
2361
- var NcRuleAnswersSchema = z9.object({
2362
- name: z9.string().min(1).max(200).optional(),
2363
- delivery: z9.enum(["immediate", "track-end"]).optional(),
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: z9.array(z9.number().int().nonnegative()).optional(),
2366
- classes: z9.array(z9.string().min(1)).optional(),
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: z9.array(z9.string().min(1)).optional(),
2369
- minConfidence: z9.number().min(0).max(1).optional(),
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: z9.array(z9.string().min(1)).optional(),
2374
- cooldownSeconds: z9.number().int().min(0).max(86400).optional()
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 = z9.looseObject({
2561
- id: z9.string().optional(),
2562
- name: z9.string().optional(),
2563
- conditions: z9.record(z9.string(), z9.unknown()).optional(),
2564
- schedule: z9.unknown().optional()
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 }) => text6(await ncRuleQuestionnaire(ctx, 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: z9.string().optional(),
3198
+ ruleId: z11.string().optional(),
2704
3199
  confirm_token: ConfirmTokenSchema
2705
3200
  },
2706
- async ({ rule, ruleId, confirm_token }) => text6(
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 text7
3220
+ text as text8
2725
3221
  } from "@camstack/develop";
2726
- import { z as z11 } from "zod";
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 z10 } from "zod";
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 = z10.looseObject({
2756
- value: z10.union([z10.string(), z10.number(), z10.boolean()]),
2757
- label: z10.string().optional()
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 = z10.looseObject({
2760
- type: z10.string().optional(),
2761
- key: z10.string().optional(),
2762
- label: z10.string().optional(),
2763
- description: z10.string().optional(),
2764
- required: z10.boolean().optional(),
2765
- secret: z10.boolean().optional(),
2766
- default: z10.unknown().optional(),
2767
- options: z10.array(FormOptionSchema).optional(),
2768
- itemFields: z10.array(
2769
- z10.looseObject({
2770
- type: z10.string().optional(),
2771
- key: z10.string().optional()
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 = z10.looseObject({
2776
- sections: z10.array(z10.looseObject({ fields: z10.array(FormFieldSchema) }))
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 z10.number();
3373
+ return z12.number();
2878
3374
  case "boolean":
2879
3375
  case "switch":
2880
3376
  case "checkbox":
2881
3377
  case "toggle":
2882
- return z10.boolean();
3378
+ return z12.boolean();
2883
3379
  case "tags":
2884
- return z10.array(z10.string());
3380
+ return z12.array(z12.string());
2885
3381
  case "editable-array":
2886
- return z10.array(z10.record(z10.string(), z10.unknown()));
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 z10.unknown();
3385
+ if (field.options === null || field.options.length === 0) return z12.unknown();
2890
3386
  const values = field.options.map((o) => o.value);
2891
- return z10.custom(
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 z10.string();
3397
+ return z12.string();
2902
3398
  default:
2903
- return z10.unknown();
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 z10.object(shape);
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 = z11.string().min(1).regex(/^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/, "not an npm package name");
2946
- var CatalogShape = z11.looseObject({
2947
- name: z11.string(),
2948
- version: z11.string(),
2949
- description: z11.string(),
2950
- keywords: z11.array(z11.string()).optional(),
2951
- installed: z11.boolean(),
2952
- capabilities: z11.array(z11.string()).nullable()
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 = z11.looseObject({
2955
- manifest: z11.looseObject({
2956
- id: z11.string(),
2957
- packageName: z11.string(),
2958
- packageVersion: z11.string(),
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: z11.boolean().nullish(),
2961
- removable: z11.boolean().nullish()
3456
+ protected: z13.boolean().nullish(),
3457
+ removable: z13.boolean().nullish()
2962
3458
  })
2963
3459
  });
2964
- var UpdateShape = z11.looseObject({
2965
- name: z11.string(),
2966
- currentVersion: z11.string(),
2967
- latestVersion: z11.string(),
2968
- category: z11.enum(["addon", "core"]),
2969
- requiresRestart: z11.boolean(),
2970
- isSystem: z11.boolean()
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 = z11.looseObject({
2973
- installations: z11.array(
2974
- z11.looseObject({
2975
- nodeId: z11.string(),
2976
- isHub: z11.boolean(),
2977
- isOnline: z11.boolean(),
2978
- version: z11.string().nullable(),
2979
- source: z11.string()
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: z11.array(z11.looseObject({ nodeId: z11.string(), error: z11.string() }))
3478
+ unreachable: z13.array(z13.looseObject({ nodeId: z13.string(), error: z13.string() }))
2983
3479
  });
2984
- var VersionsShape = z11.array(
2985
- z11.looseObject({ version: z11.string(), deprecated: z11.string().optional() })
3480
+ var VersionsShape = z13.array(
3481
+ z13.looseObject({ version: z13.string(), deprecated: z13.string().optional() })
2986
3482
  );
2987
- var UninstallShape = z11.looseObject({
2988
- success: z11.boolean(),
2989
- packageName: z11.string(),
2990
- results: z11.array(
2991
- z11.looseObject({
2992
- nodeId: z11.string(),
2993
- outcome: z11.string(),
2994
- error: z11.string().optional()
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 = z11.looseObject({
2999
- runningVersion: z11.string().nullable().optional()
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", {}, z11.array(ListedAddonShape));
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 = z11.looseObject({
3014
- peerDependencies: z11.record(z11.string(), z11.string()).optional(),
3015
- engines: z11.record(z11.string(), z11.string()).optional(),
3016
- camstack: z11.looseObject({
3017
- addons: z11.array(
3018
- z11.looseObject({
3019
- execution: z11.looseObject({ placement: z11.string().optional() }).optional()
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
- z11.array(CatalogShape)
3567
+ z13.array(CatalogShape)
3072
3568
  ),
3073
- readQuery(client, "addons.list", {}, z11.array(ListedAddonShape)),
3074
- readQuery(client, "addons.listUpdates", {}, z11.array(UpdateShape)),
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
- z11.array(CatalogShape)
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
- z11.array(UpdateShape)
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: z11.string().max(100).optional(),
3378
- capability: z11.string().max(100).optional()
3873
+ query: z13.string().max(100).optional(),
3874
+ capability: z13.string().max(100).optional()
3379
3875
  },
3380
- async ({ query, capability }) => text7(
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: z11.string().max(64).optional(),
3393
- confirm_token: z11.string().optional()
3888
+ version: z13.string().max(64).optional(),
3889
+ confirm_token: z13.string().optional()
3394
3890
  },
3395
- async ({ packageName, version, confirm_token }) => text7(
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: z11.string().max(64).optional(),
3409
- nodeId: z11.string().max(100).optional(),
3410
- confirm_token: z11.string().optional()
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 }) => text7(
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: z11.string().max(100).optional(),
3430
- everywhere: z11.boolean().optional(),
3431
- confirm_token: z11.string().optional()
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 }) => text7(
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 text8
3945
+ text as text9
3450
3946
  } from "@camstack/develop";
3451
- import { z as z12 } from "zod";
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 = z12.union([z12.string(), z12.looseObject({ name: z12.string() })]);
3465
- var InstalledAddonShape = z12.looseObject({
3466
- manifest: z12.looseObject({
3467
- id: z12.string(),
3468
- name: z12.string().optional(),
3469
- description: z12.string().optional(),
3470
- packageName: z12.string(),
3471
- packageVersion: z12.string(),
3472
- capabilities: z12.array(CapabilityEntry).optional()
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 = z12.looseObject({
3476
- name: z12.string(),
3477
- version: z12.string(),
3478
- description: z12.string(),
3479
- keywords: z12.array(z12.string()),
3480
- installed: z12.boolean(),
3481
- capabilities: z12.array(z12.string()).nullable()
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 = z12.looseObject({
3484
- providers: z12.array(z12.looseObject({ addonId: z12.string(), label: z12.string() }))
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", {}, z12.array(InstalledAddonShape)),
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
- z12.array(CatalogShape2)
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 = z12.looseObject({
3557
- stableId: z12.string(),
3558
- type: z12.string(),
3559
- suggestedName: z12.string(),
3560
- prefilledConfig: z12.record(z12.string(), z12.unknown())
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 = z12.looseObject({ candidates: z12.array(CandidateShape) });
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
- z12.unknown()
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 = z12.looseObject({ id: z12.number(), name: z12.string() });
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 needs = plan.secretNeeds.map((f) => secretRequired(f, link));
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: needs,
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: needs,
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: needs
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: z12.string().max(100).optional() },
3777
- async ({ query }) => text8(await configureCameraProviders(ctx, 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: z12.string().min(1),
3784
- params: z12.record(z12.string(), z12.unknown()).optional()
4279
+ addonId: z14.string().min(1),
4280
+ params: z14.record(z14.string(), z14.unknown()).optional()
3785
4281
  },
3786
- async ({ addonId, params }) => text8(
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: z12.string().min(1),
3798
- config: z12.record(z12.string(), z12.unknown()),
3799
- confirm_token: z12.string().optional()
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 }) => text8(await addCamera(ctx, { 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 text9
4303
+ text as text10
3808
4304
  } from "@camstack/develop";
3809
- import { z as z13 } from "zod";
4305
+ import { z as z15 } from "zod";
3810
4306
  var MAX_TARGETS = 50;
3811
- var TargetKindShape = z13.looseObject({
3812
- kind: z13.string(),
3813
- label: z13.string(),
3814
- addonId: z13.string(),
3815
- supportsDiscovery: z13.boolean().optional(),
3816
- configSchema: z13.unknown(),
3817
- destinationKeys: z13.array(z13.string()).optional()
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 = z13.looseObject({
3820
- id: z13.string(),
3821
- name: z13.string(),
3822
- kind: z13.string(),
3823
- addonId: z13.string(),
3824
- enabled: z13.boolean(),
3825
- config: z13.record(z13.string(), z13.unknown())
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 = z13.looseObject({
3828
- success: z13.boolean(),
3829
- error: z13.string().optional()
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
- z13.array(TargetKindShape)
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", {}, z13.array(TargetShape))
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", {}, z13.array(TargetShape));
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", {}, z13.array(TargetShape))
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 needs = (form?.fields ?? []).filter((f) => view.incomplete.includes(f.key)).map((f) => secretRequired(f, link));
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: needs
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 () => text9(await configureNotifiers(ctx))
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 () => text9(await configureNotificationTargets(ctx))
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: z13.string().min(1).optional(),
4124
- kind: z13.string().min(1),
4125
- name: z13.string().min(1),
4126
- config: z13.record(z13.string(), z13.unknown()),
4127
- enabled: z13.boolean().optional(),
4128
- confirm_token: z13.string().optional()
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 }) => text9(
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: z13.string().min(1), confirm_token: z13.string().optional() },
4148
- async ({ targetId, confirm_token }) => text9(await testNotificationTarget(ctx, { 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 z18 } from "zod";
4154
- import { jwtClaims, text as text10 } from "@camstack/develop";
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 z14 } from "zod";
4158
- var NodesSchema = z14.array(
4159
- z14.looseObject({
4160
- id: z14.string(),
4161
- isOnline: z14.boolean(),
4162
- name: z14.string().optional()
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 = z14.looseObject({
4166
- platform: z14.enum(["darwin", "linux", "win32"]),
4167
- arch: z14.enum(["arm64", "x64"]),
4168
- cpuModel: z14.string(),
4169
- cpuCores: z14.number(),
4170
- totalRAM_MB: z14.number(),
4171
- availableRAM_MB: z14.number(),
4172
- gpu: z14.looseObject({
4173
- type: z14.enum(["nvidia", "amd", "intel", "apple"]),
4174
- name: z14.string()
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: z14.looseObject({
4177
- type: z14.enum(["apple-ane", "intel-npu", "amd-xdna"])
4672
+ npu: z16.looseObject({
4673
+ type: z16.enum(["apple-ane", "intel-npu", "amd-xdna"])
4178
4674
  }).nullable()
4179
4675
  });
4180
- var InferenceDevicesSchema = z14.looseObject({
4181
- reachable: z14.boolean(),
4182
- devices: z14.array(
4183
- z14.looseObject({
4184
- key: z14.string(),
4185
- backend: z14.string(),
4186
- device: z14.string(),
4187
- available: z14.boolean(),
4188
- presence: z14.enum(["present", "absent", "unknown"]).optional(),
4189
- enabled: z14.boolean(),
4190
- exclusion: z14.string().nullable(),
4191
- defaultModelId: z14.string()
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 z15 } from "zod";
4316
- var DeviceSchema = z15.looseObject({
4317
- id: z15.number(),
4318
- name: z15.string(),
4319
- type: z15.string().optional(),
4320
- addonId: z15.string().optional(),
4321
- location: z15.string().nullable().optional(),
4322
- disabled: z15.boolean().optional(),
4323
- online: z15.boolean().optional(),
4324
- isCamera: z15.boolean().optional(),
4325
- features: z15.array(z15.string()).optional()
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 = z15.array(DeviceSchema);
4329
- var BindingsSchema2 = z15.looseObject({
4330
- entries: z15.array(
4331
- z15.looseObject({
4332
- capName: z15.string(),
4333
- kind: z15.string(),
4334
- providerAddonId: z15.string(),
4335
- providerNodeId: z15.string()
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 = z15.record(z15.string(), z15.unknown());
4340
- var DegradationSchema = z15.looseObject({
4341
- stage: z15.string(),
4342
- reason: z15.string(),
4343
- elapsedMs: z15.number().optional()
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 = z15.looseObject({
4346
- deviceId: z15.number(),
4347
- assignment: z15.looseObject({
4348
- detectionNodeId: z15.string().nullable(),
4349
- decoderNodeId: z15.string().nullable(),
4350
- sourceNodeId: z15.string().nullable(),
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: z15.record(z15.string(), z15.string().optional()).optional()
4848
+ reasons: z17.record(z17.string(), z17.string().optional()).optional()
4353
4849
  }),
4354
- broker: z15.looseObject({
4355
- profiles: z15.array(
4356
- z15.looseObject({
4357
- profile: z15.string(),
4358
- status: z15.string(),
4359
- subscribers: z15.number()
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: z15.looseObject({
4364
- nodeId: z15.string(),
4365
- phase: z15.string(),
4366
- actualFps: z15.number(),
4367
- configuredFps: z15.number(),
4368
- provisioning: z15.looseObject({
4369
- state: z15.string(),
4370
- error: z15.string().optional()
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: z15.looseObject({
4374
- mode: z15.string(),
4375
- active: z15.boolean(),
4376
- storageBytes: z15.number()
4869
+ recording: z17.looseObject({
4870
+ mode: z17.string(),
4871
+ active: z17.boolean(),
4872
+ storageBytes: z17.number()
4377
4873
  }).nullable(),
4378
- switchedOff: z15.array(z15.string()),
4379
- degraded: z15.array(DegradationSchema),
4380
- fetchedAt: z15.number()
4874
+ switchedOff: z17.array(z17.string()),
4875
+ degraded: z17.array(DegradationSchema),
4876
+ fetchedAt: z17.number()
4381
4877
  });
4382
- var CameraStatusListSchema = z15.array(CameraStatusSchema);
4383
- var ObjectEventSchema = z15.looseObject({
4384
- id: z15.number(),
4385
- timestamp: z15.number(),
4386
- className: z15.string().optional(),
4387
- trackId: z15.string().optional()
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 = z15.array(ObjectEventSchema);
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 = z15.looseObject({
4398
- lastFetchedAt: z15.number().optional(),
4399
- lastChangedAt: z15.number().optional()
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 z16 } from "zod";
4651
- var LevelSchema = z16.enum(["debug", "info", "warn", "error"]);
4652
- var LogEntrySchema = z16.looseObject({
4653
- timestamp: z16.string(),
4654
- level: z16.string(),
4655
- scope: z16.string().optional(),
4656
- message: z16.string(),
4657
- tags: z16.record(z16.string(), z16.string().optional()).optional(),
4658
- meta: z16.record(z16.string(), z16.unknown()).optional()
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 = z16.looseObject({
4661
- entries: z16.array(LogEntrySchema),
4662
- done: z16.boolean(),
4663
- source: z16.string()
5156
+ var LogPageSchema = z18.looseObject({
5157
+ entries: z18.array(LogEntrySchema),
5158
+ done: z18.boolean(),
5159
+ source: z18.string()
4664
5160
  });
4665
- var LogSourcesSchema = z16.array(
4666
- z16.looseObject({
4667
- addonId: z16.string(),
4668
- readable: z16.boolean(),
4669
- isDefault: z16.boolean()
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 z17 } from "zod";
4885
- var TopologyNodeSchema = z17.looseObject({
4886
- id: z17.string(),
4887
- name: z17.string().optional(),
4888
- platform: z17.string().optional(),
4889
- isHub: z17.boolean().optional(),
4890
- isOnline: z17.boolean(),
4891
- addons: z17.array(z17.looseObject({ capabilities: z17.array(z17.string()) })).optional(),
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: z17.boolean().optional()
5389
+ capabilitiesRegistered: z19.boolean().optional()
4894
5390
  });
4895
- var TopologySchema = z17.array(TopologyNodeSchema);
4896
- var ServerStatusSchema = z17.looseObject({
4897
- runningVersion: z17.string(),
4898
- restartRequired: z17.boolean().optional(),
4899
- updateAvailable: z17.boolean().optional(),
4900
- updateState: z17.string().optional()
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 = z17.looseObject({
4903
- severity: z17.string(),
4904
- title: z17.string(),
4905
- message: z17.string().optional(),
4906
- status: z17.string()
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 = z17.array(AlertSchema);
4909
- var AddonUpdateSchema = z17.looseObject({
4910
- name: z17.string(),
4911
- currentVersion: z17.string(),
4912
- latestVersion: z17.string()
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 = z17.array(AddonUpdateSchema);
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-tools.ts
5044
- var IDENTITY_CLAIMS = ["sub", "username", "name", "role"];
5045
- function identityOf(token) {
5046
- const claims = jwtClaims(token);
5047
- const out = {};
5048
- if (claims === null) return out;
5049
- for (const key of IDENTITY_CLAIMS) {
5050
- const value = claims[key];
5051
- if (typeof value === "string" && value !== "") out[key] = value;
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
- async function whoami(ctx) {
5056
- const outcome = await ctx.hub();
5057
- if (!outcome.ok) return noSessionResult(outcome);
5058
- const c = outcome.connection;
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
- status: "ok",
5061
- hub: c.server,
5062
- tier: c.tier,
5063
- source: c.source,
5064
- identity: identityOf(c.token)
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 registerOpsTools(server, ctx) {
5068
- server.tool(
5069
- "ops_whoami",
5070
- "Which hub this MCP talks to and as whom, from the `camstack login` session (no network call). If there is no session, says to run `camstack login <host>` in a terminal.",
5071
- {},
5072
- async () => text10(await whoami(ctx))
5073
- );
5074
- server.tool(
5075
- "ops_status",
5076
- 'Cluster summary: every node (online, platform, server version, capability count, "online-but-capless" flag), the active alerts and the addons with an update. A part the hub did not answer is null with its reason in `unavailable`, never zero. Node list and updates need an admin `camstack login`.',
5077
- {},
5078
- async () => text10(await opsStatus(ctx))
5079
- );
5080
- server.tool(
5081
- "ops_device",
5082
- "One device: online state, provider addon and node, bound capabilities, runtime-state age and, for a camera, its pipeline, recording state, the functions an operator switched off (not faults) and its latest object events.",
5083
- { deviceId: z18.number().int() },
5084
- async ({ deviceId }) => text10(await opsDevice(ctx, deviceId))
5085
- );
5086
- server.tool(
5087
- "ops_unhealthy_devices",
5088
- "Every enabled device that is unhealthy, each with a typed reason (offline, detection-provisioning-failed, detection-not-assigned, recording-inactive). A function an operator switched off is never listed; a camera whose status could not be fully read is listed under `unknown`.",
5089
- {},
5090
- async () => text10(await opsUnhealthyDevices(ctx))
5091
- );
5092
- server.tool(
5093
- "ops_logs",
5094
- "Hub logs scoped by deviceId, addon, node, level, window and free text, grouped and deduplicated with counts. Says which tier answered (ring = recent in-RAM; persisted = the default log destination). With `around` (a track or event id) returns `contextLines` lines on each side of its first mention. Needs an admin `camstack login`.",
5095
- {
5096
- deviceId: z18.number().int().optional(),
5097
- addonId: z18.string().optional(),
5098
- nodeId: z18.string().optional(),
5099
- level: z18.enum(["debug", "info", "warn", "error"]).optional(),
5100
- windowMinutes: z18.number().int().min(1).max(1440).optional(),
5101
- text: z18.string().max(200).optional(),
5102
- around: z18.string().max(200).optional(),
5103
- contextLines: z18.number().int().min(1).max(50).optional()
5104
- },
5105
- async (args) => text10(
5106
- await opsLogs(ctx, {
5107
- ...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
5108
- ...args.addonId ? { addonId: args.addonId } : {},
5109
- ...args.nodeId ? { nodeId: args.nodeId } : {},
5110
- ...args.level ? { level: args.level } : {},
5111
- ...args.windowMinutes !== void 0 ? { windowMinutes: args.windowMinutes } : {},
5112
- ...args.text ? { text: args.text } : {},
5113
- ...args.around ? { around: args.around } : {},
5114
- ...args.contextLines !== void 0 ? { contextLines: args.contextLines } : {}
5115
- })
5116
- )
5117
- );
5118
- server.tool(
5119
- "ops_events",
5120
- "Find tracks (and, for one camera, object events) by camera, class, zone, time window and free text. Each result carries an admin-UI link and a viewer link built on the hub address flagged for app connections.",
5121
- {
5122
- deviceId: z18.number().int().optional(),
5123
- className: z18.string().optional(),
5124
- zone: z18.string().optional(),
5125
- text: z18.string().max(200).optional(),
5126
- windowMinutes: z18.number().int().min(1).max(10080).optional(),
5127
- limit: z18.number().int().min(1).max(50).optional()
5128
- },
5129
- async (args) => text10(
5130
- await opsEvents(ctx, {
5131
- ...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
5132
- ...args.className ? { className: args.className } : {},
5133
- ...args.zone ? { zone: args.zone } : {},
5134
- ...args.text ? { text: args.text } : {},
5135
- ...args.windowMinutes !== void 0 ? { windowMinutes: args.windowMinutes } : {},
5136
- ...args.limit !== void 0 ? { limit: args.limit } : {}
5137
- })
5138
- )
5139
- );
5140
- server.tool(
5141
- "ops_track",
5142
- "One track with its media audit: best shot, keyframes, every missing media kind with the reason the track recorded for it, and the audit findings. Links to the admin UI and the viewer.",
5143
- { deviceId: z18.number().int(), trackId: z18.string().min(1) },
5144
- async ({ deviceId, trackId }) => text10(await opsTrack(ctx, { deviceId, trackId }))
5145
- );
5146
- server.tool(
5147
- "ops_accelerators",
5148
- 'Per node: the inference devices it has, the Auto-tune hardware class the node maps to, and whether the measured detector-latency table has a row for that class and accelerator. A node that could not be read is null with a reason, never "no accelerator". Needs an admin `camstack login`.',
5149
- {},
5150
- async () => text10(await opsAccelerators(ctx))
5151
- );
5687
+ function needsServerUpdate(verdict) {
5688
+ return verdict.updateAvailable === true || verdict.restartRequired === true;
5152
5689
  }
5153
-
5154
- // src/mcp/configure-tools.ts
5155
- var CurrentDeviceSchema = z19.object({ name: z19.string() }).nullable();
5156
- var RENAME_DEVICE_TOOL = "configure_rename_device";
5157
- function renameDeviceSpec(client) {
5158
- return {
5159
- tool: RENAME_DEVICE_TOOL,
5160
- async dryRun(args) {
5161
- const current = CurrentDeviceSchema.safeParse(
5162
- await client.call("GET", "deviceManager.getDevice", {
5163
- deviceId: args.deviceId
5164
- })
5165
- );
5166
- if (!current.success || current.data === null) {
5167
- return {
5168
- confirmable: false,
5169
- describe: {
5170
- problem: `device ${args.deviceId} was not found on the hub`
5171
- }
5172
- };
5173
- }
5174
- if (current.data.name === args.name) {
5175
- return {
5176
- confirmable: false,
5177
- describe: {
5178
- problem: `device ${args.deviceId} is already named "${args.name}"`
5179
- }
5180
- };
5181
- }
5182
- return {
5183
- confirmable: true,
5184
- describe: {
5185
- action: "rename device",
5186
- deviceId: args.deviceId,
5187
- from: current.data.name,
5188
- to: args.name
5189
- }
5190
- };
5191
- },
5192
- async execute(args) {
5193
- await client.call("POST", "deviceManager.setName", {
5194
- deviceId: args.deviceId,
5195
- name: args.name
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 runGated3(ctx.gate, renameDeviceSpec(outcome.connection.client), args, confirmToken);
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 text12 } from "@camstack/develop";
5231
- import { z as z23 } from "zod";
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 z22 } from "zod";
5734
+ import { z as z23 } from "zod";
5245
5735
 
5246
5736
  // src/mcp/setup-install.ts
5247
- import { z as z21 } from "zod";
5737
+ import { z as z22 } from "zod";
5248
5738
 
5249
5739
  // src/mcp/setup-install-rules.ts
5250
- import { z as z20 } from "zod";
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 = z20.enum(HOST_KINDS);
5748
+ var HostKindSchema = z21.enum(HOST_KINDS);
5259
5749
  var ROLES = ["hub", "agent"];
5260
- var RoleSchema = z20.enum(ROLES);
5750
+ var RoleSchema = z21.enum(ROLES);
5261
5751
  var ACCELERATORS = ["intel", "amd", "coral", "apple", "cpu"];
5262
- var AcceleratorSchema = z20.enum(ACCELERATORS);
5263
- var InstallPathsSchema = z20.object({
5264
- data: z20.string().min(1),
5265
- recordings: z20.string().min(1),
5266
- config: z20.string().min(1).optional(),
5267
- backups: z20.string().min(1).optional()
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 = z20.object({
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: z20.string().min(1).optional()
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 text14 = line.trim();
5904
+ const text15 = line.trim();
5415
5905
  if (indent === 4) {
5416
5906
  sub = null;
5417
- const m = /^([A-Za-z_]+):\s*(.*)$/.exec(text14);
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 (text14.startsWith("- ")) {
5912
+ if (text15.startsWith("- ")) {
5423
5913
  const list = fields.lists.get(key) ?? [];
5424
- list.push(unquote(text14.slice(2)));
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(text14);
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(text14);
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 = z21.object({
5536
- tag_name: z21.string(),
5537
- assets: z21.array(z21.object({ name: z21.string(), browser_download_url: z21.string() }))
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 = z22.object({
5829
- agents: z22.array(
5830
- z22.object({
5831
- nodeId: z22.string(),
5832
- host: z22.string(),
5833
- port: z22.number(),
5834
- version: z22.string().nullable(),
5835
- name: z22.string().nullable(),
5836
- state: z22.enum(["unpaired", "pending", "paired"])
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 = z23.object({ version: z23.string() });
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 () => text12(await about(ctx))
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: z23.string().min(1).optional().describe("Agents only: the hub to dial.")
6417
+ hubAddress: z24.string().min(1).optional().describe("Agents only: the hub to dial.")
5928
6418
  },
5929
- async (input) => text12(await installGuide(input, ctx.fetch))
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: z23.number().int().min(200).max(15e3).optional() },
5935
- async ({ timeoutMs }) => text12(await discoverHubsTool(timeoutMs, deps))
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: z23.string().min(1) },
5941
- async ({ server: address }) => text12(await connectTool(address, deps))
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: z23.string().min(1).optional()
6440
+ hubAddress: z24.string().min(1).optional()
5951
6441
  },
5952
- async (input) => text12(await addNodeTool(input, ctx))
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 text13 } from "@camstack/develop";
5958
- import { z as z28 } from "zod";
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 z24 } from "zod";
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 = z24.enum(EXPECTED_ACCELERATORS);
5974
- var HardwareSchema2 = z24.looseObject({
5975
- platform: z24.enum(["darwin", "linux", "win32"]),
5976
- arch: z24.enum(["arm64", "x64"]),
5977
- cpuModel: z24.string(),
5978
- gpu: z24.looseObject({ type: z24.string(), name: z24.string() }).nullable(),
5979
- npu: z24.looseObject({ type: z24.string() }).nullable()
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 = z24.looseObject({
5982
- reachable: z24.boolean(),
5983
- devices: z24.array(
5984
- z24.looseObject({
5985
- key: z24.string(),
5986
- available: z24.boolean(),
5987
- presence: z24.enum(["present", "absent", "unknown"]).optional(),
5988
- enabled: z24.boolean(),
5989
- exclusion: z24.string().nullable()
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 runGated4
7487
+ runGated as runGated5
6267
7488
  } from "@camstack/develop";
6268
- import { z as z25 } from "zod";
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 = z25.looseObject({
6291
- platform: z25.enum(["darwin", "linux", "win32"]),
6292
- arch: z25.enum(["arm64", "x64"]),
6293
- cpuModel: z25.string(),
6294
- cpuCores: z25.number(),
6295
- totalRAM_MB: z25.number(),
6296
- availableRAM_MB: z25.number(),
6297
- gpu: z25.looseObject({
6298
- type: z25.enum(["nvidia", "amd", "intel", "apple"]),
6299
- name: z25.string()
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: z25.looseObject({ type: z25.enum(["apple-ane", "intel-npu", "amd-xdna"]) }).nullable()
7522
+ npu: z30.looseObject({ type: z30.enum(["apple-ane", "intel-npu", "amd-xdna"]) }).nullable()
6302
7523
  });
6303
- var InferenceDevicesSchema3 = z25.looseObject({
6304
- reachable: z25.boolean(),
6305
- devices: z25.array(
6306
- z25.looseObject({
6307
- key: z25.string(),
6308
- backend: z25.string(),
6309
- available: z25.boolean(),
6310
- enabled: z25.boolean()
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 = z25.looseObject({
6315
- run: z25.looseObject({
6316
- runId: z25.string(),
6317
- nodeId: z25.string(),
6318
- state: z25.string(),
6319
- endReason: z25.string().nullable().optional(),
6320
- error: z25.string().nullable().optional()
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 = z25.array(
6324
- z25.looseObject({
6325
- id: z25.string(),
6326
- formats: z25.record(z25.string(), z25.looseObject({ downloaded: z25.boolean().optional() })).optional()
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 = z25.looseObject({
6330
- kind: z25.string().optional(),
6331
- detections: z25.array(
6332
- z25.looseObject({
6333
- debug: z25.looseObject({
6334
- stepLatencyMs: z25.record(z25.string(), z25.number()).optional()
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: z25.array(z25.unknown()).optional(),
6339
- debug: z25.looseObject({
6340
- stepTimings: z25.array(
6341
- z25.looseObject({
6342
- source: z25.string().optional(),
6343
- ms: z25.number().optional()
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: z25.number().optional(),
6347
- totalInferenceMs: z25.number().optional()
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 timing = r.debug?.stepTimings?.find(
7595
+ const timing2 = r.debug?.stepTimings?.find(
6375
7596
  (t) => t.source === DETECTOR_STEP && typeof t.ms === "number"
6376
7597
  );
6377
- const candidates = [fromDetection, timing?.ms, r.debug?.predictMs, r.debug?.totalInferenceMs];
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 runGated4(ctx.gate, benchmarkRunSpec(outcome.connection.client, deps), args, confirmToken);
6633
- }
6634
- var ResultsSchema = z25.array(
6635
- z25.looseObject({
6636
- nodeId: z25.string(),
6637
- cell: z25.looseObject({
6638
- phase: z25.string(),
6639
- deviceKey: z25.string(),
6640
- modelId: z25.string(),
6641
- p50Ms: z25.number().optional(),
6642
- fps: z25.number().optional()
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 z26 } from "zod";
6696
- var DeviceSchema2 = z26.looseObject({
6697
- id: z26.number(),
6698
- name: z26.string(),
6699
- addonId: z26.string().optional(),
6700
- disabled: z26.boolean().optional(),
6701
- online: z26.boolean().optional()
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(text14) {
6705
- return text14.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length >= 3);
7925
+ function words(text15) {
7926
+ return text15.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length >= 3);
6706
7927
  }
6707
- function isGeneric(parts) {
6708
- return parts.some((p) => GENERIC.test(p));
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 = (parts) => {
6730
- if (isGeneric(parts)) return false;
6731
- const joined = parts.join(" ").toLowerCase();
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
- execFile(
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 runGated5
8121
+ runGated as runGated6
6901
8122
  } from "@camstack/develop";
6902
- import { z as z27 } from "zod";
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 parts = ip.split(".").map((p) => Number(p));
6912
- if (parts.length !== 4) return null;
6913
- if (parts.some((p) => !Number.isInteger(p) || p < 0 || p > 255)) return null;
6914
- return parts;
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 = z27.enum(DRAFT_KINDS);
6956
- var BenchmarkCellsSchema = z27.record(
6957
- z27.string().regex(/^[a-z0-9][a-z0-9._-]{1,63}$/),
6958
- z27.object({ p50Ms: z27.number().positive(), fps: z27.number().positive() })
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 = z27.looseObject({
6964
- repository: z27.union([z27.string(), z27.looseObject({ url: z27.string() })]).optional()
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 = z27.looseObject({ runningVersion: z27.string() });
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(text14) {
7209
- const s = text14.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
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 runGated5(ctx.gate, issueSpec({ ctx, deps }), args, confirmToken);
8486
+ return runGated6(ctx.gate, issueSpec({ ctx, deps }), args, confirmToken);
7266
8487
  }
7267
8488
  async function supportDraftPr(ctx, args, confirmToken) {
7268
- return runGated5(ctx.gate, prSpec(ctx), args, confirmToken);
8489
+ return runGated6(ctx.gate, prSpec(ctx), args, confirmToken);
7269
8490
  }
7270
8491
 
7271
8492
  // src/mcp/support-tools.ts
7272
- var ModelId = z28.string().regex(/^[a-z0-9][a-z0-9._-]{1,63}$/);
8493
+ var ModelId = z33.string().regex(/^[a-z0-9][a-z0-9._-]{1,63}$/);
7273
8494
  var DraftShape = {
7274
8495
  kind: DraftKindSchema,
7275
- title: z28.string().min(3).max(300),
7276
- summary: z28.string().min(3).max(4e3),
7277
- nodeId: z28.string().max(100).optional(),
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: z28.string().max(100).optional(),
7280
- model: z28.string().max(100).optional(),
7281
- deviceId: z28.number().int().optional(),
7282
- packageName: z28.string().max(214).optional(),
7283
- column: z28.string().max(100).optional(),
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: z28.string().optional()
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: z28.string().max(100).optional() },
7307
- async ({ nodeId }) => text13(await supportBenchmarkStatus(ctx, nodeId !== void 0 ? { 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: z28.string().min(1).max(100),
7314
- deviceKey: z28.string().min(1).max(60),
7315
- models: z28.array(ModelId).min(1).max(MAX_MODELS).optional(),
7316
- warmRuns: z28.number().int().min(1).max(MAX_WARM_RUNS).optional(),
7317
- confirm_token: z28.string().optional()
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 }) => text13(
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: z28.string().min(1).max(100), expected: ExpectedAcceleratorSchema },
7337
- async ({ nodeId, expected }) => text13(await supportDiagnoseAccelerator(ctx, { 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: z28.string().max(100).optional(),
7344
- model: z28.string().max(100).optional(),
7345
- deviceId: z28.number().int().optional()
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 }) => text13(
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) => text13(await supportDraftIssue(ctx, deps, draftArgs(input), input.confirm_token))
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) => text13(await supportDraftPr(ctx, draftArgs(input), input.confirm_token))
8586
+ async (input) => text14(await supportDraftPr(ctx, draftArgs(input), input.confirm_token))
7366
8587
  );
7367
8588
  }
7368
8589