camstack 1.2.212 → 1.2.214

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,25 @@ import {
11
11
  buildViewerLink,
12
12
  capUnitResolver,
13
13
  classifyInferenceHardware,
14
- findHubs,
14
+ convertLegacyWhen,
15
+ countWhenLeaves,
15
16
  generateAutomationBlock,
16
17
  isReadingQuantity,
17
18
  prepareRecipeUnits,
18
19
  unitsForQuantity,
19
20
  validateRecipeBounds
20
- } from "./chunk-MLWXP2SI.js";
21
+ } from "./chunk-4Y3CBGOY.js";
22
+ import {
23
+ IMAGE_TAG_BY_ACCELERATOR,
24
+ MUST_NOT_OMIT,
25
+ NO_BARE_LATEST,
26
+ cliVerdict,
27
+ isOlder,
28
+ registerSetupTools,
29
+ runCommand,
30
+ text
31
+ } from "./chunk-37A556YF.js";
32
+ import "./chunk-JPGJK5DC.js";
21
33
  import "./chunk-LMMQX4CK.js";
22
34
 
23
35
  // src/mcp/server.ts
@@ -27,14 +39,12 @@ import { MutationGate, tryConnectHub } from "@camstack/develop";
27
39
 
28
40
  // src/mcp/configure-tools.ts
29
41
  import {
30
- runGated as runGated4,
31
- text as text12
42
+ runGated as runGated4
32
43
  } from "@camstack/develop";
33
- import { z as z26 } from "zod";
44
+ import { z as z24 } from "zod";
34
45
 
35
46
  // src/mcp/configure-automations.ts
36
47
  import { z as z4 } from "zod";
37
- import { text } from "@camstack/develop";
38
48
 
39
49
  // src/mcp/configure-common.ts
40
50
  import { runGated } from "@camstack/develop";
@@ -987,7 +997,7 @@ function toActions(actions) {
987
997
  function quantityNotes(recipe) {
988
998
  const notes = [];
989
999
  const walk = (c, path) => {
990
- if (c.kind === "condition" && typeof c.value === "number" && c.unit !== void 0) {
1000
+ if (c.kind === "device-is" && typeof c.value === "number" && c.unit !== void 0) {
991
1001
  notes.push({
992
1002
  where: `${path} ${c.cap}.${c.fieldPath}`,
993
1003
  value: c.value,
@@ -997,7 +1007,7 @@ function quantityNotes(recipe) {
997
1007
  if (c.kind === "not") walk(c.child, path);
998
1008
  if (c.kind === "all" || c.kind === "any") c.children.forEach((child) => walk(child, path));
999
1009
  };
1000
- if (recipe.conditions !== void 0) walk(recipe.conditions, "condition");
1010
+ walk(recipe.when, "condition");
1001
1011
  recipe.actions.forEach((a, i) => {
1002
1012
  if (a.kind !== "cap" || a.args === void 0 || a.argUnits === void 0) return;
1003
1013
  for (const [arg, unit] of Object.entries(a.argUnits)) {
@@ -1085,8 +1095,7 @@ async function interviewAutomation(client, answers) {
1085
1095
  const conditions = answers.conditions ?? [];
1086
1096
  const actions = answers.actions ?? [];
1087
1097
  const draft = {
1088
- triggers: [outcome.trigger],
1089
- ...toCondition(conditions) !== void 0 ? { conditions: toCondition(conditions) } : {},
1098
+ when: convertLegacyWhen({ triggers: [outcome.trigger], conditions: toCondition(conditions) }),
1090
1099
  actions: toActions(actions),
1091
1100
  ...answers.cooldownMinutes !== void 0 ? {
1092
1101
  runMode: {
@@ -1184,7 +1193,7 @@ function saveAutomationSpec(client) {
1184
1193
  runsOn: "the hub",
1185
1194
  enabled: args.enabled,
1186
1195
  effect: args.enabled ? "it starts running as soon as it is saved" : "it is saved switched off",
1187
- triggers: finished.recipe.triggers.length,
1196
+ rows: countWhenLeaves(finished.recipe.when),
1188
1197
  steps: finished.recipe.actions.length,
1189
1198
  quantities: quantityNotes(finished.recipe),
1190
1199
  generatedBytes: code.length
@@ -1217,7 +1226,7 @@ var GATE_HOW = "Two steps: call without confirm_token for a preview and a one-sh
1217
1226
  function registerAutomationTools(server, ctx) {
1218
1227
  server.tool(
1219
1228
  "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.",
1229
+ "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
1230
  { answers: AutomationAnswersSchema },
1222
1231
  async ({ answers }) => text(await automationQuestionnaire(ctx, answers))
1223
1232
  );
@@ -1235,7 +1244,6 @@ function registerAutomationTools(server, ctx) {
1235
1244
  }
1236
1245
 
1237
1246
  // src/mcp/configure-calibrate.ts
1238
- import { text as text2 } from "@camstack/develop";
1239
1247
  import { z as z5 } from "zod";
1240
1248
  var CALIBRATE_APPLY_TOOL = "configure_calibrate_apply";
1241
1249
  var WINDOW_MS = 24 * 36e5;
@@ -1601,7 +1609,7 @@ function registerCalibrateTools(server, ctx) {
1601
1609
  "configure_calibrate_camera",
1602
1610
  "Read-only calibration report for one camera: recent tracks and their top-score distribution per class, likely false-positive and missed patterns, the current motion and detection settings, zones and pipeline health, and the changes it proposes. Writes nothing.",
1603
1611
  { deviceId: z5.number().int().nonnegative() },
1604
- async ({ deviceId }) => text2(await calibrateCamera(ctx, { deviceId }))
1612
+ async ({ deviceId }) => text(await calibrateCamera(ctx, { deviceId }))
1605
1613
  );
1606
1614
  server.tool(
1607
1615
  CALIBRATE_APPLY_TOOL,
@@ -1612,13 +1620,12 @@ function registerCalibrateTools(server, ctx) {
1612
1620
  patch: z5.record(z5.string(), z5.union([z5.number(), z5.string(), z5.boolean()])),
1613
1621
  confirm_token: ConfirmTokenSchema
1614
1622
  },
1615
- async ({ deviceId, cap: cap2, patch, confirm_token }) => text2(await calibrateApply(ctx, { deviceId, cap: cap2, patch }, confirm_token))
1623
+ async ({ deviceId, cap: cap2, patch, confirm_token }) => text(await calibrateApply(ctx, { deviceId, cap: cap2, patch }, confirm_token))
1616
1624
  );
1617
1625
  }
1618
1626
 
1619
1627
  // src/mcp/configure-zones-alarm.ts
1620
1628
  import { randomUUID } from "crypto";
1621
- import { text as text3 } from "@camstack/develop";
1622
1629
  import { z as z6 } from "zod";
1623
1630
  var ZONE_UPSERT_TOOL = "configure_zone_upsert";
1624
1631
  var ALARM_SET_TOOL = "configure_alarm_set";
@@ -1909,7 +1916,7 @@ function registerZonesAlarmTools(server, ctx) {
1909
1916
  "configure_zones",
1910
1917
  "Read the detection zones drawn on one camera (id, name, kind, points, normalised 0..1) and a summary of its motion mask. Writes nothing.",
1911
1918
  { deviceId: z6.number().int().nonnegative() },
1912
- async ({ deviceId }) => text3(await readZonesTool(ctx, { deviceId }))
1919
+ async ({ deviceId }) => text(await readZonesTool(ctx, { deviceId }))
1913
1920
  );
1914
1921
  server.tool(
1915
1922
  ZONE_UPSERT_TOOL,
@@ -1922,7 +1929,7 @@ function registerZonesAlarmTools(server, ctx) {
1922
1929
  points: z6.array(PointSchema).min(2).max(64),
1923
1930
  confirm_token: ConfirmTokenSchema
1924
1931
  },
1925
- async ({ deviceId, zoneId, name, kind, points, confirm_token }) => text3(
1932
+ async ({ deviceId, zoneId, name, kind, points, confirm_token }) => text(
1926
1933
  await zoneUpsert(
1927
1934
  ctx,
1928
1935
  {
@@ -1940,7 +1947,7 @@ function registerZonesAlarmTools(server, ctx) {
1940
1947
  "configure_alarm",
1941
1948
  "Read the alarm panels of the hub: state, armed mode, the modes each accepts and whether it needs a PIN. Writes nothing.",
1942
1949
  { deviceId: z6.number().int().nonnegative().optional() },
1943
- async ({ deviceId }) => text3(await readAlarm(ctx, deviceId !== void 0 ? { deviceId } : {}))
1950
+ async ({ deviceId }) => text(await readAlarm(ctx, deviceId !== void 0 ? { deviceId } : {}))
1944
1951
  );
1945
1952
  server.tool(
1946
1953
  ALARM_SET_TOOL,
@@ -1951,7 +1958,7 @@ function registerZonesAlarmTools(server, ctx) {
1951
1958
  mode: AlarmArmModeSchema.optional(),
1952
1959
  confirm_token: ConfirmTokenSchema
1953
1960
  },
1954
- async ({ deviceId, action, mode, confirm_token }) => text3(
1961
+ async ({ deviceId, action, mode, confirm_token }) => text(
1955
1962
  await alarmSet(
1956
1963
  ctx,
1957
1964
  { deviceId, action, ...mode !== void 0 ? { mode } : {} },
@@ -1962,7 +1969,6 @@ function registerZonesAlarmTools(server, ctx) {
1962
1969
  }
1963
1970
 
1964
1971
  // src/mcp/configure-network.ts
1965
- import { text as text4 } from "@camstack/develop";
1966
1972
  import { z as z7 } from "zod";
1967
1973
  var REMOTE_ACCESS_SET_TOOL = "configure_remote_access_set";
1968
1974
  var RemoteAddonSchema = z7.enum(["cloudflare-tunnel", "cloudflare-turn", "static-turn"]);
@@ -2146,7 +2152,7 @@ function registerNetworkTools(server, ctx) {
2146
2152
  "configure_remote_access",
2147
2153
  "Read how the hub is reachable from outside: the access status and public endpoint, and the settings of the tunnel and TURN addons. Secret fields are reported as configured true/false, never their value. Writes nothing.",
2148
2154
  {},
2149
- async () => text4(await readRemoteAccess(ctx))
2155
+ async () => text(await readRemoteAccess(ctx))
2150
2156
  );
2151
2157
  server.tool(
2152
2158
  REMOTE_ACCESS_SET_TOOL,
@@ -2156,12 +2162,11 @@ function registerNetworkTools(server, ctx) {
2156
2162
  patch: z7.record(z7.string(), z7.union([z7.string(), z7.number(), z7.boolean()])),
2157
2163
  confirm_token: ConfirmTokenSchema
2158
2164
  },
2159
- async ({ addonId, patch, confirm_token }) => text4(await remoteAccessSet(ctx, { addonId, patch }, confirm_token))
2165
+ async ({ addonId, patch, confirm_token }) => text(await remoteAccessSet(ctx, { addonId, patch }, confirm_token))
2160
2166
  );
2161
2167
  }
2162
2168
 
2163
2169
  // src/mcp/configure-exports.ts
2164
- import { text as text5 } from "@camstack/develop";
2165
2170
  import { z as z8 } from "zod";
2166
2171
  var EXPORT_SET_TOOL = "configure_export_set";
2167
2172
  var ProvidersSchema = z8.array(z8.looseObject({ addonId: z8.string() }));
@@ -2324,7 +2329,7 @@ function registerExportTools(server, ctx) {
2324
2329
  "configure_exports",
2325
2330
  "Read the exporters (Alexa, Google Home, Home Assistant, HomeKit): link state, how many devices each shows and which cameras. Pairing codes are never shown. Writes nothing.",
2326
2331
  { deviceId: z8.number().int().nonnegative().optional() },
2327
- async ({ deviceId }) => text5(await readExports(ctx, deviceId !== void 0 ? { deviceId } : {}))
2332
+ async ({ deviceId }) => text(await readExports(ctx, deviceId !== void 0 ? { deviceId } : {}))
2328
2333
  );
2329
2334
  server.tool(
2330
2335
  EXPORT_SET_TOOL,
@@ -2335,43 +2340,535 @@ function registerExportTools(server, ctx) {
2335
2340
  exposed: z8.boolean(),
2336
2341
  confirm_token: ConfirmTokenSchema
2337
2342
  },
2338
- async ({ addonId, deviceId, exposed, confirm_token }) => text5(await exportSet(ctx, { addonId, deviceId, exposed }, confirm_token))
2343
+ async ({ addonId, deviceId, exposed, confirm_token }) => text(await exportSet(ctx, { addonId, deviceId, exposed }, confirm_token))
2339
2344
  );
2340
2345
  server.tool(
2341
2346
  "configure_ha_guide",
2342
2347
  "Read-only guide for showing CamStack cameras in Home Assistant, including the HACS route. Needs no hub.",
2343
2348
  {},
2344
- async () => text5({ status: "ok", guide: HA_GUIDE })
2349
+ async () => text({ status: "ok", guide: HA_GUIDE })
2345
2350
  );
2346
2351
  }
2347
2352
 
2348
- // src/mcp/configure-nc-rules.ts
2353
+ // src/mcp/configure-oidc.ts
2354
+ import { z as z10 } from "zod";
2355
+
2356
+ // src/mcp/configure-oidc-providers.ts
2349
2357
  import { z as z9 } from "zod";
2350
- import { text as text6 } from "@camstack/develop";
2358
+ var OidcProviderSchema = z9.enum([
2359
+ "authentik",
2360
+ "pocket-id",
2361
+ "authelia",
2362
+ "keycloak",
2363
+ "zitadel",
2364
+ "kanidm",
2365
+ "google",
2366
+ "microsoft-entra",
2367
+ "auth0",
2368
+ "okta",
2369
+ "generic"
2370
+ ]);
2371
+ var PIECE_QUESTION = {
2372
+ host: "host: the provider host name, with or without https:// (no path)",
2373
+ application_slug: "application_slug: the application slug (authentik) or client name (kanidm)",
2374
+ realm: "realm: the Keycloak realm name",
2375
+ tenant_id: "tenant_id: the Entra directory (tenant) id, a GUID or a verified domain"
2376
+ };
2377
+ function pieceQuestion(piece) {
2378
+ return PIECE_QUESTION[piece];
2379
+ }
2380
+ function hostOrigin(host) {
2381
+ const trimmed = host.trim().replace(/\/+$/, "");
2382
+ return /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
2383
+ }
2384
+ var COMMON_STEPS = [
2385
+ "Create the client as an OpenID Connect / OAuth2 web application, authorization-code flow.",
2386
+ "Paste the redirect URI shown below as the ONLY allowed redirect URI.",
2387
+ "Allow the scopes listed below."
2388
+ ];
2389
+ var OIDC_PROVIDERS = {
2390
+ authentik: {
2391
+ label: "authentik",
2392
+ pieces: ["host", "application_slug"],
2393
+ issuerFormat: "https://<host>/application/o/<application_slug>/",
2394
+ scopes: "openid profile email",
2395
+ publicPkce: true,
2396
+ usernameClaim: "preferred_username",
2397
+ whereToCreate: "Admin interface > Applications > Applications > Create with Provider (OAuth2/OpenID)",
2398
+ steps: [
2399
+ "Create an application with an OAuth2/OpenID provider; its slug is the application_slug.",
2400
+ "Set the client type to Public for PKCE without a secret, or Confidential.",
2401
+ "Add the redirect URI below as a Strict redirect URI."
2402
+ ],
2403
+ build: (p) => `${hostOrigin(p.host)}/application/o/${p.application_slug}/`
2404
+ },
2405
+ "pocket-id": {
2406
+ label: "Pocket ID",
2407
+ pieces: ["host"],
2408
+ issuerFormat: "https://<host>",
2409
+ scopes: "openid profile email",
2410
+ publicPkce: true,
2411
+ usernameClaim: "preferred_username",
2412
+ whereToCreate: "Settings > Administration > OIDC Clients > Add OIDC Client",
2413
+ steps: [
2414
+ "Add an OIDC client and paste the redirect URI below as its callback URL.",
2415
+ 'Switch on "Public Client" for PKCE without a secret; leave it off for a confidential client.',
2416
+ "Copy the client ID."
2417
+ ],
2418
+ build: (p) => hostOrigin(p.host)
2419
+ },
2420
+ authelia: {
2421
+ label: "Authelia",
2422
+ pieces: ["host"],
2423
+ issuerFormat: "https://<host>",
2424
+ scopes: "openid profile email",
2425
+ publicPkce: true,
2426
+ usernameClaim: "preferred_username",
2427
+ whereToCreate: "configuration.yml > identity_providers.oidc.clients (a file edit, then a restart)",
2428
+ steps: [
2429
+ "Add a client with its redirect URI below and `authorization_policy` of your choice.",
2430
+ "For PKCE without a secret set `public: true` and `require_pkce: true`.",
2431
+ "Set `token_endpoint_auth_method: client_secret_post` for a confidential client."
2432
+ ],
2433
+ build: (p) => hostOrigin(p.host)
2434
+ },
2435
+ keycloak: {
2436
+ label: "Keycloak",
2437
+ pieces: ["host", "realm"],
2438
+ issuerFormat: "https://<host>/realms/<realm>",
2439
+ scopes: "openid profile email",
2440
+ publicPkce: true,
2441
+ usernameClaim: "preferred_username",
2442
+ whereToCreate: "Admin console > the realm > Clients > Create client",
2443
+ steps: [
2444
+ 'Client type OpenID Connect; switch "Client authentication" on for confidential, off for public.',
2445
+ 'Under Advanced, set "Proof Key for Code Exchange Code Challenge Method" to S256.',
2446
+ "Add the redirect URI below as a valid redirect URI."
2447
+ ],
2448
+ build: (p) => `${hostOrigin(p.host)}/realms/${p.realm}`
2449
+ },
2450
+ zitadel: {
2451
+ label: "ZITADEL",
2452
+ pieces: ["host"],
2453
+ issuerFormat: "https://<host>",
2454
+ scopes: "openid profile email",
2455
+ publicPkce: true,
2456
+ usernameClaim: "preferred_username",
2457
+ whereToCreate: "Console > Project > Applications > New > Web",
2458
+ steps: [
2459
+ "Choose authentication method PKCE (public) or Basic/Post (confidential).",
2460
+ "Add the redirect URI below.",
2461
+ "Copy the client ID."
2462
+ ],
2463
+ build: (p) => hostOrigin(p.host)
2464
+ },
2465
+ kanidm: {
2466
+ label: "Kanidm",
2467
+ pieces: ["host", "application_slug"],
2468
+ issuerFormat: "https://<host>/oauth2/openid/<application_slug>",
2469
+ scopes: "openid profile email",
2470
+ publicPkce: true,
2471
+ usernameClaim: "preferred_username",
2472
+ whereToCreate: "`kanidm system oauth2 create(-public) <name> <display> <origin>` on the server",
2473
+ steps: [
2474
+ "Use `create-public` for PKCE without a secret, `create` for a confidential client.",
2475
+ "Add the redirect URI below with `add-redirect-url`.",
2476
+ "Grant scopes with `update-scope-map <name> <group> openid profile email`.",
2477
+ "The application_slug is the client name you created."
2478
+ ],
2479
+ build: (p) => `${hostOrigin(p.host)}/oauth2/openid/${p.application_slug}`
2480
+ },
2481
+ google: {
2482
+ label: "Google",
2483
+ pieces: [],
2484
+ issuerFormat: "https://accounts.google.com",
2485
+ scopes: "openid profile email",
2486
+ publicPkce: false,
2487
+ usernameClaim: "email",
2488
+ whereToCreate: "Google Cloud console > APIs & Services > Credentials > Create credentials > OAuth client ID (Web application)",
2489
+ steps: [
2490
+ 'Choose "Web application"; Google issues a client secret for it, so the client is confidential.',
2491
+ "Add the redirect URI below as an authorized redirect URI (it must be a public https host)."
2492
+ ],
2493
+ build: () => "https://accounts.google.com"
2494
+ },
2495
+ "microsoft-entra": {
2496
+ label: "Microsoft Entra ID",
2497
+ pieces: ["tenant_id"],
2498
+ issuerFormat: "https://login.microsoftonline.com/<tenant_id>/v2.0",
2499
+ scopes: "openid profile email",
2500
+ publicPkce: true,
2501
+ usernameClaim: "preferred_username",
2502
+ whereToCreate: "Entra admin center > App registrations > New registration",
2503
+ steps: [
2504
+ "Register a Web app (confidential, with a client secret) or a Single-page application (public, PKCE).",
2505
+ "Add the redirect URI below under Authentication.",
2506
+ "Copy the Application (client) ID and the Directory (tenant) ID."
2507
+ ],
2508
+ build: (p) => `https://login.microsoftonline.com/${p.tenant_id}/v2.0`
2509
+ },
2510
+ auth0: {
2511
+ label: "Auth0",
2512
+ pieces: ["host"],
2513
+ issuerFormat: "https://<host>/",
2514
+ scopes: "openid profile email",
2515
+ publicPkce: true,
2516
+ usernameClaim: "email",
2517
+ whereToCreate: "Dashboard > Applications > Applications > Create Application",
2518
+ steps: [
2519
+ "Regular Web Application for confidential, Single Page Application for public (PKCE).",
2520
+ "Add the redirect URI below under Allowed Callback URLs.",
2521
+ "host is the tenant domain, for example example.eu.auth0.com."
2522
+ ],
2523
+ build: (p) => `${hostOrigin(p.host)}/`
2524
+ },
2525
+ okta: {
2526
+ label: "Okta",
2527
+ pieces: ["host"],
2528
+ issuerFormat: "https://<host>/oauth2/default",
2529
+ scopes: "openid profile email",
2530
+ publicPkce: true,
2531
+ usernameClaim: "preferred_username",
2532
+ whereToCreate: "Admin console > Applications > Applications > Create App Integration > OIDC",
2533
+ steps: [
2534
+ "Web Application for confidential, Single-Page Application for public (PKCE).",
2535
+ "Add the redirect URI below as a sign-in redirect URI.",
2536
+ "host is your Okta domain; the default authorization server is used."
2537
+ ],
2538
+ build: (p) => `${hostOrigin(p.host)}/oauth2/default`
2539
+ },
2540
+ generic: {
2541
+ label: "Any OpenID Connect provider",
2542
+ pieces: [],
2543
+ issuerFormat: "the `issuer` value of the provider's discovery document",
2544
+ scopes: "openid profile email",
2545
+ publicPkce: true,
2546
+ usernameClaim: "preferred_username",
2547
+ whereToCreate: "the provider's admin interface (create an OIDC web client)",
2548
+ steps: COMMON_STEPS,
2549
+ note: "Pass `issuer` exactly as the provider's /.well-known/openid-configuration reports it.",
2550
+ build: () => ""
2551
+ }
2552
+ };
2553
+ function resolveIssuer(provider, explicit, pieces) {
2554
+ if (explicit !== void 0 && explicit.trim() !== "") {
2555
+ return { issuer: explicit.trim(), missing: [] };
2556
+ }
2557
+ const spec = OIDC_PROVIDERS[provider];
2558
+ const missing = spec.pieces.filter((k) => (pieces[k] ?? "").trim() === "");
2559
+ if (missing.length > 0 || provider === "generic") return { issuer: null, missing };
2560
+ return {
2561
+ issuer: spec.build({
2562
+ host: pieces.host ?? "",
2563
+ application_slug: pieces.application_slug ?? "",
2564
+ realm: pieces.realm ?? "",
2565
+ tenant_id: pieces.tenant_id ?? ""
2566
+ }),
2567
+ missing: []
2568
+ };
2569
+ }
2570
+
2571
+ // src/mcp/configure-oidc.ts
2572
+ var OIDC_TOOL = "configure_oidc";
2573
+ var OIDC_ADMIN_ROUTE = "/system/identity?tab=authentication";
2574
+ var DISCOVERY_TIMEOUT_MS = 5e3;
2575
+ var ClientTypeSchema = z10.enum(["public", "confidential"]);
2576
+ var AddressesSchema3 = z10.looseObject({
2577
+ roles: z10.looseObject({
2578
+ publicEndpoint: z10.looseObject({ resolved: z10.string().nullish() }).optional(),
2579
+ appConnections: z10.looseObject({ resolved: z10.array(z10.string()) }).optional()
2580
+ })
2581
+ });
2582
+ var LOOPBACK2 = /^(localhost|127\.|\[?::1\]?$|0\.0\.0\.0)/i;
2583
+ function isLoopback(url) {
2584
+ try {
2585
+ return LOOPBACK2.test(new URL(url).hostname);
2586
+ } catch {
2587
+ return true;
2588
+ }
2589
+ }
2590
+ async function resolveHubBase(client) {
2591
+ const read = await readQuery(client, "localNetwork.getAddresses", {}, AddressesSchema3);
2592
+ if (!read.ok) return { base: null, reason: read.reason };
2593
+ const pub = read.value.roles.publicEndpoint?.resolved;
2594
+ if (typeof pub === "string" && pub !== "" && !isLoopback(pub)) {
2595
+ return { base: pub.replace(/\/+$/, ""), source: "publicEndpoint" };
2596
+ }
2597
+ const app = read.value.roles.appConnections?.resolved.find((u) => !isLoopback(u));
2598
+ if (app !== void 0) return { base: app.replace(/\/+$/, ""), source: "appConnections" };
2599
+ return {
2600
+ base: null,
2601
+ reason: "no non-loopback public endpoint or app-connections address is set (Network page)"
2602
+ };
2603
+ }
2604
+ function callbackPath(connectionId) {
2605
+ return `/addon/auth-oidc/${connectionId}/callback`;
2606
+ }
2607
+ var DiscoverySchema = z10.looseObject({
2608
+ issuer: z10.string().optional(),
2609
+ authorization_endpoint: z10.string().optional(),
2610
+ token_endpoint: z10.string().optional(),
2611
+ jwks_uri: z10.string().optional(),
2612
+ code_challenge_methods_supported: z10.array(z10.string()).optional(),
2613
+ token_endpoint_auth_methods_supported: z10.array(z10.string()).optional()
2614
+ });
2615
+ var trimSlash = (s) => s.replace(/\/+$/, "");
2616
+ async function checkDiscovery(fetchFn, issuer, clientType) {
2617
+ const url = `${trimSlash(issuer)}/.well-known/openid-configuration`;
2618
+ let body;
2619
+ try {
2620
+ const res = await fetchFn(url, { signal: AbortSignal.timeout(DISCOVERY_TIMEOUT_MS) });
2621
+ if (!res.ok) {
2622
+ return {
2623
+ checked: false,
2624
+ url,
2625
+ warnings: [`discovery answered ${res.status}: check the issuer URL`]
2626
+ };
2627
+ }
2628
+ body = await res.json();
2629
+ } catch (err) {
2630
+ return {
2631
+ checked: false,
2632
+ url,
2633
+ warnings: [`discovery could not be fetched from this machine (${reasonOf(err)})`]
2634
+ };
2635
+ }
2636
+ const parsed = DiscoverySchema.safeParse(body);
2637
+ if (!parsed.success) {
2638
+ return {
2639
+ checked: false,
2640
+ url,
2641
+ warnings: ["discovery answered something that is not a JSON document"]
2642
+ };
2643
+ }
2644
+ const doc = parsed.data;
2645
+ const warnings = [];
2646
+ const issuerMatches = doc.issuer !== void 0 && trimSlash(doc.issuer) === trimSlash(issuer);
2647
+ if (!issuerMatches) {
2648
+ warnings.push(
2649
+ `the provider reports issuer "${doc.issuer ?? "(none)"}", which differs from "${issuer}"`
2650
+ );
2651
+ }
2652
+ if (doc.authorization_endpoint === void 0 || doc.token_endpoint === void 0) {
2653
+ warnings.push("the discovery document lacks an authorization or token endpoint");
2654
+ }
2655
+ const s256 = doc.code_challenge_methods_supported?.includes("S256") === true;
2656
+ if (!s256)
2657
+ warnings.push("code_challenge_methods_supported does not include S256 (PKCE is always used)");
2658
+ const post = doc.token_endpoint_auth_methods_supported?.includes("client_secret_post") === true;
2659
+ if (clientType === "confidential" && !post) {
2660
+ warnings.push(
2661
+ "token_endpoint_auth_methods_supported does not include client_secret_post, which CamStack uses for a confidential client"
2662
+ );
2663
+ }
2664
+ return {
2665
+ checked: true,
2666
+ url,
2667
+ issuerMatches,
2668
+ ...doc.authorization_endpoint !== void 0 ? { authorizationEndpoint: doc.authorization_endpoint } : {},
2669
+ ...doc.token_endpoint !== void 0 ? { tokenEndpoint: doc.token_endpoint } : {},
2670
+ s256,
2671
+ clientSecretPost: post,
2672
+ warnings
2673
+ };
2674
+ }
2675
+ var ConnectionListSchema = z10.array(
2676
+ z10.looseObject({ id: z10.string(), displayName: z10.string().optional() })
2677
+ );
2678
+ function connectionIdOf(args) {
2679
+ return args.connection_id ?? args.provider;
2680
+ }
2681
+ function questionnaire(spec, args, redirectUri) {
2682
+ const questions = [
2683
+ ...spec.pieces.map(pieceQuestion),
2684
+ ...args.provider === "generic" ? ["issuer: the exact issuer from the discovery document"] : [],
2685
+ "client_id: the client id the provider shows after you create the client",
2686
+ `client_type: "public" (PKCE, no secret${spec.publicPkce ? "" : " - NOT supported by this provider"}) or "confidential" (a secret you type into the admin UI)`,
2687
+ "display_name (optional): the text of the login button",
2688
+ "connection_id (optional): to update an existing connection instead of creating one"
2689
+ ];
2690
+ return {
2691
+ status: "questions",
2692
+ provider: args.provider,
2693
+ label: spec.label,
2694
+ questions,
2695
+ issuerFormat: spec.issuerFormat,
2696
+ defaultScopes: spec.scopes,
2697
+ publicClientSupported: spec.publicPkce,
2698
+ whereToCreateTheClient: spec.whereToCreate,
2699
+ providerSideSteps: spec.steps,
2700
+ ...spec.note !== void 0 ? { note: spec.note } : {},
2701
+ redirectUri,
2702
+ ...redirectUri === null ? {
2703
+ redirectUriNote: "the hub address could not be resolved; set a public endpoint on the Network page"
2704
+ } : {},
2705
+ next: `Call ${OIDC_TOOL} again with the answers; it previews the connection and gives a confirm_token.`,
2706
+ secretRule: "A client secret is never a tool argument. For a confidential client you type it in the admin UI."
2707
+ };
2708
+ }
2709
+ function oidcSpec(client, fetchFn) {
2710
+ return {
2711
+ tool: OIDC_TOOL,
2712
+ async dryRun(args) {
2713
+ const spec = OIDC_PROVIDERS[args.provider];
2714
+ const clientType = args.client_type ?? "public";
2715
+ const resolved = resolveIssuer(args.provider, args.issuer, args);
2716
+ if (resolved.issuer === null || args.client_id === void 0 || args.client_id.trim() === "") {
2717
+ return { confirmable: false, describe: { problem: "issuer and client_id are required" } };
2718
+ }
2719
+ if (clientType === "public" && !spec.publicPkce) {
2720
+ return {
2721
+ confirmable: false,
2722
+ describe: {
2723
+ problem: `${spec.label} does not issue public clients; use client_type "confidential"`
2724
+ }
2725
+ };
2726
+ }
2727
+ const id = connectionIdOf(args);
2728
+ const list = await readQuery(
2729
+ client,
2730
+ "oidcConnections.listConnections",
2731
+ {},
2732
+ ConnectionListSchema
2733
+ );
2734
+ if (!list.ok) return { confirmable: false, describe: { problem: list.reason } };
2735
+ const exists = list.value.some((c) => c.id === id);
2736
+ if (exists && args.connection_id === void 0) {
2737
+ return {
2738
+ confirmable: false,
2739
+ describe: {
2740
+ problem: `a connection "${id}" already exists; pass connection_id "${id}" to update it, or another id to create a new one`
2741
+ }
2742
+ };
2743
+ }
2744
+ const base = await resolveHubBase(client);
2745
+ const discovery = await checkDiscovery(fetchFn, resolved.issuer, clientType);
2746
+ const link = await adminLink(client, OIDC_ADMIN_ROUTE);
2747
+ return {
2748
+ confirmable: true,
2749
+ describe: {
2750
+ action: exists ? "update OIDC connection" : "create OIDC connection",
2751
+ connection: {
2752
+ id,
2753
+ displayName: args.display_name ?? spec.label,
2754
+ issuerUrl: resolved.issuer,
2755
+ clientId: args.client_id.trim(),
2756
+ clientType,
2757
+ scopes: spec.scopes,
2758
+ usernameClaim: spec.usernameClaim,
2759
+ clientSecret: clientType === "public" ? "none (public PKCE client)" : "entered in the admin UI"
2760
+ },
2761
+ redirectUri: base.base !== null ? `${base.base}${callbackPath(id)}` : null,
2762
+ ...base.base === null ? { redirectUriProblem: base.reason } : {},
2763
+ discovery,
2764
+ warnings: discovery.warnings,
2765
+ ...secretStep(clientType, link.url)
2766
+ }
2767
+ };
2768
+ },
2769
+ async execute(args) {
2770
+ const spec = OIDC_PROVIDERS[args.provider];
2771
+ const clientType = args.client_type ?? "public";
2772
+ const resolved = resolveIssuer(args.provider, args.issuer, args);
2773
+ const id = connectionIdOf(args);
2774
+ const displayName = args.display_name ?? spec.label;
2775
+ const list = await readQuery(
2776
+ client,
2777
+ "oidcConnections.listConnections",
2778
+ {},
2779
+ ConnectionListSchema
2780
+ );
2781
+ const exists = list.ok && list.value.some((c) => c.id === id);
2782
+ if (!exists) {
2783
+ await client.call("POST", "oidcConnections.createConnection", { id, displayName });
2784
+ }
2785
+ await client.call("POST", "oidcConnections.updateConnection", {
2786
+ id,
2787
+ displayName,
2788
+ issuerUrl: resolved.issuer ?? "",
2789
+ clientId: (args.client_id ?? "").trim(),
2790
+ scopes: spec.scopes,
2791
+ usernameClaim: spec.usernameClaim,
2792
+ ...exists && clientType === "public" ? { clientSecret: { $secretClear: true } } : {}
2793
+ });
2794
+ const link = await adminLink(client, OIDC_ADMIN_ROUTE);
2795
+ return {
2796
+ connectionId: id,
2797
+ applied: exists ? "updated" : "created",
2798
+ ...secretStep(clientType, link.url)
2799
+ };
2800
+ }
2801
+ };
2802
+ }
2803
+ function secretStep(clientType, url) {
2804
+ if (clientType === "public") return {};
2805
+ return {
2806
+ secretStep: {
2807
+ required: true,
2808
+ 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`,
2809
+ note: "The secret is never a tool argument and is not shown here."
2810
+ }
2811
+ };
2812
+ }
2813
+ async function configureOidc(ctx, args, confirmToken) {
2814
+ const spec = OIDC_PROVIDERS[args.provider];
2815
+ const resolved = resolveIssuer(args.provider, args.issuer, args);
2816
+ const asking = resolved.issuer === null || (args.client_id ?? "").trim() === "";
2817
+ if (asking) {
2818
+ const base = await withHub(ctx, async ({ client }) => resolveHubBase(client));
2819
+ const redirect = "status" in base || base.base === null ? null : `${base.base}${callbackPath(connectionIdOf(args))}`;
2820
+ return questionnaire(spec, args, redirect);
2821
+ }
2822
+ return adminGated(ctx, (client) => oidcSpec(client, ctx.fetch), args, confirmToken);
2823
+ }
2824
+ var OIDC_TOOL_SHAPE = {
2825
+ provider: OidcProviderSchema,
2826
+ issuer: z10.string().optional(),
2827
+ host: z10.string().optional(),
2828
+ application_slug: z10.string().optional(),
2829
+ realm: z10.string().optional(),
2830
+ tenant_id: z10.string().optional(),
2831
+ client_id: z10.string().optional(),
2832
+ client_type: ClientTypeSchema.optional(),
2833
+ display_name: z10.string().optional(),
2834
+ connection_id: z10.string().optional(),
2835
+ confirm_token: ConfirmTokenSchema
2836
+ };
2837
+ function registerOidcTools(server, ctx) {
2838
+ server.tool(
2839
+ OIDC_TOOL,
2840
+ "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.",
2841
+ OIDC_TOOL_SHAPE,
2842
+ async ({ confirm_token, ...args }) => text(await configureOidc(ctx, args, confirm_token))
2843
+ );
2844
+ }
2845
+
2846
+ // src/mcp/configure-nc-rules.ts
2847
+ import { z as z11 } from "zod";
2351
2848
  var TIME = /^\d{1,2}:\d{2}$/;
2352
2849
  var ALL_DAYS = [0, 1, 2, 3, 4, 5, 6];
2353
- var QuietHoursAnswerSchema = z9.object({
2850
+ var QuietHoursAnswerSchema = z11.object({
2354
2851
  /** Notifications are muted from this time … */
2355
- from: z9.string().regex(TIME),
2852
+ from: z11.string().regex(TIME),
2356
2853
  /** … until this one (HH:MM, hub time zone). A window may cross midnight. */
2357
- to: z9.string().regex(TIME),
2854
+ to: z11.string().regex(TIME),
2358
2855
  /** 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()
2856
+ days: z11.array(z11.number().int().min(0).max(6)).min(1).optional()
2360
2857
  });
2361
- var NcRuleAnswersSchema = z9.object({
2362
- name: z9.string().min(1).max(200).optional(),
2363
- delivery: z9.enum(["immediate", "track-end"]).optional(),
2858
+ var NcRuleAnswersSchema = z11.object({
2859
+ name: z11.string().min(1).max(200).optional(),
2860
+ delivery: z11.enum(["immediate", "track-end"]).optional(),
2364
2861
  /** Camera ids; `[]` means every camera. */
2365
- cameras: z9.array(z9.number().int().nonnegative()).optional(),
2366
- classes: z9.array(z9.string().min(1)).optional(),
2862
+ cameras: z11.array(z11.number().int().nonnegative()).optional(),
2863
+ classes: z11.array(z11.string().min(1)).optional(),
2367
2864
  /** 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(),
2865
+ zones: z11.array(z11.string().min(1)).optional(),
2866
+ minConfidence: z11.number().min(0).max(1).optional(),
2370
2867
  /** `null` states "no quiet hours"; absent means not asked yet. */
2371
2868
  quietHours: QuietHoursAnswerSchema.nullable().optional(),
2372
2869
  /** Target ids or names. */
2373
- targets: z9.array(z9.string().min(1)).optional(),
2374
- cooldownSeconds: z9.number().int().min(0).max(86400).optional()
2870
+ targets: z11.array(z11.string().min(1)).optional(),
2871
+ cooldownSeconds: z11.number().int().min(0).max(86400).optional()
2375
2872
  });
2376
2873
  var SAVE_RULE_TOOL = "configure_notification_rule_save";
2377
2874
  var DELIVERY_CHOICES = [
@@ -2557,11 +3054,11 @@ async function interviewNcRule(client, answers) {
2557
3054
  async function ncRuleQuestionnaire(ctx, answers) {
2558
3055
  return withHub(ctx, ({ client }) => interviewNcRule(client, answers));
2559
3056
  }
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()
3057
+ var StoredRuleSchema = z11.looseObject({
3058
+ id: z11.string().optional(),
3059
+ name: z11.string().optional(),
3060
+ conditions: z11.record(z11.string(), z11.unknown()).optional(),
3061
+ schedule: z11.unknown().optional()
2565
3062
  });
2566
3063
  var AUTHORED_CONDITION_KEYS = [
2567
3064
  "devices",
@@ -2693,17 +3190,17 @@ function registerNcRuleTools(server, ctx) {
2693
3190
  "configure_notification_rule_questionnaire",
2694
3191
  "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
3192
  { answers: NcRuleAnswersSchema },
2696
- async ({ answers }) => text6(await ncRuleQuestionnaire(ctx, answers))
3193
+ async ({ answers }) => text(await ncRuleQuestionnaire(ctx, answers))
2697
3194
  );
2698
3195
  server.tool(
2699
3196
  SAVE_RULE_TOOL,
2700
3197
  `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
3198
  {
2702
3199
  rule: NcRuleInputSchema,
2703
- ruleId: z9.string().optional(),
3200
+ ruleId: z11.string().optional(),
2704
3201
  confirm_token: ConfirmTokenSchema
2705
3202
  },
2706
- async ({ rule, ruleId, confirm_token }) => text6(
3203
+ async ({ rule, ruleId, confirm_token }) => text(
2707
3204
  await saveRule(ctx, { rule, ...ruleId !== void 0 ? { ruleId } : {} }, confirm_token)
2708
3205
  )
2709
3206
  );
@@ -2716,20 +3213,18 @@ function registerConfigureP3bTools(server, ctx) {
2716
3213
  registerCalibrateTools(server, ctx);
2717
3214
  registerExportTools(server, ctx);
2718
3215
  registerNetworkTools(server, ctx);
3216
+ registerOidcTools(server, ctx);
2719
3217
  registerZonesAlarmTools(server, ctx);
2720
3218
  }
2721
3219
 
2722
3220
  // src/mcp/configure-addons.ts
2723
- import {
2724
- text as text7
2725
- } from "@camstack/develop";
2726
- import { z as z11 } from "zod";
3221
+ import { z as z13 } from "zod";
2727
3222
 
2728
3223
  // src/mcp/configure-support.ts
2729
3224
  import {
2730
3225
  runGated as runGated2
2731
3226
  } from "@camstack/develop";
2732
- import { z as z10 } from "zod";
3227
+ import { z as z12 } from "zod";
2733
3228
  async function runGatedAdmin(ctx, makeSpec, args, confirmToken) {
2734
3229
  const outcome = await ctx.hub();
2735
3230
  if (!outcome.ok) return noSessionResult(outcome);
@@ -2752,28 +3247,28 @@ async function withAdminHub(ctx, body) {
2752
3247
  }
2753
3248
  return body(outcome.connection);
2754
3249
  }
2755
- var FormOptionSchema = z10.looseObject({
2756
- value: z10.union([z10.string(), z10.number(), z10.boolean()]),
2757
- label: z10.string().optional()
3250
+ var FormOptionSchema = z12.looseObject({
3251
+ value: z12.union([z12.string(), z12.number(), z12.boolean()]),
3252
+ label: z12.string().optional()
2758
3253
  });
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()
3254
+ var FormFieldSchema = z12.looseObject({
3255
+ type: z12.string().optional(),
3256
+ key: z12.string().optional(),
3257
+ label: z12.string().optional(),
3258
+ description: z12.string().optional(),
3259
+ required: z12.boolean().optional(),
3260
+ secret: z12.boolean().optional(),
3261
+ default: z12.unknown().optional(),
3262
+ options: z12.array(FormOptionSchema).optional(),
3263
+ itemFields: z12.array(
3264
+ z12.looseObject({
3265
+ type: z12.string().optional(),
3266
+ key: z12.string().optional()
2772
3267
  })
2773
3268
  ).optional()
2774
3269
  });
2775
- var FormSchemaShape = z10.looseObject({
2776
- sections: z10.array(z10.looseObject({ fields: z10.array(FormFieldSchema) }))
3270
+ var FormSchemaShape = z12.looseObject({
3271
+ sections: z12.array(z12.looseObject({ fields: z12.array(FormFieldSchema) }))
2777
3272
  });
2778
3273
  var NON_INPUT_TYPES = /* @__PURE__ */ new Set(["info", "divider", "heading", "button"]);
2779
3274
  var CREDENTIAL_NAME = /pass(word|wd|phrase)?|token|secret|api[_-]?key|credential|private[_-]?key/i;
@@ -2874,21 +3369,21 @@ function splitConfig(fields, config) {
2874
3369
  function fieldValidator(field) {
2875
3370
  switch (field.type) {
2876
3371
  case "number":
2877
- return z10.number();
3372
+ return z12.number();
2878
3373
  case "boolean":
2879
3374
  case "switch":
2880
3375
  case "checkbox":
2881
3376
  case "toggle":
2882
- return z10.boolean();
3377
+ return z12.boolean();
2883
3378
  case "tags":
2884
- return z10.array(z10.string());
3379
+ return z12.array(z12.string());
2885
3380
  case "editable-array":
2886
- return z10.array(z10.record(z10.string(), z10.unknown()));
3381
+ return z12.array(z12.record(z12.string(), z12.unknown()));
2887
3382
  case "select":
2888
3383
  case "radio": {
2889
- if (field.options === null || field.options.length === 0) return z10.unknown();
3384
+ if (field.options === null || field.options.length === 0) return z12.unknown();
2890
3385
  const values = field.options.map((o) => o.value);
2891
- return z10.custom(
3386
+ return z12.custom(
2892
3387
  (v) => values.some((allowed) => allowed === v),
2893
3388
  `one of: ${values.map(String).join(", ")}`
2894
3389
  );
@@ -2898,9 +3393,9 @@ function fieldValidator(field) {
2898
3393
  case "textarea":
2899
3394
  case "url":
2900
3395
  case "email":
2901
- return z10.string();
3396
+ return z12.string();
2902
3397
  default:
2903
- return z10.unknown();
3398
+ return z12.unknown();
2904
3399
  }
2905
3400
  }
2906
3401
  function formValidator(fields) {
@@ -2910,7 +3405,7 @@ function formValidator(fields) {
2910
3405
  const base = fieldValidator(field);
2911
3406
  shape[field.key] = field.required ? base : base.optional();
2912
3407
  }
2913
- return z10.object(shape);
3408
+ return z12.object(shape);
2914
3409
  }
2915
3410
  function validationProblems(error) {
2916
3411
  return error.issues.map((issue) => {
@@ -2942,64 +3437,64 @@ function viewConfig(fields, config) {
2942
3437
  var REGISTRY = "https://registry.npmjs.org";
2943
3438
  var REGISTRY_TIMEOUT_MS = 5e3;
2944
3439
  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()
3440
+ var PackageName = z13.string().min(1).regex(/^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/, "not an npm package name");
3441
+ var CatalogShape = z13.looseObject({
3442
+ name: z13.string(),
3443
+ version: z13.string(),
3444
+ description: z13.string(),
3445
+ keywords: z13.array(z13.string()).optional(),
3446
+ installed: z13.boolean(),
3447
+ capabilities: z13.array(z13.string()).nullable()
2953
3448
  });
2954
- var ListedAddonShape = z11.looseObject({
2955
- manifest: z11.looseObject({
2956
- id: z11.string(),
2957
- packageName: z11.string(),
2958
- packageVersion: z11.string(),
3449
+ var ListedAddonShape = z13.looseObject({
3450
+ manifest: z13.looseObject({
3451
+ id: z13.string(),
3452
+ packageName: z13.string(),
3453
+ packageVersion: z13.string(),
2959
3454
  // The hub answers null, not absent, for a manifest without the flag.
2960
- protected: z11.boolean().nullish(),
2961
- removable: z11.boolean().nullish()
3455
+ protected: z13.boolean().nullish(),
3456
+ removable: z13.boolean().nullish()
2962
3457
  })
2963
3458
  });
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()
3459
+ var UpdateShape = z13.looseObject({
3460
+ name: z13.string(),
3461
+ currentVersion: z13.string(),
3462
+ latestVersion: z13.string(),
3463
+ category: z13.enum(["addon", "core"]),
3464
+ requiresRestart: z13.boolean(),
3465
+ isSystem: z13.boolean()
2971
3466
  });
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()
3467
+ var InstallationsShape = z13.looseObject({
3468
+ installations: z13.array(
3469
+ z13.looseObject({
3470
+ nodeId: z13.string(),
3471
+ isHub: z13.boolean(),
3472
+ isOnline: z13.boolean(),
3473
+ version: z13.string().nullable(),
3474
+ source: z13.string()
2980
3475
  })
2981
3476
  ),
2982
- unreachable: z11.array(z11.looseObject({ nodeId: z11.string(), error: z11.string() }))
3477
+ unreachable: z13.array(z13.looseObject({ nodeId: z13.string(), error: z13.string() }))
2983
3478
  });
2984
- var VersionsShape = z11.array(
2985
- z11.looseObject({ version: z11.string(), deprecated: z11.string().optional() })
3479
+ var VersionsShape = z13.array(
3480
+ z13.looseObject({ version: z13.string(), deprecated: z13.string().optional() })
2986
3481
  );
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()
3482
+ var UninstallShape = z13.looseObject({
3483
+ success: z13.boolean(),
3484
+ packageName: z13.string(),
3485
+ results: z13.array(
3486
+ z13.looseObject({
3487
+ nodeId: z13.string(),
3488
+ outcome: z13.string(),
3489
+ error: z13.string().optional()
2995
3490
  })
2996
3491
  )
2997
3492
  });
2998
- var ServerStatusShape = z11.looseObject({
2999
- runningVersion: z11.string().nullable().optional()
3493
+ var ServerStatusShape = z13.looseObject({
3494
+ runningVersion: z13.string().nullable().optional()
3000
3495
  });
3001
3496
  async function readProtection(client, packageName) {
3002
- const list = await readQuery(client, "addons.list", {}, z11.array(ListedAddonShape));
3497
+ const list = await readQuery(client, "addons.list", {}, z13.array(ListedAddonShape));
3003
3498
  if (!list.ok) return null;
3004
3499
  const mine = list.value.filter((a) => a.manifest.packageName === packageName);
3005
3500
  return {
@@ -3010,13 +3505,13 @@ async function readProtection(client, packageName) {
3010
3505
  }
3011
3506
  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
3507
  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()
3508
+ var PackumentShape = z13.looseObject({
3509
+ peerDependencies: z13.record(z13.string(), z13.string()).optional(),
3510
+ engines: z13.record(z13.string(), z13.string()).optional(),
3511
+ camstack: z13.looseObject({
3512
+ addons: z13.array(
3513
+ z13.looseObject({
3514
+ execution: z13.looseObject({ placement: z13.string().optional() }).optional()
3020
3515
  })
3021
3516
  ).optional()
3022
3517
  }).optional()
@@ -3068,10 +3563,10 @@ async function configureAddonsSearch(ctx, args) {
3068
3563
  ...args.query !== void 0 ? { query: args.query } : {},
3069
3564
  ...args.capability !== void 0 ? { capability: args.capability } : {}
3070
3565
  },
3071
- z11.array(CatalogShape)
3566
+ z13.array(CatalogShape)
3072
3567
  ),
3073
- readQuery(client, "addons.list", {}, z11.array(ListedAddonShape)),
3074
- readQuery(client, "addons.listUpdates", {}, z11.array(UpdateShape)),
3568
+ readQuery(client, "addons.list", {}, z13.array(ListedAddonShape)),
3569
+ readQuery(client, "addons.listUpdates", {}, z13.array(UpdateShape)),
3075
3570
  readQuery(client, "serverManagement.getServerPackageStatus", void 0, ServerStatusShape)
3076
3571
  ]);
3077
3572
  if (!catalog.ok) return { status: "unavailable", reason: catalog.reason };
@@ -3150,7 +3645,7 @@ function installAddonSpec(client, fetchFn) {
3150
3645
  client,
3151
3646
  "addons.searchAvailable",
3152
3647
  { query: args.packageName },
3153
- z11.array(CatalogShape)
3648
+ z13.array(CatalogShape)
3154
3649
  );
3155
3650
  if (!catalog.ok) return { confirmable: false, describe: { problem: catalog.reason } };
3156
3651
  const entry = catalog.value.find((c) => c.name === args.packageName);
@@ -3253,7 +3748,7 @@ function updateAddonSpec(client) {
3253
3748
  client,
3254
3749
  "addons.listUpdates",
3255
3750
  args.nodeId !== void 0 ? { nodeId: args.nodeId } : {},
3256
- z11.array(UpdateShape)
3751
+ z13.array(UpdateShape)
3257
3752
  );
3258
3753
  if (!updates.ok) return { confirmable: false, describe: { problem: updates.reason } };
3259
3754
  const entry = updates.value.find((u) => u.name === args.packageName);
@@ -3374,10 +3869,10 @@ function registerAddonTools(server, ctx) {
3374
3869
  "configure_addons_search",
3375
3870
  "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
3871
  {
3377
- query: z11.string().max(100).optional(),
3378
- capability: z11.string().max(100).optional()
3872
+ query: z13.string().max(100).optional(),
3873
+ capability: z13.string().max(100).optional()
3379
3874
  },
3380
- async ({ query, capability }) => text7(
3875
+ async ({ query, capability }) => text(
3381
3876
  await configureAddonsSearch(ctx, {
3382
3877
  ...query !== void 0 ? { query } : {},
3383
3878
  ...capability !== void 0 ? { capability } : {}
@@ -3389,10 +3884,10 @@ function registerAddonTools(server, ctx) {
3389
3884
  `Install an addon package on the hub from the store. ${TWO_STEP} Agents reconcile later; the result says so.`,
3390
3885
  {
3391
3886
  packageName: PackageName,
3392
- version: z11.string().max(64).optional(),
3393
- confirm_token: z11.string().optional()
3887
+ version: z13.string().max(64).optional(),
3888
+ confirm_token: z13.string().optional()
3394
3889
  },
3395
- async ({ packageName, version, confirm_token }) => text7(
3890
+ async ({ packageName, version, confirm_token }) => text(
3396
3891
  await installAddon(
3397
3892
  ctx,
3398
3893
  { packageName, ...version !== void 0 ? { version } : {} },
@@ -3405,11 +3900,11 @@ function registerAddonTools(server, ctx) {
3405
3900
  `Update an installed addon package (hub by default, or one node). Protected and framework packages are refused in the preview. ${TWO_STEP}`,
3406
3901
  {
3407
3902
  packageName: PackageName,
3408
- version: z11.string().max(64).optional(),
3409
- nodeId: z11.string().max(100).optional(),
3410
- confirm_token: z11.string().optional()
3903
+ version: z13.string().max(64).optional(),
3904
+ nodeId: z13.string().max(100).optional(),
3905
+ confirm_token: z13.string().optional()
3411
3906
  },
3412
- async ({ packageName, version, nodeId, confirm_token }) => text7(
3907
+ async ({ packageName, version, nodeId, confirm_token }) => text(
3413
3908
  await updateAddon(
3414
3909
  ctx,
3415
3910
  {
@@ -3426,11 +3921,11 @@ function registerAddonTools(server, ctx) {
3426
3921
  `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
3922
  {
3428
3923
  packageName: PackageName,
3429
- nodeId: z11.string().max(100).optional(),
3430
- everywhere: z11.boolean().optional(),
3431
- confirm_token: z11.string().optional()
3924
+ nodeId: z13.string().max(100).optional(),
3925
+ everywhere: z13.boolean().optional(),
3926
+ confirm_token: z13.string().optional()
3432
3927
  },
3433
- async ({ packageName, nodeId, everywhere, confirm_token }) => text7(
3928
+ async ({ packageName, nodeId, everywhere, confirm_token }) => text(
3434
3929
  await uninstallAddon(
3435
3930
  ctx,
3436
3931
  {
@@ -3445,10 +3940,7 @@ function registerAddonTools(server, ctx) {
3445
3940
  }
3446
3941
 
3447
3942
  // src/mcp/configure-cameras.ts
3448
- import {
3449
- text as text8
3450
- } from "@camstack/develop";
3451
- import { z as z12 } from "zod";
3943
+ import { z as z14 } from "zod";
3452
3944
  var UNSUPPORTED_HINT = "use support_diagnose_camera / support_draft_issue";
3453
3945
  var MAX_PROVIDERS = 40;
3454
3946
  var MAX_CANDIDATES = 50;
@@ -3461,34 +3953,34 @@ function unsupported(reason, provider) {
3461
3953
  hint: UNSUPPORTED_HINT
3462
3954
  };
3463
3955
  }
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()
3956
+ var CapabilityEntry = z14.union([z14.string(), z14.looseObject({ name: z14.string() })]);
3957
+ var InstalledAddonShape = z14.looseObject({
3958
+ manifest: z14.looseObject({
3959
+ id: z14.string(),
3960
+ name: z14.string().optional(),
3961
+ description: z14.string().optional(),
3962
+ packageName: z14.string(),
3963
+ packageVersion: z14.string(),
3964
+ capabilities: z14.array(CapabilityEntry).optional()
3473
3965
  })
3474
3966
  });
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()
3967
+ var CatalogShape2 = z14.looseObject({
3968
+ name: z14.string(),
3969
+ version: z14.string(),
3970
+ description: z14.string(),
3971
+ keywords: z14.array(z14.string()),
3972
+ installed: z14.boolean(),
3973
+ capabilities: z14.array(z14.string()).nullable()
3482
3974
  });
3483
- var DiscoveryProvidersShape = z12.looseObject({
3484
- providers: z12.array(z12.looseObject({ addonId: z12.string(), label: z12.string() }))
3975
+ var DiscoveryProvidersShape = z14.looseObject({
3976
+ providers: z14.array(z14.looseObject({ addonId: z14.string(), label: z14.string() }))
3485
3977
  });
3486
3978
  function capabilityName(entry) {
3487
3979
  return typeof entry === "string" ? entry : entry.name;
3488
3980
  }
3489
3981
  async function readInstalledProviders(client) {
3490
3982
  const [list, discovery] = await Promise.all([
3491
- readQuery(client, "addons.list", {}, z12.array(InstalledAddonShape)),
3983
+ readQuery(client, "addons.list", {}, z14.array(InstalledAddonShape)),
3492
3984
  readQuery(client, "deviceManager.discoveryProviders", {}, DiscoveryProvidersShape)
3493
3985
  ]);
3494
3986
  if (!list.ok) return list;
@@ -3519,7 +4011,7 @@ async function configureCameraProviders(ctx, query) {
3519
4011
  client,
3520
4012
  "addons.searchAvailable",
3521
4013
  { capability: DEVICE_PROVIDER },
3522
- z12.array(CatalogShape2)
4014
+ z14.array(CatalogShape2)
3523
4015
  )
3524
4016
  ]);
3525
4017
  if (!installed.ok) return { status: "unavailable", reason: installed.reason };
@@ -3553,13 +4045,13 @@ async function configureCameraProviders(ctx, query) {
3553
4045
  };
3554
4046
  });
3555
4047
  }
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())
4048
+ var CandidateShape = z14.looseObject({
4049
+ stableId: z14.string(),
4050
+ type: z14.string(),
4051
+ suggestedName: z14.string(),
4052
+ prefilledConfig: z14.record(z14.string(), z14.unknown())
3561
4053
  });
3562
- var DiscoverShape = z12.looseObject({ candidates: z12.array(CandidateShape) });
4054
+ var DiscoverShape = z14.looseObject({ candidates: z14.array(CandidateShape) });
3563
4055
  async function configureCameraDiscover(ctx, args) {
3564
4056
  return withAdminHub(ctx, async ({ client }) => {
3565
4057
  const providers = await readQuery(
@@ -3645,7 +4137,7 @@ async function planAdd(client, args) {
3645
4137
  client,
3646
4138
  "deviceManager.getCreationSchema",
3647
4139
  { addonId: args.addonId, type: "camera" },
3648
- z12.unknown()
4140
+ z14.unknown()
3649
4141
  );
3650
4142
  if (!schema.ok) return { ok: false, problems: [schema.reason] };
3651
4143
  if (schema.value === null || schema.value === void 0) {
@@ -3696,7 +4188,7 @@ async function providerPage(client, addonId) {
3696
4188
  const link = await resolveLinkBase(client);
3697
4189
  return link.base === null ? null : `${link.base}/integrations/${addonId}`;
3698
4190
  }
3699
- var CreatedShape = z12.looseObject({ id: z12.number(), name: z12.string() });
4191
+ var CreatedShape = z14.looseObject({ id: z14.number(), name: z14.string() });
3700
4192
  function addCameraSpec(client) {
3701
4193
  return {
3702
4194
  tool: ADD_CAMERA_TOOL,
@@ -3773,17 +4265,17 @@ function registerCameraTools(server, ctx) {
3773
4265
  server.tool(
3774
4266
  "configure_camera_providers",
3775
4267
  "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))
4268
+ { query: z14.string().max(100).optional() },
4269
+ async ({ query }) => text(await configureCameraProviders(ctx, query))
3778
4270
  );
3779
4271
  server.tool(
3780
4272
  "configure_camera_discover",
3781
4273
  '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
4274
  {
3783
- addonId: z12.string().min(1),
3784
- params: z12.record(z12.string(), z12.unknown()).optional()
4275
+ addonId: z14.string().min(1),
4276
+ params: z14.record(z14.string(), z14.unknown()).optional()
3785
4277
  },
3786
- async ({ addonId, params }) => text8(
4278
+ async ({ addonId, params }) => text(
3787
4279
  await configureCameraDiscover(ctx, {
3788
4280
  addonId,
3789
4281
  ...params !== void 0 ? { params } : {}
@@ -3794,46 +4286,43 @@ function registerCameraTools(server, ctx) {
3794
4286
  ADD_CAMERA_TOOL,
3795
4287
  "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
4288
  {
3797
- addonId: z12.string().min(1),
3798
- config: z12.record(z12.string(), z12.unknown()),
3799
- confirm_token: z12.string().optional()
4289
+ addonId: z14.string().min(1),
4290
+ config: z14.record(z14.string(), z14.unknown()),
4291
+ confirm_token: z14.string().optional()
3800
4292
  },
3801
- async ({ addonId, config, confirm_token }) => text8(await addCamera(ctx, { addonId, config }, confirm_token))
4293
+ async ({ addonId, config, confirm_token }) => text(await addCamera(ctx, { addonId, config }, confirm_token))
3802
4294
  );
3803
4295
  }
3804
4296
 
3805
4297
  // src/mcp/configure-notifiers.ts
3806
- import {
3807
- text as text9
3808
- } from "@camstack/develop";
3809
- import { z as z13 } from "zod";
4298
+ import { z as z15 } from "zod";
3810
4299
  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()
4300
+ var TargetKindShape = z15.looseObject({
4301
+ kind: z15.string(),
4302
+ label: z15.string(),
4303
+ addonId: z15.string(),
4304
+ supportsDiscovery: z15.boolean().optional(),
4305
+ configSchema: z15.unknown(),
4306
+ destinationKeys: z15.array(z15.string()).optional()
3818
4307
  });
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())
4308
+ var TargetShape = z15.looseObject({
4309
+ id: z15.string(),
4310
+ name: z15.string(),
4311
+ kind: z15.string(),
4312
+ addonId: z15.string(),
4313
+ enabled: z15.boolean(),
4314
+ config: z15.record(z15.string(), z15.unknown())
3826
4315
  });
3827
- var SendResultShape = z13.looseObject({
3828
- success: z13.boolean(),
3829
- error: z13.string().optional()
4316
+ var SendResultShape = z15.looseObject({
4317
+ success: z15.boolean(),
4318
+ error: z15.string().optional()
3830
4319
  });
3831
4320
  async function readKinds(client) {
3832
4321
  const read = await readQuery(
3833
4322
  client,
3834
4323
  "notificationOutput.listTargetKinds",
3835
4324
  {},
3836
- z13.array(TargetKindShape)
4325
+ z15.array(TargetKindShape)
3837
4326
  );
3838
4327
  if (!read.ok) return read;
3839
4328
  return {
@@ -3888,7 +4377,7 @@ async function configureNotificationTargets(ctx) {
3888
4377
  return withAdminHub(ctx, async ({ client }) => {
3889
4378
  const [kinds, targets] = await Promise.all([
3890
4379
  readKinds(client),
3891
- readQuery(client, "notificationOutput.listTargets", {}, z13.array(TargetShape))
4380
+ readQuery(client, "notificationOutput.listTargets", {}, z15.array(TargetShape))
3892
4381
  ]);
3893
4382
  if (!targets.ok) return { status: "unavailable", reason: targets.reason };
3894
4383
  const forms = kinds.ok ? kinds.value : [];
@@ -3929,7 +4418,7 @@ async function planUpsert(client, args) {
3929
4418
  const fields = form.fields;
3930
4419
  let existing = null;
3931
4420
  if (args.targetId !== void 0) {
3932
- const list = await readQuery(client, "notificationOutput.listTargets", {}, z13.array(TargetShape));
4421
+ const list = await readQuery(client, "notificationOutput.listTargets", {}, z15.array(TargetShape));
3933
4422
  if (!list.ok) return { ok: false, problems: [list.reason] };
3934
4423
  existing = list.value.find((t) => t.id === args.targetId) ?? null;
3935
4424
  if (existing === null) return { ok: false, problems: [`target ${args.targetId} was not found`] };
@@ -4036,7 +4525,7 @@ function testTargetSpec(client) {
4036
4525
  async dryRun(args) {
4037
4526
  const [kinds, targets] = await Promise.all([
4038
4527
  readKinds(client),
4039
- readQuery(client, "notificationOutput.listTargets", {}, z13.array(TargetShape))
4528
+ readQuery(client, "notificationOutput.listTargets", {}, z15.array(TargetShape))
4040
4529
  ]);
4041
4530
  if (!targets.ok) return { confirmable: false, describe: { problem: targets.reason } };
4042
4531
  const target = targets.value.find((t) => t.id === args.targetId);
@@ -4108,26 +4597,26 @@ function registerNotifierTools(server, ctx) {
4108
4597
  "configure_notifiers",
4109
4598
  "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
4599
  {},
4111
- async () => text9(await configureNotifiers(ctx))
4600
+ async () => text(await configureNotifiers(ctx))
4112
4601
  );
4113
4602
  server.tool(
4114
4603
  "configure_notification_targets",
4115
4604
  "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
4605
  {},
4117
- async () => text9(await configureNotificationTargets(ctx))
4606
+ async () => text(await configureNotificationTargets(ctx))
4118
4607
  );
4119
4608
  server.tool(
4120
4609
  UPSERT_TARGET_TOOL,
4121
4610
  "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
4611
  {
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()
4612
+ targetId: z15.string().min(1).optional(),
4613
+ kind: z15.string().min(1),
4614
+ name: z15.string().min(1),
4615
+ config: z15.record(z15.string(), z15.unknown()),
4616
+ enabled: z15.boolean().optional(),
4617
+ confirm_token: z15.string().optional()
4129
4618
  },
4130
- async ({ targetId, kind, name, config, enabled, confirm_token }) => text9(
4619
+ async ({ targetId, kind, name, config, enabled, confirm_token }) => text(
4131
4620
  await upsertNotificationTarget(
4132
4621
  ctx,
4133
4622
  {
@@ -4144,51 +4633,51 @@ function registerNotifierTools(server, ctx) {
4144
4633
  server.tool(
4145
4634
  TEST_TARGET_TOOL,
4146
4635
  "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))
4636
+ { targetId: z15.string().min(1), confirm_token: z15.string().optional() },
4637
+ async ({ targetId, confirm_token }) => text(await testNotificationTarget(ctx, { targetId }, confirm_token))
4149
4638
  );
4150
4639
  }
4151
4640
 
4152
4641
  // src/mcp/ops-tools.ts
4153
- import { z as z25 } from "zod";
4154
- import { jwtClaims, text as text11 } from "@camstack/develop";
4642
+ import { z as z23 } from "zod";
4643
+ import { jwtClaims } from "@camstack/develop";
4155
4644
 
4156
4645
  // 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()
4646
+ import { z as z16 } from "zod";
4647
+ var NodesSchema = z16.array(
4648
+ z16.looseObject({
4649
+ id: z16.string(),
4650
+ isOnline: z16.boolean(),
4651
+ name: z16.string().optional()
4163
4652
  })
4164
4653
  );
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()
4654
+ var HardwareSchema = z16.looseObject({
4655
+ platform: z16.enum(["darwin", "linux", "win32"]),
4656
+ arch: z16.enum(["arm64", "x64"]),
4657
+ cpuModel: z16.string(),
4658
+ cpuCores: z16.number(),
4659
+ totalRAM_MB: z16.number(),
4660
+ availableRAM_MB: z16.number(),
4661
+ gpu: z16.looseObject({
4662
+ type: z16.enum(["nvidia", "amd", "intel", "apple"]),
4663
+ name: z16.string()
4175
4664
  }).nullable(),
4176
- npu: z14.looseObject({
4177
- type: z14.enum(["apple-ane", "intel-npu", "amd-xdna"])
4665
+ npu: z16.looseObject({
4666
+ type: z16.enum(["apple-ane", "intel-npu", "amd-xdna"])
4178
4667
  }).nullable()
4179
4668
  });
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()
4669
+ var InferenceDevicesSchema = z16.looseObject({
4670
+ reachable: z16.boolean(),
4671
+ devices: z16.array(
4672
+ z16.looseObject({
4673
+ key: z16.string(),
4674
+ backend: z16.string(),
4675
+ device: z16.string(),
4676
+ available: z16.boolean(),
4677
+ presence: z16.enum(["present", "absent", "unknown"]).optional(),
4678
+ enabled: z16.boolean(),
4679
+ exclusion: z16.string().nullable(),
4680
+ defaultModelId: z16.string()
4192
4681
  })
4193
4682
  )
4194
4683
  });
@@ -4312,81 +4801,81 @@ async function opsAccelerators(ctx) {
4312
4801
  }
4313
4802
 
4314
4803
  // 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()
4804
+ import { z as z17 } from "zod";
4805
+ var DeviceSchema = z17.looseObject({
4806
+ id: z17.number(),
4807
+ name: z17.string(),
4808
+ type: z17.string().optional(),
4809
+ addonId: z17.string().optional(),
4810
+ location: z17.string().nullable().optional(),
4811
+ disabled: z17.boolean().optional(),
4812
+ online: z17.boolean().optional(),
4813
+ isCamera: z17.boolean().optional(),
4814
+ features: z17.array(z17.string()).optional()
4326
4815
  });
4327
4816
  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()
4817
+ var DeviceListSchema = z17.array(DeviceSchema);
4818
+ var BindingsSchema2 = z17.looseObject({
4819
+ entries: z17.array(
4820
+ z17.looseObject({
4821
+ capName: z17.string(),
4822
+ kind: z17.string(),
4823
+ providerAddonId: z17.string(),
4824
+ providerNodeId: z17.string()
4336
4825
  })
4337
4826
  )
4338
4827
  });
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()
4828
+ var StatusAggregateSchema = z17.record(z17.string(), z17.unknown());
4829
+ var DegradationSchema = z17.looseObject({
4830
+ stage: z17.string(),
4831
+ reason: z17.string(),
4832
+ elapsedMs: z17.number().optional()
4344
4833
  });
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(),
4834
+ var CameraStatusSchema = z17.looseObject({
4835
+ deviceId: z17.number(),
4836
+ assignment: z17.looseObject({
4837
+ detectionNodeId: z17.string().nullable(),
4838
+ decoderNodeId: z17.string().nullable(),
4839
+ sourceNodeId: z17.string().nullable(),
4351
4840
  /** Why a stage sits where it does; `armed:on-motion` means idle by design. */
4352
- reasons: z15.record(z15.string(), z15.string().optional()).optional()
4841
+ reasons: z17.record(z17.string(), z17.string().optional()).optional()
4353
4842
  }),
4354
- broker: z15.looseObject({
4355
- profiles: z15.array(
4356
- z15.looseObject({
4357
- profile: z15.string(),
4358
- status: z15.string(),
4359
- subscribers: z15.number()
4843
+ broker: z17.looseObject({
4844
+ profiles: z17.array(
4845
+ z17.looseObject({
4846
+ profile: z17.string(),
4847
+ status: z17.string(),
4848
+ subscribers: z17.number()
4360
4849
  })
4361
4850
  )
4362
4851
  }).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()
4852
+ detection: z17.looseObject({
4853
+ nodeId: z17.string(),
4854
+ phase: z17.string(),
4855
+ actualFps: z17.number(),
4856
+ configuredFps: z17.number(),
4857
+ provisioning: z17.looseObject({
4858
+ state: z17.string(),
4859
+ error: z17.string().optional()
4371
4860
  })
4372
4861
  }).nullable(),
4373
- recording: z15.looseObject({
4374
- mode: z15.string(),
4375
- active: z15.boolean(),
4376
- storageBytes: z15.number()
4862
+ recording: z17.looseObject({
4863
+ mode: z17.string(),
4864
+ active: z17.boolean(),
4865
+ storageBytes: z17.number()
4377
4866
  }).nullable(),
4378
- switchedOff: z15.array(z15.string()),
4379
- degraded: z15.array(DegradationSchema),
4380
- fetchedAt: z15.number()
4867
+ switchedOff: z17.array(z17.string()),
4868
+ degraded: z17.array(DegradationSchema),
4869
+ fetchedAt: z17.number()
4381
4870
  });
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()
4871
+ var CameraStatusListSchema = z17.array(CameraStatusSchema);
4872
+ var ObjectEventSchema = z17.looseObject({
4873
+ id: z17.number(),
4874
+ timestamp: z17.number(),
4875
+ className: z17.string().optional(),
4876
+ trackId: z17.string().optional()
4388
4877
  });
4389
- var ObjectEventsSchema2 = z15.array(ObjectEventSchema);
4878
+ var ObjectEventsSchema2 = z17.array(ObjectEventSchema);
4390
4879
  var BINDING_LIMIT = 40;
4391
4880
  var EVENT_LIMIT = 5;
4392
4881
  var UNHEALTHY_LIMIT = 50;
@@ -4394,9 +4883,9 @@ function freshness(aggregate, now) {
4394
4883
  const fetched = [];
4395
4884
  let changed = null;
4396
4885
  for (const [capName, value] of Object.entries(aggregate)) {
4397
- const parsed = z15.looseObject({
4398
- lastFetchedAt: z15.number().optional(),
4399
- lastChangedAt: z15.number().optional()
4886
+ const parsed = z17.looseObject({
4887
+ lastFetchedAt: z17.number().optional(),
4888
+ lastChangedAt: z17.number().optional()
4400
4889
  }).safeParse(value);
4401
4890
  if (!parsed.success) continue;
4402
4891
  const at = parsed.data.lastFetchedAt;
@@ -4647,26 +5136,26 @@ async function opsUnhealthyDevices(ctx) {
4647
5136
  }
4648
5137
 
4649
5138
  // 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()
5139
+ import { z as z18 } from "zod";
5140
+ var LevelSchema = z18.enum(["debug", "info", "warn", "error"]);
5141
+ var LogEntrySchema = z18.looseObject({
5142
+ timestamp: z18.string(),
5143
+ level: z18.string(),
5144
+ scope: z18.string().optional(),
5145
+ message: z18.string(),
5146
+ tags: z18.record(z18.string(), z18.string().optional()).optional(),
5147
+ meta: z18.record(z18.string(), z18.unknown()).optional()
4659
5148
  });
4660
- var LogPageSchema = z16.looseObject({
4661
- entries: z16.array(LogEntrySchema),
4662
- done: z16.boolean(),
4663
- source: z16.string()
5149
+ var LogPageSchema = z18.looseObject({
5150
+ entries: z18.array(LogEntrySchema),
5151
+ done: z18.boolean(),
5152
+ source: z18.string()
4664
5153
  });
4665
- var LogSourcesSchema = z16.array(
4666
- z16.looseObject({
4667
- addonId: z16.string(),
4668
- readable: z16.boolean(),
4669
- isDefault: z16.boolean()
5154
+ var LogSourcesSchema = z18.array(
5155
+ z18.looseObject({
5156
+ addonId: z18.string(),
5157
+ readable: z18.boolean(),
5158
+ isDefault: z18.boolean()
4670
5159
  })
4671
5160
  );
4672
5161
  var RING_SOURCE = "ring";
@@ -4881,37 +5370,37 @@ async function opsLogs(ctx, args, now = Date.now()) {
4881
5370
  }
4882
5371
 
4883
5372
  // 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(),
5373
+ import { z as z19 } from "zod";
5374
+ var TopologyNodeSchema = z19.looseObject({
5375
+ id: z19.string(),
5376
+ name: z19.string().optional(),
5377
+ platform: z19.string().optional(),
5378
+ isHub: z19.boolean().optional(),
5379
+ isOnline: z19.boolean(),
5380
+ addons: z19.array(z19.looseObject({ capabilities: z19.array(z19.string()) })).optional(),
4892
5381
  /** Agents report it; the hub does not. Absent means "not claimed". */
4893
- capabilitiesRegistered: z17.boolean().optional()
5382
+ capabilitiesRegistered: z19.boolean().optional()
4894
5383
  });
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()
5384
+ var TopologySchema = z19.array(TopologyNodeSchema);
5385
+ var ServerStatusSchema = z19.looseObject({
5386
+ runningVersion: z19.string(),
5387
+ restartRequired: z19.boolean().optional(),
5388
+ updateAvailable: z19.boolean().optional(),
5389
+ updateState: z19.string().optional()
4901
5390
  });
4902
- var AlertSchema = z17.looseObject({
4903
- severity: z17.string(),
4904
- title: z17.string(),
4905
- message: z17.string().optional(),
4906
- status: z17.string()
5391
+ var AlertSchema = z19.looseObject({
5392
+ severity: z19.string(),
5393
+ title: z19.string(),
5394
+ message: z19.string().optional(),
5395
+ status: z19.string()
4907
5396
  });
4908
- var AlertsSchema = z17.array(AlertSchema);
4909
- var AddonUpdateSchema = z17.looseObject({
4910
- name: z17.string(),
4911
- currentVersion: z17.string(),
4912
- latestVersion: z17.string()
5397
+ var AlertsSchema = z19.array(AlertSchema);
5398
+ var AddonUpdateSchema = z19.looseObject({
5399
+ name: z19.string(),
5400
+ currentVersion: z19.string(),
5401
+ latestVersion: z19.string()
4913
5402
  });
4914
- var AddonUpdatesSchema = z17.array(AddonUpdateSchema);
5403
+ var AddonUpdatesSchema = z19.array(AddonUpdateSchema);
4915
5404
  var NODE_LIMIT2 = 40;
4916
5405
  var ALERT_LIMIT = 10;
4917
5406
  var UPDATE_LIMIT = 20;
@@ -5044,65 +5533,65 @@ async function opsStatus(ctx) {
5044
5533
  import {
5045
5534
  runGated as runGated3
5046
5535
  } from "@camstack/develop";
5047
- import { z as z23 } from "zod";
5536
+ import { z as z21 } from "zod";
5048
5537
 
5049
5538
  // src/mcp/ops-update-model.ts
5050
- import { z as z18 } from "zod";
5051
- var ImageContractShape = z18.looseObject({
5052
- state: z18.enum(["in-sync", "behind-patch", "behind-series", "ahead", "unknown"]),
5053
- seedVersion: z18.string().nullable(),
5054
- contractVersion: z18.string().nullable(),
5055
- message: z18.string().optional()
5539
+ import { z as z20 } from "zod";
5540
+ var ImageContractShape = z20.looseObject({
5541
+ state: z20.enum(["in-sync", "behind-patch", "behind-series", "ahead", "unknown"]),
5542
+ seedVersion: z20.string().nullable(),
5543
+ contractVersion: z20.string().nullable(),
5544
+ message: z20.string().optional()
5056
5545
  });
5057
- var WrapperShape = z18.looseObject({
5058
- kind: z18.enum(["docker", "electron", "native", "contradictory", "unknown"]),
5059
- currentVersion: z18.string().nullable().optional()
5546
+ var WrapperShape = z20.looseObject({
5547
+ kind: z20.enum(["docker", "electron", "native", "contradictory", "unknown"]),
5548
+ currentVersion: z20.string().nullable().optional()
5060
5549
  });
5061
- var ServerStatusShape2 = z18.looseObject({
5062
- runningVersion: z18.string().nullable(),
5063
- installedVersion: z18.string().nullable().optional(),
5064
- latestVersion: z18.string().nullable().optional(),
5065
- pendingVersion: z18.string().nullable().optional(),
5066
- restartRequired: z18.boolean().optional(),
5067
- updateAvailable: z18.boolean().optional(),
5068
- updateState: z18.string().optional(),
5069
- lastCheckedAtMs: z18.number().nullable().optional(),
5070
- rolledBack: z18.looseObject({
5071
- fromVersion: z18.string(),
5072
- toVersion: z18.string().nullable(),
5073
- reason: z18.string()
5550
+ var ServerStatusShape2 = z20.looseObject({
5551
+ runningVersion: z20.string().nullable(),
5552
+ installedVersion: z20.string().nullable().optional(),
5553
+ latestVersion: z20.string().nullable().optional(),
5554
+ pendingVersion: z20.string().nullable().optional(),
5555
+ restartRequired: z20.boolean().optional(),
5556
+ updateAvailable: z20.boolean().optional(),
5557
+ updateState: z20.string().optional(),
5558
+ lastCheckedAtMs: z20.number().nullable().optional(),
5559
+ rolledBack: z20.looseObject({
5560
+ fromVersion: z20.string(),
5561
+ toVersion: z20.string().nullable(),
5562
+ reason: z20.string()
5074
5563
  }).nullable().optional(),
5075
5564
  imageContract: ImageContractShape.optional(),
5076
5565
  wrapper: WrapperShape.optional()
5077
5566
  });
5078
- var AddonUpdateShape = z18.looseObject({
5079
- name: z18.string(),
5080
- currentVersion: z18.string(),
5081
- latestVersion: z18.string(),
5082
- category: z18.enum(["addon", "core"]),
5083
- requiresRestart: z18.boolean().optional(),
5084
- isSystem: z18.boolean()
5567
+ var AddonUpdateShape = z20.looseObject({
5568
+ name: z20.string(),
5569
+ currentVersion: z20.string(),
5570
+ latestVersion: z20.string(),
5571
+ category: z20.enum(["addon", "core"]),
5572
+ requiresRestart: z20.boolean().optional(),
5573
+ isSystem: z20.boolean()
5085
5574
  });
5086
- var InstallationsShape2 = z18.looseObject({
5087
- installations: z18.array(
5088
- z18.looseObject({
5089
- packageName: z18.string(),
5090
- nodeId: z18.string(),
5091
- isHub: z18.boolean(),
5092
- isOnline: z18.boolean(),
5093
- version: z18.string().nullable(),
5094
- onHub: z18.boolean().optional()
5575
+ var InstallationsShape2 = z20.looseObject({
5576
+ installations: z20.array(
5577
+ z20.looseObject({
5578
+ packageName: z20.string(),
5579
+ nodeId: z20.string(),
5580
+ isHub: z20.boolean(),
5581
+ isOnline: z20.boolean(),
5582
+ version: z20.string().nullable(),
5583
+ onHub: z20.boolean().optional()
5095
5584
  })
5096
5585
  ),
5097
- unreachable: z18.array(z18.looseObject({ nodeId: z18.string(), error: z18.string() }))
5586
+ unreachable: z20.array(z20.looseObject({ nodeId: z20.string(), error: z20.string() }))
5098
5587
  });
5099
- var ServerActionShape = z18.looseObject({
5100
- accepted: z18.boolean().optional(),
5101
- outcome: z18.enum(["staged", "restart-scheduled", "already-current", "refused", "failed"]).optional(),
5102
- reason: z18.string().nullable().optional(),
5103
- error: z18.string().nullable().optional(),
5104
- message: z18.string().optional(),
5105
- blockers: z18.array(z18.looseObject({ addonId: z18.string(), reason: z18.string() })).optional()
5588
+ var ServerActionShape = z20.looseObject({
5589
+ accepted: z20.boolean().optional(),
5590
+ outcome: z20.enum(["staged", "restart-scheduled", "already-current", "refused", "failed"]).optional(),
5591
+ reason: z20.string().nullable().optional(),
5592
+ error: z20.string().nullable().optional(),
5593
+ message: z20.string().optional(),
5594
+ blockers: z20.array(z20.looseObject({ addonId: z20.string(), reason: z20.string() })).optional()
5106
5595
  });
5107
5596
  function statusInput(node) {
5108
5597
  return node.isHub === true ? void 0 : { nodeId: node.id };
@@ -5217,790 +5706,6 @@ function addonDrift(installations) {
5217
5706
  return drift;
5218
5707
  }
5219
5708
 
5220
- // src/mcp/ops-update-cli.ts
5221
- import { execFile } from "child_process";
5222
-
5223
- // src/mcp/setup-tools.ts
5224
- import { text as text10 } from "@camstack/develop";
5225
- import { z as z22 } from "zod";
5226
-
5227
- // src/mcp/setup-hubs.ts
5228
- import * as https from "https";
5229
- import {
5230
- createHubAgent,
5231
- explainTlsFailure,
5232
- HubAddressError,
5233
- hubCaFileForServer,
5234
- loadSession,
5235
- normalizeHubAddress,
5236
- resolveHubTls
5237
- } from "@camstack/develop";
5238
- import { z as z21 } from "zod";
5239
-
5240
- // src/mcp/setup-install.ts
5241
- import { z as z20 } from "zod";
5242
-
5243
- // src/mcp/setup-install-rules.ts
5244
- import { z as z19 } from "zod";
5245
- var HOST_KINDS = [
5246
- "docker-compose",
5247
- "docker",
5248
- "unraid",
5249
- "proxmox-lxc",
5250
- "macos-desktop"
5251
- ];
5252
- var HostKindSchema = z19.enum(HOST_KINDS);
5253
- var ROLES = ["hub", "agent"];
5254
- var RoleSchema = z19.enum(ROLES);
5255
- var ACCELERATORS = ["intel", "amd", "coral", "apple", "cpu"];
5256
- var AcceleratorSchema = z19.enum(ACCELERATORS);
5257
- var InstallPathsSchema = z19.object({
5258
- data: z19.string().min(1),
5259
- recordings: z19.string().min(1),
5260
- config: z19.string().min(1).optional(),
5261
- backups: z19.string().min(1).optional()
5262
- });
5263
- var InstallGuideInputSchema = z19.object({
5264
- hostKind: HostKindSchema.optional(),
5265
- role: RoleSchema.optional(),
5266
- accelerator: AcceleratorSchema.optional(),
5267
- paths: InstallPathsSchema.optional(),
5268
- /** Agents only: the hub the agent should dial (host or IP). */
5269
- hubAddress: z19.string().min(1).optional()
5270
- });
5271
- var RAW = "https://raw.githubusercontent.com/camstack";
5272
- var TEMPLATE_URLS = {
5273
- compose: {
5274
- hub: `${RAW}/proxmox/main/compose/hub.yml`,
5275
- agent: `${RAW}/proxmox/main/compose/agent.yml`
5276
- },
5277
- proxmoxInstaller: `${RAW}/proxmox/main/install-camstack-proxmox.sh`,
5278
- unraid: {
5279
- hub: `${RAW}/unraid-templates/main/templates/camstack.xml`,
5280
- agent: `${RAW}/unraid-templates/main/templates/camstack-agent.xml`
5281
- },
5282
- latestRelease: "https://api.github.com/repos/camstack/server/releases/latest"
5283
- };
5284
- var IMAGE_REPOSITORY = "ghcr.io/camstack/camstack";
5285
- var IMAGE_TAG_BY_ACCELERATOR = {
5286
- intel: "intel-latest",
5287
- amd: "amd-latest",
5288
- // No Coral- or CPU-specific image: Intel hosts and the Unraid templates track intel-latest.
5289
- coral: "intel-latest",
5290
- cpu: "intel-latest",
5291
- apple: "intel-latest"
5292
- };
5293
- var NO_BARE_LATEST = "There is no bare :latest tag: use intel-<version>/intel-latest or amd-<version>/amd-latest, and never mix them across vendors.";
5294
- var MUST_NOT_OMIT = [
5295
- "--pids-limit=8192 (pids_limit: 8192): the default 2048 counts threads and silently stops media on a busy hub.",
5296
- "CAMSTACK_MEDIA_ROOT=/recordings: without it recordings land in /data and fill the cache drive.",
5297
- "/config mapped: it is a VOLUME, so an unmapped path mints an anonymous volume on every re-create.",
5298
- "--restart unless-stopped: the NVR must come back after a host reboot.",
5299
- "CAMSTACK_MOLECULER_USE_HOSTNAME=false: otherwise a multi-machine cluster never forms.",
5300
- "Coral USB: /dev/bus/usb AND the device-cgroup rule c 189:* rmw."
5301
- ];
5302
- var COMPOSE_MARKERS_BY_ACCELERATOR = {
5303
- intel: ["devices", "dri", "accel"],
5304
- amd: ["devices", "dri"],
5305
- coral: ["devices", "usb"],
5306
- cpu: [],
5307
- apple: []
5308
- };
5309
- function firstRunHub(host) {
5310
- return [
5311
- `Open https://${host}:4443 (a self-signed certificate: accept the browser warning).`,
5312
- "Sign in as admin / changeme; CamStack asks for a new password at once. Change it now.",
5313
- `Then, where your MCP client runs: camstack login ${host}`
5314
- ];
5315
- }
5316
-
5317
- // src/mcp/setup-compose.ts
5318
- var MARKER_LINE = /^(\s*)# (.*#@([A-Za-z0-9_]+)\s*)$/;
5319
- var VOLUME_HOST_PATHS = [
5320
- { container: "/data", key: "data" },
5321
- { container: "/recordings", key: "recordings" },
5322
- { container: "/config", key: "config" },
5323
- { container: "/backups", key: "backups" }
5324
- ];
5325
- function swapImageTag(body, imageTag) {
5326
- const escaped = IMAGE_REPOSITORY.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5327
- return body.replace(new RegExp(`(${escaped}):[A-Za-z0-9._-]+`), `$1:${imageTag}`);
5328
- }
5329
- function enableMarkers(body, markers) {
5330
- return body.split("\n").map((line) => {
5331
- const match = MARKER_LINE.exec(line);
5332
- if (match === null) return line;
5333
- const tag = match[3];
5334
- return tag !== void 0 && markers.includes(tag) ? `${match[1] ?? ""}${match[2] ?? ""}` : line;
5335
- }).join("\n");
5336
- }
5337
- function setVolumeHosts(body, paths) {
5338
- const edits = [];
5339
- let out = body;
5340
- for (const { container, key } of VOLUME_HOST_PATHS) {
5341
- const wanted = paths?.[key];
5342
- const volume = new RegExp(`^(\\s*- )([^\\s:]+)(:${container}\\s*)$`, "m");
5343
- if (!volume.test(out)) {
5344
- if (wanted !== void 0) {
5345
- edits.push(
5346
- `${container}: no volume line found in the template; map ${wanted} to it yourself`
5347
- );
5348
- }
5349
- continue;
5350
- }
5351
- if (wanted === void 0) {
5352
- edits.push(
5353
- `${container}: left at the template's host path, change it if that is not where you want it`
5354
- );
5355
- continue;
5356
- }
5357
- out = out.replace(
5358
- volume,
5359
- (_m, pre, _host, post) => `${pre}${wanted}${post}`
5360
- );
5361
- }
5362
- return { body: out, edits };
5363
- }
5364
- function setHubAddress(body, hubAddress) {
5365
- const line = /^(\s*- CAMSTACK_HUB_ADDRESS=).*$/m;
5366
- if (!line.test(body)) return { body, edits: [] };
5367
- if (hubAddress === void 0) {
5368
- return {
5369
- body,
5370
- edits: [
5371
- "CAMSTACK_HUB_ADDRESS: export it before `docker compose up` (or edit the line) with the hub IP or hostname"
5372
- ]
5373
- };
5374
- }
5375
- return {
5376
- body: body.replace(line, (_m, pre) => `${pre}${hubAddress}`),
5377
- edits: []
5378
- };
5379
- }
5380
- function customizeCompose(source, c) {
5381
- const tagged = enableMarkers(swapImageTag(source, c.imageTag), c.markers);
5382
- const volumes = setVolumeHosts(tagged, c.paths);
5383
- const hub = setHubAddress(volumes.body, c.hubAddress);
5384
- return { body: hub.body, edits: [...volumes.edits, ...hub.edits] };
5385
- }
5386
- function unquote(value) {
5387
- const v = value.trim();
5388
- if (v.length >= 2 && (v.startsWith('"') || v.startsWith("'")) && v.endsWith(v[0] ?? "")) {
5389
- return v.slice(1, -1);
5390
- }
5391
- return v;
5392
- }
5393
- function stripComment(line) {
5394
- return line.replace(/\s+#.*$/, "");
5395
- }
5396
- function readService(body) {
5397
- const fields = {
5398
- scalars: /* @__PURE__ */ new Map(),
5399
- lists: /* @__PURE__ */ new Map(),
5400
- maps: /* @__PURE__ */ new Map()
5401
- };
5402
- let key = null;
5403
- let sub = null;
5404
- for (const raw of body.split("\n")) {
5405
- if (raw.trim() === "" || raw.trim().startsWith("#")) continue;
5406
- const line = stripComment(raw);
5407
- const indent = line.length - line.trimStart().length;
5408
- const text14 = line.trim();
5409
- if (indent === 4) {
5410
- sub = null;
5411
- const m = /^([A-Za-z_]+):\s*(.*)$/.exec(text14);
5412
- if (m === null) continue;
5413
- key = m[1] ?? null;
5414
- if (key !== null && (m[2] ?? "") !== "") fields.scalars.set(key, unquote(m[2] ?? ""));
5415
- } else if (indent === 6 && key !== null) {
5416
- if (text14.startsWith("- ")) {
5417
- const list = fields.lists.get(key) ?? [];
5418
- list.push(unquote(text14.slice(2)));
5419
- fields.lists.set(key, list);
5420
- } else {
5421
- const m = /^([A-Za-z_-]+):\s*(.*)$/.exec(text14);
5422
- if (m === null) continue;
5423
- sub = m[1] ?? null;
5424
- const map = fields.maps.get(key) ?? /* @__PURE__ */ new Map();
5425
- if ((m[2] ?? "") !== "" && sub !== null) map.set(sub, unquote(m[2] ?? ""));
5426
- fields.maps.set(key, map);
5427
- }
5428
- } else if (indent === 8 && key !== null && sub !== null) {
5429
- const m = /^([A-Za-z_-]+):\s*(.*)$/.exec(text14);
5430
- if (m === null) continue;
5431
- const map = fields.maps.get(`${key}.${sub}`) ?? /* @__PURE__ */ new Map();
5432
- map.set(m[1] ?? "", unquote(m[2] ?? ""));
5433
- fields.maps.set(`${key}.${sub}`, map);
5434
- }
5435
- }
5436
- return fields;
5437
- }
5438
- function shellQuote(value) {
5439
- return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
5440
- }
5441
- function composeToDockerRun(body) {
5442
- const f = readService(body);
5443
- const image = f.scalars.get("image");
5444
- if (image === void 0) return null;
5445
- const args = ["docker run -d"];
5446
- const name = f.scalars.get("container_name");
5447
- if (name !== void 0) args.push(`--name ${shellQuote(name)}`);
5448
- const restart = f.scalars.get("restart");
5449
- if (restart !== void 0) args.push(`--restart ${shellQuote(restart)}`);
5450
- const network = f.scalars.get("network_mode");
5451
- if (network !== void 0) args.push(`--network ${shellQuote(network)}`);
5452
- const pids = f.scalars.get("pids_limit");
5453
- if (pids !== void 0) args.push(`--pids-limit=${pids}`);
5454
- for (const d of f.lists.get("devices") ?? []) args.push(`--device ${shellQuote(d)}`);
5455
- for (const r of f.lists.get("device_cgroup_rules") ?? []) {
5456
- args.push(`--device-cgroup-rule ${shellQuote(r)}`);
5457
- }
5458
- for (const g of f.lists.get("group_add") ?? []) args.push(`--group-add ${shellQuote(g)}`);
5459
- for (const v of f.lists.get("volumes") ?? []) args.push(`-v ${shellQuote(v)}`);
5460
- for (const e of f.lists.get("environment") ?? []) {
5461
- if (!e.includes("${")) args.push(`-e ${shellQuote(e)}`);
5462
- }
5463
- const driver = f.maps.get("logging")?.get("driver");
5464
- if (driver !== void 0) args.push(`--log-driver ${shellQuote(driver)}`);
5465
- for (const [k, v] of f.maps.get("logging.options") ?? []) {
5466
- args.push(`--log-opt ${shellQuote(`${k}=${v}`)}`);
5467
- }
5468
- args.push(shellQuote(image));
5469
- return args.join(" \\\n ");
5470
- }
5471
-
5472
- // src/mcp/setup-install.ts
5473
- var FETCH_TIMEOUT_MS = 1e4;
5474
- var CONTAINER_ROUTES = ["docker-compose", "docker"];
5475
- var Q_HOST = {
5476
- id: "hostKind",
5477
- question: "Where will CamStack run?",
5478
- options: ["docker-compose", "docker", "unraid", "proxmox-lxc", "macos-desktop"]
5479
- };
5480
- var Q_ROLE = {
5481
- id: "role",
5482
- question: "Which role? Start with a hub; an agent only adds a second machine to an existing hub.",
5483
- options: ["hub", "agent"]
5484
- };
5485
- var Q_ACCEL = {
5486
- id: "accelerator",
5487
- question: "Which accelerator does this host have?",
5488
- options: ["intel", "amd", "coral", "cpu"]
5489
- };
5490
- var Q_PATHS = {
5491
- id: "paths",
5492
- question: "Host paths as {data, recordings} (optionally config, backups). Recordings is the one that grows: put it on a roomy disk.",
5493
- options: []
5494
- };
5495
- function missingQuestions(input) {
5496
- const out = [];
5497
- if (input.hostKind === void 0) out.push(Q_HOST);
5498
- if (input.role === void 0) out.push(Q_ROLE);
5499
- const desktop = input.hostKind === "macos-desktop";
5500
- if (!desktop && (input.accelerator === void 0 || input.accelerator === "apple")) {
5501
- out.push(Q_ACCEL);
5502
- }
5503
- if (input.hostKind !== void 0 && CONTAINER_ROUTES.includes(input.hostKind) && input.paths === void 0) {
5504
- out.push(Q_PATHS);
5505
- }
5506
- return out;
5507
- }
5508
- async function fetchText(url, fetchFn) {
5509
- try {
5510
- const res = await fetchFn(url, {
5511
- signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
5512
- });
5513
- if (!res.ok) {
5514
- return {
5515
- ok: false,
5516
- failure: {
5517
- status: "fetch-failed",
5518
- url,
5519
- error: `answered ${res.status}`
5520
- }
5521
- };
5522
- }
5523
- return { ok: true, body: await res.text() };
5524
- } catch (err) {
5525
- const error = err instanceof Error ? err.message : String(err);
5526
- return { ok: false, failure: { status: "fetch-failed", url, error } };
5527
- }
5528
- }
5529
- var ReleaseSchema = z20.object({
5530
- tag_name: z20.string(),
5531
- assets: z20.array(z20.object({ name: z20.string(), browser_download_url: z20.string() }))
5532
- });
5533
- var DMG = /^CamStack-[0-9][0-9A-Za-z.-]*-(arm64|x64)\.dmg$/;
5534
- function acceleratorEdits(accelerator, role) {
5535
- const note = `Image tag ${IMAGE_TAG_BY_ACCELERATOR[accelerator]} for ${accelerator}. ${NO_BARE_LATEST}`;
5536
- return role === "agent" ? [note, "Do not run a hub and an agent on the same host."] : [note];
5537
- }
5538
- async function composeGuide(input, fetchFn, asDockerRun) {
5539
- const url = TEMPLATE_URLS.compose[input.role];
5540
- const fetched = await fetchText(url, fetchFn);
5541
- if (!fetched.ok) return fetched.failure;
5542
- const imageTag = IMAGE_TAG_BY_ACCELERATOR[input.accelerator];
5543
- const custom = customizeCompose(fetched.body, {
5544
- imageTag,
5545
- markers: COMPOSE_MARKERS_BY_ACCELERATOR[input.accelerator],
5546
- paths: input.paths,
5547
- hubAddress: input.hubAddress
5548
- });
5549
- const edits = [...custom.edits, ...acceleratorEdits(input.accelerator, input.role)];
5550
- const first = firstRun(input);
5551
- if (!asDockerRun) {
5552
- return ready(input, imageTag, {
5553
- steps: [
5554
- "Save docker-compose.yml (below) in an empty directory on the host.",
5555
- "docker compose up -d && docker compose logs -f camstack",
5556
- "The first boot seeds addons and pulls models: allow a few minutes.",
5557
- ...first
5558
- ],
5559
- files: [{ path: "docker-compose.yml", body: custom.body, source: url }],
5560
- edits
5561
- });
5562
- }
5563
- const run = composeToDockerRun(custom.body);
5564
- if (run === null) {
5565
- return {
5566
- status: "fetch-failed",
5567
- url,
5568
- error: "the official compose file has no image line; cannot derive docker run"
5569
- };
5570
- }
5571
- return ready(input, imageTag, {
5572
- steps: [
5573
- "Create the host directories named by the -v lines, then run the file below as a shell script.",
5574
- "docker logs -f camstack (the first boot seeds addons and pulls models).",
5575
- ...first
5576
- ],
5577
- files: [{ path: "run-camstack.sh", body: `#!/bin/sh
5578
- ${run}
5579
- `, source: url }],
5580
- edits
5581
- });
5582
- }
5583
- async function unraidGuide(input, fetchFn) {
5584
- const url = TEMPLATE_URLS.unraid[input.role];
5585
- const fetched = await fetchText(url, fetchFn);
5586
- if (!fetched.ok) return fetched.failure;
5587
- const imageTag = IMAGE_TAG_BY_ACCELERATOR[input.accelerator];
5588
- const edits = [
5589
- `In the template form, set Repository to ${IMAGE_REPOSITORY}:${imageTag} (the template ships intel-latest). ${NO_BARE_LATEST}`,
5590
- "Recordings: point it at a roomy array share, not the cache; leave Media root as /recordings.",
5591
- "Keep ExtraParams as shipped: --device-cgroup-rule and --pids-limit=8192.",
5592
- ...unraidDeviceEdits(input.accelerator)
5593
- ];
5594
- const first = firstRun(input);
5595
- return ready(input, imageTag, {
5596
- steps: [
5597
- `Unraid: Docker > Add Container > Template, paste ${url}`,
5598
- "Review the paths and Repository (edits below), then Apply.",
5599
- ...first
5600
- ],
5601
- files: [{ path: urlName(url), body: fetched.body, source: url }],
5602
- edits
5603
- });
5604
- }
5605
- function unraidDeviceEdits(accelerator) {
5606
- if (accelerator === "intel" || accelerator === "amd") {
5607
- return ["Intel/AMD iGPU: enter /dev/dri in that field. Intel NPU: enter /dev/accel."];
5608
- }
5609
- if (accelerator === "coral") {
5610
- return ["Coral USB: enter /dev/bus/usb (the whole bus). Coral PCIe/M.2: /dev/apex_0 (and _1)."];
5611
- }
5612
- return [];
5613
- }
5614
- function urlName(url) {
5615
- return url.slice(url.lastIndexOf("/") + 1);
5616
- }
5617
- async function proxmoxGuide(input, fetchFn) {
5618
- const url = TEMPLATE_URLS.proxmoxInstaller;
5619
- const fetched = await fetchText(url, fetchFn);
5620
- if (!fetched.ok) return fetched.failure;
5621
- const imageTag = IMAGE_TAG_BY_ACCELERATOR[input.accelerator];
5622
- const vars = [`ROLE=${input.role}`];
5623
- vars.push(`IMAGE_TAG=${IMAGE_REPOSITORY}:${imageTag}`);
5624
- if (input.accelerator === "cpu") vars.push("PASS_ACCEL=none");
5625
- if (input.role === "agent") vars.push(`CAMSTACK_HUB_ADDRESS=${input.hubAddress ?? "<hub-ip>"}`);
5626
- if (input.paths !== void 0) vars.push(`RECORDINGS_HOST_PATH=${input.paths.recordings}`);
5627
- const header = fetched.body.split("\n").slice(1, 20).filter((l) => l.startsWith("#")).join("\n");
5628
- const first = firstRun(input);
5629
- return ready(input, imageTag, {
5630
- steps: [
5631
- "Run on the Proxmox HOST (needs `pct`), not inside a container. It asks nothing: everything is an environment variable (defaults: VMID 200 hub / 201 agent, 8 cores, 8 GB, 64 GB disk, bridge vmbr0).",
5632
- `${vars.join(" ")} bash -c "$(curl -fsSL ${url})"`,
5633
- "It builds an Ubuntu 24.04 LXC with Docker, passes through the accelerators the host has (PASS_ACCEL=none skips it) and starts CamStack.",
5634
- ...first
5635
- ],
5636
- files: [
5637
- {
5638
- path: "install-camstack-proxmox.sh (header)",
5639
- body: header,
5640
- source: url
5641
- }
5642
- ],
5643
- edits: [
5644
- `The script defaults to the amd image; IMAGE_TAG above sets ${imageTag}. ${NO_BARE_LATEST}`,
5645
- "A hub and an agent on one machine need two CTs (host networking collides otherwise)."
5646
- ]
5647
- });
5648
- }
5649
- async function macGuide(input, fetchFn) {
5650
- const url = TEMPLATE_URLS.latestRelease;
5651
- const fetched = await fetchText(url, fetchFn);
5652
- if (!fetched.ok) return fetched.failure;
5653
- let release;
5654
- try {
5655
- const parsed = ReleaseSchema.safeParse(JSON.parse(fetched.body));
5656
- if (!parsed.success) {
5657
- return {
5658
- status: "fetch-failed",
5659
- url,
5660
- error: "the release answer had no assets"
5661
- };
5662
- }
5663
- release = parsed.data;
5664
- } catch {
5665
- return {
5666
- status: "fetch-failed",
5667
- url,
5668
- error: "the release answer was not JSON"
5669
- };
5670
- }
5671
- const dmgs = release.assets.filter((a) => DMG.test(a.name));
5672
- const first = firstRun(input);
5673
- return ready(input, null, {
5674
- steps: [
5675
- `Download the disk image for your Mac from release ${release.tag_name}: ${dmgs.map((a) => `${a.name} (${a.browser_download_url})`).join(" or ")}. Apple silicon (M1 and later) takes arm64; Intel takes x64. The CamStackViewer files are a different app.`,
5676
- "Drag CamStack into Applications and open it.",
5677
- `The setup window asks what this Mac should be: choose ${input.role === "hub" ? "Hub (full server)" : "Agent (worker)"}, then Save and start.`,
5678
- ...first
5679
- ],
5680
- files: [],
5681
- edits: ["A Mac uses the app, not Docker: a container there cannot reach the Neural Engine."]
5682
- });
5683
- }
5684
- function firstRun(input) {
5685
- const host = input.hostKind === "macos-desktop" ? "localhost" : "<host>";
5686
- if (input.role === "hub") return firstRunHub(host);
5687
- return [
5688
- "The agent status dashboard answers on http://<agent-host>:4444.",
5689
- "Pair it from the hub: run setup_add_node (it lists agents found on the network), then in the admin UI Cluster > Nodes > Agents found on the network. Type the six digits shown on the other screen on BOTH sides."
5690
- ];
5691
- }
5692
- function ready(input, imageTag, body) {
5693
- return {
5694
- status: "ready",
5695
- hostKind: input.hostKind,
5696
- role: input.role,
5697
- imageTag,
5698
- steps: body.steps,
5699
- files: body.files,
5700
- edits: body.edits,
5701
- mustNotOmit: input.hostKind === "macos-desktop" ? [] : MUST_NOT_OMIT
5702
- };
5703
- }
5704
- async function installGuide(input, fetchFn) {
5705
- const questions = missingQuestions(input);
5706
- if (questions.length > 0) return { status: "need-answers", questions };
5707
- const { hostKind, role } = input;
5708
- if (hostKind === void 0 || role === void 0) {
5709
- return { status: "need-answers", questions: missingQuestions({}) };
5710
- }
5711
- const resolved = {
5712
- hostKind,
5713
- role,
5714
- accelerator: hostKind === "macos-desktop" ? "apple" : input.accelerator ?? "cpu",
5715
- paths: input.paths,
5716
- hubAddress: input.hubAddress
5717
- };
5718
- switch (hostKind) {
5719
- case "docker-compose":
5720
- return composeGuide(resolved, fetchFn, false);
5721
- case "docker":
5722
- return composeGuide(resolved, fetchFn, true);
5723
- case "unraid":
5724
- return unraidGuide(resolved, fetchFn);
5725
- case "proxmox-lxc":
5726
- return proxmoxGuide(resolved, fetchFn);
5727
- case "macos-desktop":
5728
- return macGuide(resolved, fetchFn);
5729
- }
5730
- }
5731
-
5732
- // src/mcp/setup-hubs.ts
5733
- var DEFAULT_DISCOVER_TIMEOUT_MS = 2500;
5734
- var MAX_DISCOVER_TIMEOUT_MS = 15e3;
5735
- var HEALTH_TIMEOUT_MS = 5e3;
5736
- var LOGIN = (server) => `run \`camstack login ${server}\` in a terminal`;
5737
- var httpsHealth = (url, tls) => new Promise((resolve) => {
5738
- const req = https.get(
5739
- `${url}/health`,
5740
- { agent: createHubAgent(tls), timeout: HEALTH_TIMEOUT_MS },
5741
- (res) => {
5742
- res.resume();
5743
- resolve({ ok: true, status: res.statusCode ?? 0 });
5744
- }
5745
- );
5746
- req.on("timeout", () => req.destroy(new Error("timed out")));
5747
- req.on("error", (err) => {
5748
- resolve({
5749
- ok: false,
5750
- error: err.message,
5751
- tlsHint: explainTlsFailure(url, err)
5752
- });
5753
- });
5754
- });
5755
- async function discoverHubsTool(timeoutMs, deps) {
5756
- const wait = Math.min(timeoutMs ?? DEFAULT_DISCOVER_TIMEOUT_MS, MAX_DISCOVER_TIMEOUT_MS);
5757
- const finder = deps.findHubs ?? ((ms) => findHubs({
5758
- mdnsTimeoutMs: ms,
5759
- ...deps.sessionDir !== void 0 ? { sessionDir: deps.sessionDir } : {},
5760
- ...deps.env !== void 0 ? { env: deps.env } : {}
5761
- }));
5762
- const found = await finder(wait);
5763
- const hubs = found.map((h) => ({
5764
- host: h.host,
5765
- port: h.port,
5766
- name: h.nodeId,
5767
- version: h.version,
5768
- sources: h.sources
5769
- }));
5770
- return {
5771
- status: "ok",
5772
- hubs,
5773
- next: hubs.length === 0 ? "no hub answered: install one with setup_install_guide, or pass its address to setup_connect" : "pass the host of the hub you want to setup_connect"
5774
- };
5775
- }
5776
- function pinnedTls(url, deps) {
5777
- const caFile = hubCaFileForServer(url, deps.sessionDir);
5778
- try {
5779
- return {
5780
- tls: resolveHubTls(url, deps.env ?? process.env, deps.sessionDir),
5781
- caFile
5782
- };
5783
- } catch {
5784
- return { tls: {}, caFile };
5785
- }
5786
- }
5787
- async function connectTool(server, deps) {
5788
- let url;
5789
- try {
5790
- url = normalizeHubAddress(server);
5791
- } catch (err) {
5792
- const message = err instanceof HubAddressError ? err.message : String(err);
5793
- return { status: "invalid-address", server, message };
5794
- }
5795
- const { tls, caFile } = pinnedTls(url, deps);
5796
- const outcome = await (deps.health ?? httpsHealth)(url, tls);
5797
- const caPinned = tls.ca !== void 0;
5798
- const session = loadSession(url, deps.sessionDir);
5799
- const sessionInfo = {
5800
- exists: session !== null,
5801
- username: session?.username ?? null
5802
- };
5803
- if (!outcome.ok) {
5804
- return {
5805
- status: "unreachable",
5806
- server: url,
5807
- message: outcome.error,
5808
- tls: { caPinned, caFile, hint: outcome.tlsHint },
5809
- session: sessionInfo,
5810
- next: outcome.tlsHint === null ? "check the host, that the hub is running, and that port 4443 is reachable" : `the hub answers but its certificate is not trusted: ${LOGIN(url)}; login pins the CA (never disable verification)`
5811
- };
5812
- }
5813
- return {
5814
- status: "reachable",
5815
- server: url,
5816
- health: { status: outcome.status },
5817
- tls: { caPinned, caFile, hint: null },
5818
- session: sessionInfo,
5819
- next: session === null ? LOGIN(url) : "a session exists: the ops tools can use it"
5820
- };
5821
- }
5822
- var DiscoveredAgentsSchema = z21.object({
5823
- agents: z21.array(
5824
- z21.object({
5825
- nodeId: z21.string(),
5826
- host: z21.string(),
5827
- port: z21.number(),
5828
- version: z21.string().nullable(),
5829
- name: z21.string().nullable(),
5830
- state: z21.enum(["unpaired", "pending", "paired"])
5831
- })
5832
- )
5833
- });
5834
- var PAIRING_STEPS = [
5835
- "Pairing is started from the HUB (D857): admin UI, Cluster > Nodes > Agents found on the network. Pick the agent, or add it by address (host and port 4444).",
5836
- "Both screens now show six digits. Type the digits shown on the OTHER screen, on BOTH sides: the hub operator types the code the agent shows, and the agent operator confirms on the agent.",
5837
- "The agent installs its certificate only after its own operator confirmed. Nothing here (and no tool of this MCP) confirms a pairing for you.",
5838
- "The agent then appears under Cluster > Nodes."
5839
- ];
5840
- async function addNodeTool(input, ctx) {
5841
- const guide = await installGuide({ ...input, role: "agent" }, ctx.fetch);
5842
- const outcome = await ctx.hub();
5843
- if (!outcome.ok) {
5844
- return {
5845
- guide,
5846
- pairing: PAIRING_STEPS,
5847
- agentsOnNetwork: { status: "no-session", action: outcome.action }
5848
- };
5849
- }
5850
- try {
5851
- const raw = await outcome.connection.client.call("GET", "agentPairing.listDiscoveredAgents", {});
5852
- const parsed = DiscoveredAgentsSchema.safeParse(raw);
5853
- return {
5854
- guide,
5855
- pairing: PAIRING_STEPS,
5856
- agentsOnNetwork: parsed.success ? { status: "ok", agents: parsed.data.agents } : {
5857
- status: "unavailable",
5858
- error: "the hub answered in an unexpected shape"
5859
- }
5860
- };
5861
- } catch (err) {
5862
- return {
5863
- guide,
5864
- pairing: PAIRING_STEPS,
5865
- agentsOnNetwork: {
5866
- status: "unavailable",
5867
- error: err instanceof Error ? err.message : String(err)
5868
- }
5869
- };
5870
- }
5871
- }
5872
-
5873
- // src/mcp/setup-tools.ts
5874
- var SUPPORTED_HOST_KINDS = HOST_KINDS;
5875
- var REGISTRY2 = "https://registry.npmjs.org";
5876
- var REGISTRY_TIMEOUT_MS2 = 5e3;
5877
- var RegistryLatestSchema = z22.object({ version: z22.string() });
5878
- async function latestOnNpm(name, fetchFn) {
5879
- const url = `${REGISTRY2}/${name.replace("/", "%2f")}/latest`;
5880
- try {
5881
- const res = await fetchFn(url, {
5882
- signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS2)
5883
- });
5884
- if (!res.ok) return { latest: null, error: `registry answered ${res.status}` };
5885
- const parsed = RegistryLatestSchema.safeParse(await res.json());
5886
- if (!parsed.success) return { latest: null, error: "registry answer had no version" };
5887
- return { latest: parsed.data.version };
5888
- } catch (err) {
5889
- return {
5890
- latest: null,
5891
- error: err instanceof Error ? err.message : String(err)
5892
- };
5893
- }
5894
- }
5895
- async function about(ctx) {
5896
- const [cli, server] = await Promise.all([
5897
- latestOnNpm("camstack", ctx.fetch),
5898
- latestOnNpm("@camstack/server", ctx.fetch)
5899
- ]);
5900
- return {
5901
- cli: { installed: ctx.cliVersion, ...cli },
5902
- server,
5903
- supportedHostKinds: SUPPORTED_HOST_KINDS
5904
- };
5905
- }
5906
- function registerSetupTools(server, ctx, deps = {}) {
5907
- server.tool(
5908
- "setup_about",
5909
- "Versions of the camstack CLI (installed and latest on npm), the latest @camstack/server on npm, and the host kinds CamStack supports. Needs no hub and no login.",
5910
- {},
5911
- async () => text10(await about(ctx))
5912
- );
5913
- server.tool(
5914
- "setup_install_guide",
5915
- 'Interview-style install guide. Call it with whatever you know; while an answer is missing it returns {status:"need-answers", questions} listing only what is still unknown. When complete it returns the steps and the OFFICIAL template files (fetched live) for that route: docker-compose, docker, unraid, proxmox-lxc or macos-desktop. Start with a hub.',
5916
- {
5917
- hostKind: HostKindSchema.optional(),
5918
- role: RoleSchema.optional(),
5919
- accelerator: AcceleratorSchema.optional(),
5920
- paths: InstallPathsSchema.optional(),
5921
- hubAddress: z22.string().min(1).optional().describe("Agents only: the hub to dial.")
5922
- },
5923
- async (input) => text10(await installGuide(input, ctx.fetch))
5924
- );
5925
- server.tool(
5926
- "setup_discover_hubs",
5927
- "Find CamStack hubs on the local network (mDNS) plus the hubs this machine already knows, with host, port, name and version. Needs no login.",
5928
- { timeoutMs: z22.number().int().min(200).max(15e3).optional() },
5929
- async ({ timeoutMs }) => text10(await discoverHubsTool(timeoutMs, deps))
5930
- );
5931
- server.tool(
5932
- "setup_connect",
5933
- "Check that a hub answers on https://<server>:4443/health, whether its CA is already pinned for TLS, and whether a `camstack login` session exists. Never takes a password: when there is no session it says to run `camstack login <server>` in a terminal.",
5934
- { server: z22.string().min(1) },
5935
- async ({ server: address }) => text10(await connectTool(address, deps))
5936
- );
5937
- server.tool(
5938
- "setup_add_node",
5939
- "Guide to add an AGENT (a second machine) to a hub: the install route for that host, then the pairing flow where the operator types the six digits shown on the other screen on both sides. With a login session it also lists the agents found on the network. It never confirms a pairing.",
5940
- {
5941
- hostKind: HostKindSchema.optional(),
5942
- accelerator: AcceleratorSchema.optional(),
5943
- paths: InstallPathsSchema.optional(),
5944
- hubAddress: z22.string().min(1).optional()
5945
- },
5946
- async (input) => text10(await addNodeTool(input, ctx))
5947
- );
5948
- }
5949
-
5950
- // src/mcp/ops-update-cli.ts
5951
- var CLI_PACKAGE = "camstack";
5952
- var NPM_GLOBAL = /[\\/]lib[\\/]node_modules[\\/]camstack[\\/]|[\\/]npm[\\/]node_modules[\\/]camstack[\\/]/;
5953
- var OTHER_MANAGER = /[\\/]\.bun[\\/]|[\\/]pnpm[\\/]|[\\/]\.yarn[\\/]|[\\/]yarn[\\/]|[\\/]\.volta[\\/]/;
5954
- function detectCliInstall(entryPath) {
5955
- const manual = `Update it with the package manager it was installed with, for example \`npm install -g ${CLI_PACKAGE}@latest\` (no sudo: fix the npm prefix permissions instead).`;
5956
- if (/[\\/]_npx[\\/]/.test(entryPath)) {
5957
- return {
5958
- method: "npx",
5959
- command: null,
5960
- instructions: `The CLI runs through npx: there is nothing installed to update. Start it as \`npx ${CLI_PACKAGE}@latest mcp\` (or clear the npx cache), then restart the MCP in your client.`
5961
- };
5962
- }
5963
- if (OTHER_MANAGER.test(entryPath) || !NPM_GLOBAL.test(entryPath)) {
5964
- return { method: "unknown", command: null, instructions: manual };
5965
- }
5966
- return {
5967
- method: "npm-global",
5968
- command: ["npm", "install", "-g", `${CLI_PACKAGE}@latest`],
5969
- instructions: `npm install -g ${CLI_PACKAGE}@latest`
5970
- };
5971
- }
5972
- var runCommand = (file, args) => new Promise((resolve) => {
5973
- execFile(file, [...args], { timeout: 5 * 6e4 }, (err, stdout, stderr) => {
5974
- const out = `${stdout}${stderr}`.trim().slice(-600);
5975
- resolve({
5976
- ok: err === null,
5977
- output: err ? `${err.message} ${out}`.trim() : out
5978
- });
5979
- });
5980
- });
5981
- function parts(v) {
5982
- return v.split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
5983
- }
5984
- function isOlder(a, b) {
5985
- const x = parts(a);
5986
- const y = parts(b);
5987
- for (let i = 0; i < 3; i++) {
5988
- const d = (x[i] ?? 0) - (y[i] ?? 0);
5989
- if (d !== 0) return d < 0;
5990
- }
5991
- return false;
5992
- }
5993
- async function cliVerdict(installed, fetchFn, entryPath) {
5994
- const latest = await latestOnNpm(CLI_PACKAGE, fetchFn);
5995
- return {
5996
- installed,
5997
- latest: latest.latest,
5998
- updateAvailable: latest.latest === null ? null : isOlder(installed, latest.latest),
5999
- ...latest.error !== void 0 ? { error: latest.error } : {},
6000
- install: detectCliInstall(entryPath)
6001
- };
6002
- }
6003
-
6004
5709
  // src/mcp/ops-update-apply.ts
6005
5710
  var UPDATE_APPLY_TOOL = "ops_update_apply";
6006
5711
  function timing(deps) {
@@ -6053,7 +5758,7 @@ async function readPlan(client, args, ctx, deps) {
6053
5758
  wrapper: wrapperVerdict(status.value)
6054
5759
  });
6055
5760
  }
6056
- const updates = args.addons === false ? null : await readQuery(client, "addons.listUpdates", {}, z23.array(AddonUpdateShape));
5761
+ const updates = args.addons === false ? null : await readQuery(client, "addons.listUpdates", {}, z21.array(AddonUpdateShape));
6057
5762
  const cli = args.cli === false ? null : await cliVerdict(ctx.cliVersion, ctx.fetch, deps.entryPath ?? process.argv[1] ?? "");
6058
5763
  const hub = plans.find((p) => p.node.isHub === true) ?? null;
6059
5764
  return {
@@ -6369,14 +6074,14 @@ async function opsUpdateApply(ctx, args, confirmToken, deps = {}) {
6369
6074
  }
6370
6075
 
6371
6076
  // src/mcp/ops-update-check.ts
6372
- import { z as z24 } from "zod";
6077
+ import { z as z22 } from "zod";
6373
6078
  var ADDON_LIMIT = 30;
6374
6079
  async function opsUpdateCheck(ctx, deps = {}) {
6375
6080
  return withAdminHub(ctx, async ({ client, server }) => {
6376
6081
  const unavailable = {};
6377
6082
  const [topology, updates, installs, cli] = await Promise.all([
6378
6083
  readTopology(client),
6379
- readQuery(client, "addons.listUpdates", {}, z24.array(AddonUpdateShape)),
6084
+ readQuery(client, "addons.listUpdates", {}, z22.array(AddonUpdateShape)),
6380
6085
  readInstallations(client),
6381
6086
  cliVerdict(ctx.cliVersion, ctx.fetch, deps.entryPath ?? process.argv[1] ?? "")
6382
6087
  ]);
@@ -6494,40 +6199,40 @@ function registerOpsTools(server, ctx) {
6494
6199
  "ops_whoami",
6495
6200
  "Which hub this MCP talks to and as whom, from the `camstack login` session (no network call). If there is no session, says to run `camstack login <host>` in a terminal.",
6496
6201
  {},
6497
- async () => text11(await whoami(ctx))
6202
+ async () => text(await whoami(ctx))
6498
6203
  );
6499
6204
  server.tool(
6500
6205
  "ops_status",
6501
6206
  'Cluster summary: every node (online, platform, server version, capability count, "online-but-capless" flag), the active alerts and the addons with an update. A part the hub did not answer is null with its reason in `unavailable`, never zero. Node list and updates need an admin `camstack login`.',
6502
6207
  {},
6503
- async () => text11(await opsStatus(ctx))
6208
+ async () => text(await opsStatus(ctx))
6504
6209
  );
6505
6210
  server.tool(
6506
6211
  "ops_device",
6507
6212
  "One device: online state, provider addon and node, bound capabilities, runtime-state age and, for a camera, its pipeline, recording state, the functions an operator switched off (not faults) and its latest object events.",
6508
- { deviceId: z25.number().int() },
6509
- async ({ deviceId }) => text11(await opsDevice(ctx, deviceId))
6213
+ { deviceId: z23.number().int() },
6214
+ async ({ deviceId }) => text(await opsDevice(ctx, deviceId))
6510
6215
  );
6511
6216
  server.tool(
6512
6217
  "ops_unhealthy_devices",
6513
6218
  "Every enabled device that is unhealthy, each with a typed reason (offline, detection-provisioning-failed, detection-not-assigned, recording-inactive). A function an operator switched off is never listed; a camera whose status could not be fully read is listed under `unknown`.",
6514
6219
  {},
6515
- async () => text11(await opsUnhealthyDevices(ctx))
6220
+ async () => text(await opsUnhealthyDevices(ctx))
6516
6221
  );
6517
6222
  server.tool(
6518
6223
  "ops_logs",
6519
6224
  "Hub logs scoped by deviceId, addon, node, level, window and free text, grouped and deduplicated with counts. Says which tier answered (ring = recent in-RAM; persisted = the default log destination). With `around` (a track or event id) returns `contextLines` lines on each side of its first mention. Needs an admin `camstack login`.",
6520
6225
  {
6521
- deviceId: z25.number().int().optional(),
6522
- addonId: z25.string().optional(),
6523
- nodeId: z25.string().optional(),
6524
- level: z25.enum(["debug", "info", "warn", "error"]).optional(),
6525
- windowMinutes: z25.number().int().min(1).max(1440).optional(),
6526
- text: z25.string().max(200).optional(),
6527
- around: z25.string().max(200).optional(),
6528
- contextLines: z25.number().int().min(1).max(50).optional()
6226
+ deviceId: z23.number().int().optional(),
6227
+ addonId: z23.string().optional(),
6228
+ nodeId: z23.string().optional(),
6229
+ level: z23.enum(["debug", "info", "warn", "error"]).optional(),
6230
+ windowMinutes: z23.number().int().min(1).max(1440).optional(),
6231
+ text: z23.string().max(200).optional(),
6232
+ around: z23.string().max(200).optional(),
6233
+ contextLines: z23.number().int().min(1).max(50).optional()
6529
6234
  },
6530
- async (args) => text11(
6235
+ async (args) => text(
6531
6236
  await opsLogs(ctx, {
6532
6237
  ...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
6533
6238
  ...args.addonId ? { addonId: args.addonId } : {},
@@ -6544,14 +6249,14 @@ function registerOpsTools(server, ctx) {
6544
6249
  "ops_events",
6545
6250
  "Find tracks (and, for one camera, object events) by camera, class, zone, time window and free text. Each result carries an admin-UI link and a viewer link built on the hub address flagged for app connections.",
6546
6251
  {
6547
- deviceId: z25.number().int().optional(),
6548
- className: z25.string().optional(),
6549
- zone: z25.string().optional(),
6550
- text: z25.string().max(200).optional(),
6551
- windowMinutes: z25.number().int().min(1).max(10080).optional(),
6552
- limit: z25.number().int().min(1).max(50).optional()
6252
+ deviceId: z23.number().int().optional(),
6253
+ className: z23.string().optional(),
6254
+ zone: z23.string().optional(),
6255
+ text: z23.string().max(200).optional(),
6256
+ windowMinutes: z23.number().int().min(1).max(10080).optional(),
6257
+ limit: z23.number().int().min(1).max(50).optional()
6553
6258
  },
6554
- async (args) => text11(
6259
+ async (args) => text(
6555
6260
  await opsEvents(ctx, {
6556
6261
  ...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
6557
6262
  ...args.className ? { className: args.className } : {},
@@ -6565,31 +6270,31 @@ function registerOpsTools(server, ctx) {
6565
6270
  server.tool(
6566
6271
  "ops_track",
6567
6272
  "One track with its media audit: best shot, keyframes, every missing media kind with the reason the track recorded for it, and the audit findings. Links to the admin UI and the viewer.",
6568
- { deviceId: z25.number().int(), trackId: z25.string().min(1) },
6569
- async ({ deviceId, trackId }) => text11(await opsTrack(ctx, { deviceId, trackId }))
6273
+ { deviceId: z23.number().int(), trackId: z23.string().min(1) },
6274
+ async ({ deviceId, trackId }) => text(await opsTrack(ctx, { deviceId, trackId }))
6570
6275
  );
6571
6276
  server.tool(
6572
6277
  "ops_accelerators",
6573
6278
  'Per node: the inference devices it has, the Auto-tune hardware class the node maps to, and whether the measured detector-latency table has a row for that class and accelerator. A node that could not be read is null with a reason, never "no accelerator". Needs an admin `camstack login`.',
6574
6279
  {},
6575
- async () => text11(await opsAccelerators(ctx))
6280
+ async () => text(await opsAccelerators(ctx))
6576
6281
  );
6577
6282
  server.tool(
6578
6283
  "ops_update_check",
6579
6284
  'Read-only update report for the whole cluster. Every node from the topology: running vs installed vs latest @camstack/server, restartRequired / updateState / rolledBack, the wrapper (Docker image / desktop app) verdict, and whether its addon versions match the hub; the hub\'s addon updates; the viewer (not exposed by any API, said so); and this CLI/MCP against npm. A node that cannot be read is null with a reason, never "up to date". Needs an admin `camstack login`.',
6580
6285
  {},
6581
- async () => text11(await opsUpdateCheck(ctx))
6286
+ async () => text(await opsUpdateCheck(ctx))
6582
6287
  );
6583
6288
  server.tool(
6584
6289
  UPDATE_APPLY_TOOL,
6585
6290
  "Bring the cluster up to date, verified step by step: hub server closure, wait until the hub runs it, then each online agent, then addons (agents follow by reconciliation, verified), then this CLI last. Two steps: call without confirm_token for a preview (order, versions, wrapper instructions, timeouts) and a one-shot token, then again with the SAME arguments and that token. A failed step stops the chain and names the node. Wrapper (image/app) updates are only reported with instructions. Needs an admin `camstack login`.",
6586
6291
  {
6587
- version: z25.string().min(1).optional().describe("Server target; default = the hub's latest."),
6588
- addons: z25.boolean().optional().describe("Update addons too (default true)."),
6589
- cli: z25.boolean().optional().describe("Update this CLI last (default true)."),
6590
- confirm_token: z25.string().optional()
6292
+ version: z23.string().min(1).optional().describe("Server target; default = the hub's latest."),
6293
+ addons: z23.boolean().optional().describe("Update addons too (default true)."),
6294
+ cli: z23.boolean().optional().describe("Update this CLI last (default true)."),
6295
+ confirm_token: z23.string().optional()
6591
6296
  },
6592
- async ({ version, addons, cli, confirm_token }) => text11(
6297
+ async ({ version, addons, cli, confirm_token }) => text(
6593
6298
  await opsUpdateApply(
6594
6299
  ctx,
6595
6300
  {
@@ -6604,7 +6309,7 @@ function registerOpsTools(server, ctx) {
6604
6309
  }
6605
6310
 
6606
6311
  // src/mcp/configure-tools.ts
6607
- var CurrentDeviceSchema = z26.object({ name: z26.string() }).nullable();
6312
+ var CurrentDeviceSchema = z24.object({ name: z24.string() }).nullable();
6608
6313
  var RENAME_DEVICE_TOOL = "configure_rename_device";
6609
6314
  function renameDeviceSpec(client) {
6610
6315
  return {
@@ -6666,11 +6371,11 @@ function registerConfigureTools(server, ctx) {
6666
6371
  RENAME_DEVICE_TOOL,
6667
6372
  "Rename a device. Two steps: call without confirm_token for a preview and a one-shot token, then call again with the SAME arguments and that token to apply. Needs an admin `camstack login` session.",
6668
6373
  {
6669
- deviceId: z26.number().int(),
6670
- name: z26.string().min(1),
6671
- confirm_token: z26.string().optional()
6374
+ deviceId: z24.number().int(),
6375
+ name: z24.string().min(1),
6376
+ confirm_token: z24.string().optional()
6672
6377
  },
6673
- async ({ deviceId, name, confirm_token }) => text12(await renameDevice(ctx, { deviceId, name }, confirm_token))
6378
+ async ({ deviceId, name, confirm_token }) => text(await renameDevice(ctx, { deviceId, name }, confirm_token))
6674
6379
  );
6675
6380
  registerAddonTools(server, ctx);
6676
6381
  registerCameraTools(server, ctx);
@@ -6679,11 +6384,10 @@ function registerConfigureTools(server, ctx) {
6679
6384
  }
6680
6385
 
6681
6386
  // src/mcp/support-tools.ts
6682
- import { text as text13 } from "@camstack/develop";
6683
- import { z as z31 } from "zod";
6387
+ import { z as z29 } from "zod";
6684
6388
 
6685
6389
  // src/mcp/support-accelerator.ts
6686
- import { z as z27 } from "zod";
6390
+ import { z as z25 } from "zod";
6687
6391
  var EXPECTED_ACCELERATORS = [
6688
6392
  "intel-igpu",
6689
6393
  "intel-npu",
@@ -6695,23 +6399,23 @@ var EXPECTED_ACCELERATORS = [
6695
6399
  "apple-ane",
6696
6400
  "nvidia"
6697
6401
  ];
6698
- var ExpectedAcceleratorSchema = z27.enum(EXPECTED_ACCELERATORS);
6699
- var HardwareSchema2 = z27.looseObject({
6700
- platform: z27.enum(["darwin", "linux", "win32"]),
6701
- arch: z27.enum(["arm64", "x64"]),
6702
- cpuModel: z27.string(),
6703
- gpu: z27.looseObject({ type: z27.string(), name: z27.string() }).nullable(),
6704
- npu: z27.looseObject({ type: z27.string() }).nullable()
6402
+ var ExpectedAcceleratorSchema = z25.enum(EXPECTED_ACCELERATORS);
6403
+ var HardwareSchema2 = z25.looseObject({
6404
+ platform: z25.enum(["darwin", "linux", "win32"]),
6405
+ arch: z25.enum(["arm64", "x64"]),
6406
+ cpuModel: z25.string(),
6407
+ gpu: z25.looseObject({ type: z25.string(), name: z25.string() }).nullable(),
6408
+ npu: z25.looseObject({ type: z25.string() }).nullable()
6705
6409
  });
6706
- var InferenceDevicesSchema2 = z27.looseObject({
6707
- reachable: z27.boolean(),
6708
- devices: z27.array(
6709
- z27.looseObject({
6710
- key: z27.string(),
6711
- available: z27.boolean(),
6712
- presence: z27.enum(["present", "absent", "unknown"]).optional(),
6713
- enabled: z27.boolean(),
6714
- exclusion: z27.string().nullable()
6410
+ var InferenceDevicesSchema2 = z25.looseObject({
6411
+ reachable: z25.boolean(),
6412
+ devices: z25.array(
6413
+ z25.looseObject({
6414
+ key: z25.string(),
6415
+ available: z25.boolean(),
6416
+ presence: z25.enum(["present", "absent", "unknown"]).optional(),
6417
+ enabled: z25.boolean(),
6418
+ exclusion: z25.string().nullable()
6715
6419
  })
6716
6420
  )
6717
6421
  });
@@ -6907,11 +6611,11 @@ function diagnoseAccelerator(input) {
6907
6611
  }
6908
6612
  const matched = devices.filter((d) => expectation.deviceKeys(d.key));
6909
6613
  const matchedKeys = matched.map((d) => d.key);
6910
- const ready2 = matched.filter((d) => d.available && d.enabled && d.exclusion === null);
6911
- if (ready2.length > 0) {
6614
+ const ready = matched.filter((d) => d.available && d.enabled && d.exclusion === null);
6615
+ if (ready.length > 0) {
6912
6616
  return finish(
6913
6617
  "ok",
6914
- `The node offers ${ready2.map((d) => d.key).join(", ")}, available and enabled.`,
6618
+ `The node offers ${ready.map((d) => d.key).join(", ")}, available and enabled.`,
6915
6619
  matchedKeys,
6916
6620
  [],
6917
6621
  null
@@ -6990,7 +6694,7 @@ async function supportDiagnoseAccelerator(ctx, args) {
6990
6694
  import {
6991
6695
  runGated as runGated5
6992
6696
  } from "@camstack/develop";
6993
- import { z as z28 } from "zod";
6697
+ import { z as z26 } from "zod";
6994
6698
  var BENCHMARK_RUN_TOOL = "support_benchmark_run";
6995
6699
  var AUTO_TUNE_MODELS = [
6996
6700
  "yolov9t-320",
@@ -7012,64 +6716,64 @@ var CONTRIBUTE_HOW = [
7012
6716
  "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.",
7013
6717
  '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.'
7014
6718
  ];
7015
- var HardwareSchema3 = z28.looseObject({
7016
- platform: z28.enum(["darwin", "linux", "win32"]),
7017
- arch: z28.enum(["arm64", "x64"]),
7018
- cpuModel: z28.string(),
7019
- cpuCores: z28.number(),
7020
- totalRAM_MB: z28.number(),
7021
- availableRAM_MB: z28.number(),
7022
- gpu: z28.looseObject({
7023
- type: z28.enum(["nvidia", "amd", "intel", "apple"]),
7024
- name: z28.string()
6719
+ var HardwareSchema3 = z26.looseObject({
6720
+ platform: z26.enum(["darwin", "linux", "win32"]),
6721
+ arch: z26.enum(["arm64", "x64"]),
6722
+ cpuModel: z26.string(),
6723
+ cpuCores: z26.number(),
6724
+ totalRAM_MB: z26.number(),
6725
+ availableRAM_MB: z26.number(),
6726
+ gpu: z26.looseObject({
6727
+ type: z26.enum(["nvidia", "amd", "intel", "apple"]),
6728
+ name: z26.string()
7025
6729
  }).nullable(),
7026
- npu: z28.looseObject({ type: z28.enum(["apple-ane", "intel-npu", "amd-xdna"]) }).nullable()
6730
+ npu: z26.looseObject({ type: z26.enum(["apple-ane", "intel-npu", "amd-xdna"]) }).nullable()
7027
6731
  });
7028
- var InferenceDevicesSchema3 = z28.looseObject({
7029
- reachable: z28.boolean(),
7030
- devices: z28.array(
7031
- z28.looseObject({
7032
- key: z28.string(),
7033
- backend: z28.string(),
7034
- available: z28.boolean(),
7035
- enabled: z28.boolean()
6732
+ var InferenceDevicesSchema3 = z26.looseObject({
6733
+ reachable: z26.boolean(),
6734
+ devices: z26.array(
6735
+ z26.looseObject({
6736
+ key: z26.string(),
6737
+ backend: z26.string(),
6738
+ available: z26.boolean(),
6739
+ enabled: z26.boolean()
7036
6740
  })
7037
6741
  )
7038
6742
  });
7039
- var RunStatusSchema = z28.looseObject({
7040
- run: z28.looseObject({
7041
- runId: z28.string(),
7042
- nodeId: z28.string(),
7043
- state: z28.string(),
7044
- endReason: z28.string().nullable().optional(),
7045
- error: z28.string().nullable().optional()
6743
+ var RunStatusSchema = z26.looseObject({
6744
+ run: z26.looseObject({
6745
+ runId: z26.string(),
6746
+ nodeId: z26.string(),
6747
+ state: z26.string(),
6748
+ endReason: z26.string().nullable().optional(),
6749
+ error: z26.string().nullable().optional()
7046
6750
  }).nullable()
7047
6751
  });
7048
- var ModelListSchema = z28.array(
7049
- z28.looseObject({
7050
- id: z28.string(),
7051
- formats: z28.record(z28.string(), z28.looseObject({ downloaded: z28.boolean().optional() })).optional()
6752
+ var ModelListSchema = z26.array(
6753
+ z26.looseObject({
6754
+ id: z26.string(),
6755
+ formats: z26.record(z26.string(), z26.looseObject({ downloaded: z26.boolean().optional() })).optional()
7052
6756
  })
7053
6757
  );
7054
- var PipelineTestSchema = z28.looseObject({
7055
- kind: z28.string().optional(),
7056
- detections: z28.array(
7057
- z28.looseObject({
7058
- debug: z28.looseObject({
7059
- stepLatencyMs: z28.record(z28.string(), z28.number()).optional()
6758
+ var PipelineTestSchema = z26.looseObject({
6759
+ kind: z26.string().optional(),
6760
+ detections: z26.array(
6761
+ z26.looseObject({
6762
+ debug: z26.looseObject({
6763
+ stepLatencyMs: z26.record(z26.string(), z26.number()).optional()
7060
6764
  }).optional()
7061
6765
  })
7062
6766
  ).optional(),
7063
- discarded: z28.array(z28.unknown()).optional(),
7064
- debug: z28.looseObject({
7065
- stepTimings: z28.array(
7066
- z28.looseObject({
7067
- source: z28.string().optional(),
7068
- ms: z28.number().optional()
6767
+ discarded: z26.array(z26.unknown()).optional(),
6768
+ debug: z26.looseObject({
6769
+ stepTimings: z26.array(
6770
+ z26.looseObject({
6771
+ source: z26.string().optional(),
6772
+ ms: z26.number().optional()
7069
6773
  })
7070
6774
  ).optional(),
7071
- predictMs: z28.number().optional(),
7072
- totalInferenceMs: z28.number().optional()
6775
+ predictMs: z26.number().optional(),
6776
+ totalInferenceMs: z26.number().optional()
7073
6777
  }).optional()
7074
6778
  });
7075
6779
  var FORMAT_BY_BACKEND = {
@@ -7356,15 +7060,15 @@ async function supportBenchmarkRun(ctx, deps, args, confirmToken) {
7356
7060
  }
7357
7061
  return runGated5(ctx.gate, benchmarkRunSpec(outcome.connection.client, deps), args, confirmToken);
7358
7062
  }
7359
- var ResultsSchema = z28.array(
7360
- z28.looseObject({
7361
- nodeId: z28.string(),
7362
- cell: z28.looseObject({
7363
- phase: z28.string(),
7364
- deviceKey: z28.string(),
7365
- modelId: z28.string(),
7366
- p50Ms: z28.number().optional(),
7367
- fps: z28.number().optional()
7063
+ var ResultsSchema = z26.array(
7064
+ z26.looseObject({
7065
+ nodeId: z26.string(),
7066
+ cell: z26.looseObject({
7067
+ phase: z26.string(),
7068
+ deviceKey: z26.string(),
7069
+ modelId: z26.string(),
7070
+ p50Ms: z26.number().optional(),
7071
+ fps: z26.number().optional()
7368
7072
  })
7369
7073
  })
7370
7074
  );
@@ -7417,20 +7121,20 @@ async function supportBenchmarkStatus(ctx, args) {
7417
7121
  }
7418
7122
 
7419
7123
  // src/mcp/support-camera.ts
7420
- import { z as z29 } from "zod";
7421
- var DeviceSchema2 = z29.looseObject({
7422
- id: z29.number(),
7423
- name: z29.string(),
7424
- addonId: z29.string().optional(),
7425
- disabled: z29.boolean().optional(),
7426
- online: z29.boolean().optional()
7124
+ import { z as z27 } from "zod";
7125
+ var DeviceSchema2 = z27.looseObject({
7126
+ id: z27.number(),
7127
+ name: z27.string(),
7128
+ addonId: z27.string().optional(),
7129
+ disabled: z27.boolean().optional(),
7130
+ online: z27.boolean().optional()
7427
7131
  }).nullable();
7428
7132
  var GENERIC = /onvif|rtsp/i;
7429
- function words(text14) {
7430
- return text14.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length >= 3);
7133
+ function words(text2) {
7134
+ return text2.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length >= 3);
7431
7135
  }
7432
- function isGeneric(parts2) {
7433
- return parts2.some((p) => GENERIC.test(p));
7136
+ function isGeneric(parts) {
7137
+ return parts.some((p) => GENERIC.test(p));
7434
7138
  }
7435
7139
  function installedPick(p) {
7436
7140
  return {
@@ -7451,9 +7155,9 @@ function availablePick(p) {
7451
7155
  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.";
7452
7156
  function matchIn(pools, tokens) {
7453
7157
  if (tokens.length === 0) return null;
7454
- const hit = (parts2) => {
7455
- if (isGeneric(parts2)) return false;
7456
- const joined = parts2.join(" ").toLowerCase();
7158
+ const hit = (parts) => {
7159
+ if (isGeneric(parts)) return false;
7160
+ const joined = parts.join(" ").toLowerCase();
7457
7161
  return tokens.some((t) => joined.includes(t));
7458
7162
  };
7459
7163
  const installed = pools.installed.find(
@@ -7589,10 +7293,10 @@ async function supportDiagnoseCamera(ctx, args) {
7589
7293
  }
7590
7294
 
7591
7295
  // src/mcp/support-deps.ts
7592
- import { execFile as execFile2 } from "child_process";
7296
+ import { execFile } from "child_process";
7593
7297
  var RUN_TIMEOUT_MS = 6e4;
7594
7298
  var spawnRunner = (command, args) => new Promise((resolve) => {
7595
- execFile2(
7299
+ execFile(
7596
7300
  command,
7597
7301
  [...args],
7598
7302
  { timeout: RUN_TIMEOUT_MS, maxBuffer: 1e6 },
@@ -7624,7 +7328,7 @@ import {
7624
7328
  digestArgs,
7625
7329
  runGated as runGated6
7626
7330
  } from "@camstack/develop";
7627
- import { z as z30 } from "zod";
7331
+ import { z as z28 } from "zod";
7628
7332
 
7629
7333
  // src/mcp/support-scrub.ts
7630
7334
  import { redactToolArgs, scrubSecrets } from "@camstack/develop";
@@ -7633,10 +7337,10 @@ var IPV6 = /\b(?:[0-9a-fA-F]{1,4}:){4,7}[0-9a-fA-F]{1,4}(?![\w:])|\b[0-9a-fA-F]{
7633
7337
  var MAC = /\b(?:[0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}\b/g;
7634
7338
  var HOME_DIR = /(\/Users|\/home)\/[^/\s'"]+/g;
7635
7339
  function octets(ip) {
7636
- const parts2 = ip.split(".").map((p) => Number(p));
7637
- if (parts2.length !== 4) return null;
7638
- if (parts2.some((p) => !Number.isInteger(p) || p < 0 || p > 255)) return null;
7639
- return parts2;
7340
+ const parts = ip.split(".").map((p) => Number(p));
7341
+ if (parts.length !== 4) return null;
7342
+ if (parts.some((p) => !Number.isInteger(p) || p < 0 || p > 255)) return null;
7343
+ return parts;
7640
7344
  }
7641
7345
  function maskIpv4(ip) {
7642
7346
  const o = octets(ip);
@@ -7677,16 +7381,16 @@ var INFERENCE_PACKAGE = "@camstack/addon-pipeline";
7677
7381
  var TITLE_MAX = 140;
7678
7382
  var DIAGNOSIS_MAX = 4e3;
7679
7383
  var DRAFT_KINDS = ["accelerator", "camera", "benchmark", "other"];
7680
- var DraftKindSchema = z30.enum(DRAFT_KINDS);
7681
- var BenchmarkCellsSchema = z30.record(
7682
- z30.string().regex(/^[a-z0-9][a-z0-9._-]{1,63}$/),
7683
- z30.object({ p50Ms: z30.number().positive(), fps: z30.number().positive() })
7384
+ var DraftKindSchema = z28.enum(DRAFT_KINDS);
7385
+ var BenchmarkCellsSchema = z28.record(
7386
+ z28.string().regex(/^[a-z0-9][a-z0-9._-]{1,63}$/),
7387
+ z28.object({ p50Ms: z28.number().positive(), fps: z28.number().positive() })
7684
7388
  );
7685
7389
  var PackageNameRe = /^(@[a-z0-9._-]+\/)?[a-z0-9._-]+$/;
7686
7390
  var GithubRepoRe = /github\.com[:/]([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?(?:[/#?].*)?$/;
7687
7391
  var ShorthandRe = /^(?:github:)?([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/;
7688
- var RegistryDocSchema = z30.looseObject({
7689
- repository: z30.union([z30.string(), z30.looseObject({ url: z30.string() })]).optional()
7392
+ var RegistryDocSchema = z28.looseObject({
7393
+ repository: z28.union([z28.string(), z28.looseObject({ url: z28.string() })]).optional()
7690
7394
  });
7691
7395
  function githubRepoOf(value) {
7692
7396
  const url = GithubRepoRe.exec(value);
@@ -7728,7 +7432,7 @@ async function resolveRepo(ctx, packageName) {
7728
7432
  reason: null
7729
7433
  };
7730
7434
  }
7731
- var ServerStatusSchema2 = z30.looseObject({ runningVersion: z30.string() });
7435
+ var ServerStatusSchema2 = z28.looseObject({ runningVersion: z28.string() });
7732
7436
  async function serverVersion(client, nodeId) {
7733
7437
  const status = await readQuery(
7734
7438
  client,
@@ -7843,11 +7547,11 @@ async function buildDraft(ctx, args, heading) {
7843
7547
  ].filter((s) => s !== "");
7844
7548
  return { target, title: cleanTitle(args.title), body: sections.join("\n\n") };
7845
7549
  }
7846
- function shellQuote2(value) {
7550
+ function shellQuote(value) {
7847
7551
  return `'${value.replace(/'/g, `'\\''`)}'`;
7848
7552
  }
7849
7553
  function issueCommand(draft, bodyFile) {
7850
- return `gh issue create --repo ${draft.target.repo} --title ${shellQuote2(draft.title)} --body-file ${bodyFile}`;
7554
+ return `gh issue create --repo ${draft.target.repo} --title ${shellQuote(draft.title)} --body-file ${bodyFile}`;
7851
7555
  }
7852
7556
  var ISSUE_BODY_FILE = "./camstack-issue.md";
7853
7557
  async function issueDraft(ctx, args) {
@@ -7930,8 +7634,8 @@ var BENCHMARK_FILES = [
7930
7634
  "docs/benchmarks/detector-latency.json",
7931
7635
  "packages/types/src/pipeline/auto-tune/detector-performance.ts"
7932
7636
  ];
7933
- function slug(text14) {
7934
- const s = text14.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
7637
+ function slug(text2) {
7638
+ const s = text2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
7935
7639
  return s === "" ? "change" : s;
7936
7640
  }
7937
7641
  function prProblem(args) {
@@ -7954,7 +7658,7 @@ function planFor(args, draft) {
7954
7658
  `Merge the measured cells into docs/benchmarks/detector-latency.json under "${args.column ?? ""}" (set "measured": true on the column when every kept cell was measured on the box) and mirror the same cells into DETECTOR_PERF in packages/types/src/pipeline/auto-tune/detector-performance.ts.`,
7955
7659
  "npx vitest run packages/types/src/pipeline/auto-tune/__tests__/detector-performance.spec.ts (it fails until the two files agree)."
7956
7660
  ] : ["Make the change with a spec that fails first, then passes."],
7957
- `git commit -am ${shellQuote2(benchmark ? `docs: measured detector latency for ${args.column ?? ""}` : `fix: ${draft.title}`)}`,
7661
+ `git commit -am ${shellQuote(benchmark ? `docs: measured detector latency for ${args.column ?? ""}` : `fix: ${draft.title}`)}`,
7958
7662
  `git push -u origin ${branch}`,
7959
7663
  `Save the body below as ${PR_BODY_FILE}, then run the command.`
7960
7664
  ];
@@ -7967,7 +7671,7 @@ function planFor(args, draft) {
7967
7671
  branch,
7968
7672
  files,
7969
7673
  steps,
7970
- command: `gh pr create --repo ${draft.target.repo} --head <your-github-user>:${branch} --title ${shellQuote2(draft.title)} --body-file ${PR_BODY_FILE}`,
7674
+ command: `gh pr create --repo ${draft.target.repo} --head <your-github-user>:${branch} --title ${shellQuote(draft.title)} --body-file ${PR_BODY_FILE}`,
7971
7675
  note: "This tool pushes no code and calls no GitHub API; you run these steps with your own git and gh."
7972
7676
  };
7973
7677
  }
@@ -7994,20 +7698,20 @@ async function supportDraftPr(ctx, args, confirmToken) {
7994
7698
  }
7995
7699
 
7996
7700
  // src/mcp/support-tools.ts
7997
- var ModelId = z31.string().regex(/^[a-z0-9][a-z0-9._-]{1,63}$/);
7701
+ var ModelId = z29.string().regex(/^[a-z0-9][a-z0-9._-]{1,63}$/);
7998
7702
  var DraftShape = {
7999
7703
  kind: DraftKindSchema,
8000
- title: z31.string().min(3).max(300),
8001
- summary: z31.string().min(3).max(4e3),
8002
- nodeId: z31.string().max(100).optional(),
7704
+ title: z29.string().min(3).max(300),
7705
+ summary: z29.string().min(3).max(4e3),
7706
+ nodeId: z29.string().max(100).optional(),
8003
7707
  expected: ExpectedAcceleratorSchema.optional(),
8004
- brand: z31.string().max(100).optional(),
8005
- model: z31.string().max(100).optional(),
8006
- deviceId: z31.number().int().optional(),
8007
- packageName: z31.string().max(214).optional(),
8008
- column: z31.string().max(100).optional(),
7708
+ brand: z29.string().max(100).optional(),
7709
+ model: z29.string().max(100).optional(),
7710
+ deviceId: z29.number().int().optional(),
7711
+ packageName: z29.string().max(214).optional(),
7712
+ column: z29.string().max(100).optional(),
8009
7713
  cells: BenchmarkCellsSchema.optional(),
8010
- confirm_token: z31.string().optional()
7714
+ confirm_token: z29.string().optional()
8011
7715
  };
8012
7716
  function draftArgs(input) {
8013
7717
  return {
@@ -8028,20 +7732,20 @@ function registerSupportTools(server, ctx, deps = defaultSupportDeps) {
8028
7732
  server.tool(
8029
7733
  "support_benchmark_status",
8030
7734
  "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`.",
8031
- { nodeId: z31.string().max(100).optional() },
8032
- async ({ nodeId }) => text13(await supportBenchmarkStatus(ctx, nodeId !== void 0 ? { nodeId } : {}))
7735
+ { nodeId: z29.string().max(100).optional() },
7736
+ async ({ nodeId }) => text(await supportBenchmarkStatus(ctx, nodeId !== void 0 ? { nodeId } : {}))
8033
7737
  );
8034
7738
  server.tool(
8035
7739
  BENCHMARK_RUN_TOOL,
8036
7740
  "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`.",
8037
7741
  {
8038
- nodeId: z31.string().min(1).max(100),
8039
- deviceKey: z31.string().min(1).max(60),
8040
- models: z31.array(ModelId).min(1).max(MAX_MODELS).optional(),
8041
- warmRuns: z31.number().int().min(1).max(MAX_WARM_RUNS).optional(),
8042
- confirm_token: z31.string().optional()
7742
+ nodeId: z29.string().min(1).max(100),
7743
+ deviceKey: z29.string().min(1).max(60),
7744
+ models: z29.array(ModelId).min(1).max(MAX_MODELS).optional(),
7745
+ warmRuns: z29.number().int().min(1).max(MAX_WARM_RUNS).optional(),
7746
+ confirm_token: z29.string().optional()
8043
7747
  },
8044
- async ({ nodeId, deviceKey, models, warmRuns, confirm_token }) => text13(
7748
+ async ({ nodeId, deviceKey, models, warmRuns, confirm_token }) => text(
8045
7749
  await supportBenchmarkRun(
8046
7750
  ctx,
8047
7751
  deps,
@@ -8058,18 +7762,18 @@ function registerSupportTools(server, ctx, deps = defaultSupportDeps) {
8058
7762
  server.tool(
8059
7763
  "support_diagnose_accelerator",
8060
7764
  "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.",
8061
- { nodeId: z31.string().min(1).max(100), expected: ExpectedAcceleratorSchema },
8062
- async ({ nodeId, expected }) => text13(await supportDiagnoseAccelerator(ctx, { nodeId, expected }))
7765
+ { nodeId: z29.string().min(1).max(100), expected: ExpectedAcceleratorSchema },
7766
+ async ({ nodeId, expected }) => text(await supportDiagnoseAccelerator(ctx, { nodeId, expected }))
8063
7767
  );
8064
7768
  server.tool(
8065
7769
  "support_diagnose_camera",
8066
7770
  "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.",
8067
7771
  {
8068
- brand: z31.string().max(100).optional(),
8069
- model: z31.string().max(100).optional(),
8070
- deviceId: z31.number().int().optional()
7772
+ brand: z29.string().max(100).optional(),
7773
+ model: z29.string().max(100).optional(),
7774
+ deviceId: z29.number().int().optional()
8071
7775
  },
8072
- async ({ brand, model, deviceId }) => text13(
7776
+ async ({ brand, model, deviceId }) => text(
8073
7777
  await supportDiagnoseCamera(ctx, {
8074
7778
  ...brand !== void 0 ? { brand } : {},
8075
7779
  ...model !== void 0 ? { model } : {},
@@ -8081,13 +7785,13 @@ function registerSupportTools(server, ctx, deps = defaultSupportDeps) {
8081
7785
  DRAFT_ISSUE_TOOL,
8082
7786
  "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.",
8083
7787
  DraftShape,
8084
- async (input) => text13(await supportDraftIssue(ctx, deps, draftArgs(input), input.confirm_token))
7788
+ async (input) => text(await supportDraftIssue(ctx, deps, draftArgs(input), input.confirm_token))
8085
7789
  );
8086
7790
  server.tool(
8087
7791
  DRAFT_PR_TOOL,
8088
7792
  '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.',
8089
7793
  DraftShape,
8090
- async (input) => text13(await supportDraftPr(ctx, draftArgs(input), input.confirm_token))
7794
+ async (input) => text(await supportDraftPr(ctx, draftArgs(input), input.confirm_token))
8091
7795
  );
8092
7796
  }
8093
7797