camstack 1.2.213 → 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.
- package/CHANGELOG.md +7 -0
- package/README.md +6 -0
- package/dist/chunk-37A556YF.js +822 -0
- package/dist/{chunk-MJJWVAEM.js → chunk-4Y3CBGOY.js} +53 -109
- package/dist/chunk-BYNP7S4W.js +56 -0
- package/dist/chunk-JPGJK5DC.js +75 -0
- package/dist/cli.js +127 -70
- package/dist/doctor-command-S7Q2U6HO.js +227 -0
- package/dist/{launcher-6TBFW332.js → launcher-JAOMC5YY.js} +716 -243
- package/dist/{server-EFXGQ7FD.js → server-TZCZTLBW.js} +215 -1007
- package/package.json +1 -1
|
@@ -13,13 +13,23 @@ import {
|
|
|
13
13
|
classifyInferenceHardware,
|
|
14
14
|
convertLegacyWhen,
|
|
15
15
|
countWhenLeaves,
|
|
16
|
-
findHubs,
|
|
17
16
|
generateAutomationBlock,
|
|
18
17
|
isReadingQuantity,
|
|
19
18
|
prepareRecipeUnits,
|
|
20
19
|
unitsForQuantity,
|
|
21
20
|
validateRecipeBounds
|
|
22
|
-
} from "./chunk-
|
|
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";
|
|
23
33
|
import "./chunk-LMMQX4CK.js";
|
|
24
34
|
|
|
25
35
|
// src/mcp/server.ts
|
|
@@ -29,14 +39,12 @@ import { MutationGate, tryConnectHub } from "@camstack/develop";
|
|
|
29
39
|
|
|
30
40
|
// src/mcp/configure-tools.ts
|
|
31
41
|
import {
|
|
32
|
-
runGated as runGated4
|
|
33
|
-
text as text13
|
|
42
|
+
runGated as runGated4
|
|
34
43
|
} from "@camstack/develop";
|
|
35
|
-
import { z as
|
|
44
|
+
import { z as z24 } from "zod";
|
|
36
45
|
|
|
37
46
|
// src/mcp/configure-automations.ts
|
|
38
47
|
import { z as z4 } from "zod";
|
|
39
|
-
import { text } from "@camstack/develop";
|
|
40
48
|
|
|
41
49
|
// src/mcp/configure-common.ts
|
|
42
50
|
import { runGated } from "@camstack/develop";
|
|
@@ -1236,7 +1244,6 @@ function registerAutomationTools(server, ctx) {
|
|
|
1236
1244
|
}
|
|
1237
1245
|
|
|
1238
1246
|
// src/mcp/configure-calibrate.ts
|
|
1239
|
-
import { text as text2 } from "@camstack/develop";
|
|
1240
1247
|
import { z as z5 } from "zod";
|
|
1241
1248
|
var CALIBRATE_APPLY_TOOL = "configure_calibrate_apply";
|
|
1242
1249
|
var WINDOW_MS = 24 * 36e5;
|
|
@@ -1602,7 +1609,7 @@ function registerCalibrateTools(server, ctx) {
|
|
|
1602
1609
|
"configure_calibrate_camera",
|
|
1603
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.",
|
|
1604
1611
|
{ deviceId: z5.number().int().nonnegative() },
|
|
1605
|
-
async ({ deviceId }) =>
|
|
1612
|
+
async ({ deviceId }) => text(await calibrateCamera(ctx, { deviceId }))
|
|
1606
1613
|
);
|
|
1607
1614
|
server.tool(
|
|
1608
1615
|
CALIBRATE_APPLY_TOOL,
|
|
@@ -1613,13 +1620,12 @@ function registerCalibrateTools(server, ctx) {
|
|
|
1613
1620
|
patch: z5.record(z5.string(), z5.union([z5.number(), z5.string(), z5.boolean()])),
|
|
1614
1621
|
confirm_token: ConfirmTokenSchema
|
|
1615
1622
|
},
|
|
1616
|
-
async ({ deviceId, cap: cap2, patch, confirm_token }) =>
|
|
1623
|
+
async ({ deviceId, cap: cap2, patch, confirm_token }) => text(await calibrateApply(ctx, { deviceId, cap: cap2, patch }, confirm_token))
|
|
1617
1624
|
);
|
|
1618
1625
|
}
|
|
1619
1626
|
|
|
1620
1627
|
// src/mcp/configure-zones-alarm.ts
|
|
1621
1628
|
import { randomUUID } from "crypto";
|
|
1622
|
-
import { text as text3 } from "@camstack/develop";
|
|
1623
1629
|
import { z as z6 } from "zod";
|
|
1624
1630
|
var ZONE_UPSERT_TOOL = "configure_zone_upsert";
|
|
1625
1631
|
var ALARM_SET_TOOL = "configure_alarm_set";
|
|
@@ -1910,7 +1916,7 @@ function registerZonesAlarmTools(server, ctx) {
|
|
|
1910
1916
|
"configure_zones",
|
|
1911
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.",
|
|
1912
1918
|
{ deviceId: z6.number().int().nonnegative() },
|
|
1913
|
-
async ({ deviceId }) =>
|
|
1919
|
+
async ({ deviceId }) => text(await readZonesTool(ctx, { deviceId }))
|
|
1914
1920
|
);
|
|
1915
1921
|
server.tool(
|
|
1916
1922
|
ZONE_UPSERT_TOOL,
|
|
@@ -1923,7 +1929,7 @@ function registerZonesAlarmTools(server, ctx) {
|
|
|
1923
1929
|
points: z6.array(PointSchema).min(2).max(64),
|
|
1924
1930
|
confirm_token: ConfirmTokenSchema
|
|
1925
1931
|
},
|
|
1926
|
-
async ({ deviceId, zoneId, name, kind, points, confirm_token }) =>
|
|
1932
|
+
async ({ deviceId, zoneId, name, kind, points, confirm_token }) => text(
|
|
1927
1933
|
await zoneUpsert(
|
|
1928
1934
|
ctx,
|
|
1929
1935
|
{
|
|
@@ -1941,7 +1947,7 @@ function registerZonesAlarmTools(server, ctx) {
|
|
|
1941
1947
|
"configure_alarm",
|
|
1942
1948
|
"Read the alarm panels of the hub: state, armed mode, the modes each accepts and whether it needs a PIN. Writes nothing.",
|
|
1943
1949
|
{ deviceId: z6.number().int().nonnegative().optional() },
|
|
1944
|
-
async ({ deviceId }) =>
|
|
1950
|
+
async ({ deviceId }) => text(await readAlarm(ctx, deviceId !== void 0 ? { deviceId } : {}))
|
|
1945
1951
|
);
|
|
1946
1952
|
server.tool(
|
|
1947
1953
|
ALARM_SET_TOOL,
|
|
@@ -1952,7 +1958,7 @@ function registerZonesAlarmTools(server, ctx) {
|
|
|
1952
1958
|
mode: AlarmArmModeSchema.optional(),
|
|
1953
1959
|
confirm_token: ConfirmTokenSchema
|
|
1954
1960
|
},
|
|
1955
|
-
async ({ deviceId, action, mode, confirm_token }) =>
|
|
1961
|
+
async ({ deviceId, action, mode, confirm_token }) => text(
|
|
1956
1962
|
await alarmSet(
|
|
1957
1963
|
ctx,
|
|
1958
1964
|
{ deviceId, action, ...mode !== void 0 ? { mode } : {} },
|
|
@@ -1963,7 +1969,6 @@ function registerZonesAlarmTools(server, ctx) {
|
|
|
1963
1969
|
}
|
|
1964
1970
|
|
|
1965
1971
|
// src/mcp/configure-network.ts
|
|
1966
|
-
import { text as text4 } from "@camstack/develop";
|
|
1967
1972
|
import { z as z7 } from "zod";
|
|
1968
1973
|
var REMOTE_ACCESS_SET_TOOL = "configure_remote_access_set";
|
|
1969
1974
|
var RemoteAddonSchema = z7.enum(["cloudflare-tunnel", "cloudflare-turn", "static-turn"]);
|
|
@@ -2147,7 +2152,7 @@ function registerNetworkTools(server, ctx) {
|
|
|
2147
2152
|
"configure_remote_access",
|
|
2148
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.",
|
|
2149
2154
|
{},
|
|
2150
|
-
async () =>
|
|
2155
|
+
async () => text(await readRemoteAccess(ctx))
|
|
2151
2156
|
);
|
|
2152
2157
|
server.tool(
|
|
2153
2158
|
REMOTE_ACCESS_SET_TOOL,
|
|
@@ -2157,12 +2162,11 @@ function registerNetworkTools(server, ctx) {
|
|
|
2157
2162
|
patch: z7.record(z7.string(), z7.union([z7.string(), z7.number(), z7.boolean()])),
|
|
2158
2163
|
confirm_token: ConfirmTokenSchema
|
|
2159
2164
|
},
|
|
2160
|
-
async ({ addonId, patch, confirm_token }) =>
|
|
2165
|
+
async ({ addonId, patch, confirm_token }) => text(await remoteAccessSet(ctx, { addonId, patch }, confirm_token))
|
|
2161
2166
|
);
|
|
2162
2167
|
}
|
|
2163
2168
|
|
|
2164
2169
|
// src/mcp/configure-exports.ts
|
|
2165
|
-
import { text as text5 } from "@camstack/develop";
|
|
2166
2170
|
import { z as z8 } from "zod";
|
|
2167
2171
|
var EXPORT_SET_TOOL = "configure_export_set";
|
|
2168
2172
|
var ProvidersSchema = z8.array(z8.looseObject({ addonId: z8.string() }));
|
|
@@ -2325,7 +2329,7 @@ function registerExportTools(server, ctx) {
|
|
|
2325
2329
|
"configure_exports",
|
|
2326
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.",
|
|
2327
2331
|
{ deviceId: z8.number().int().nonnegative().optional() },
|
|
2328
|
-
async ({ deviceId }) =>
|
|
2332
|
+
async ({ deviceId }) => text(await readExports(ctx, deviceId !== void 0 ? { deviceId } : {}))
|
|
2329
2333
|
);
|
|
2330
2334
|
server.tool(
|
|
2331
2335
|
EXPORT_SET_TOOL,
|
|
@@ -2336,18 +2340,17 @@ function registerExportTools(server, ctx) {
|
|
|
2336
2340
|
exposed: z8.boolean(),
|
|
2337
2341
|
confirm_token: ConfirmTokenSchema
|
|
2338
2342
|
},
|
|
2339
|
-
async ({ addonId, deviceId, exposed, confirm_token }) =>
|
|
2343
|
+
async ({ addonId, deviceId, exposed, confirm_token }) => text(await exportSet(ctx, { addonId, deviceId, exposed }, confirm_token))
|
|
2340
2344
|
);
|
|
2341
2345
|
server.tool(
|
|
2342
2346
|
"configure_ha_guide",
|
|
2343
2347
|
"Read-only guide for showing CamStack cameras in Home Assistant, including the HACS route. Needs no hub.",
|
|
2344
2348
|
{},
|
|
2345
|
-
async () =>
|
|
2349
|
+
async () => text({ status: "ok", guide: HA_GUIDE })
|
|
2346
2350
|
);
|
|
2347
2351
|
}
|
|
2348
2352
|
|
|
2349
2353
|
// src/mcp/configure-oidc.ts
|
|
2350
|
-
import { text as text6 } from "@camstack/develop";
|
|
2351
2354
|
import { z as z10 } from "zod";
|
|
2352
2355
|
|
|
2353
2356
|
// src/mcp/configure-oidc-providers.ts
|
|
@@ -2836,13 +2839,12 @@ function registerOidcTools(server, ctx) {
|
|
|
2836
2839
|
OIDC_TOOL,
|
|
2837
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.",
|
|
2838
2841
|
OIDC_TOOL_SHAPE,
|
|
2839
|
-
async ({ confirm_token, ...args }) =>
|
|
2842
|
+
async ({ confirm_token, ...args }) => text(await configureOidc(ctx, args, confirm_token))
|
|
2840
2843
|
);
|
|
2841
2844
|
}
|
|
2842
2845
|
|
|
2843
2846
|
// src/mcp/configure-nc-rules.ts
|
|
2844
2847
|
import { z as z11 } from "zod";
|
|
2845
|
-
import { text as text7 } from "@camstack/develop";
|
|
2846
2848
|
var TIME = /^\d{1,2}:\d{2}$/;
|
|
2847
2849
|
var ALL_DAYS = [0, 1, 2, 3, 4, 5, 6];
|
|
2848
2850
|
var QuietHoursAnswerSchema = z11.object({
|
|
@@ -3188,7 +3190,7 @@ function registerNcRuleTools(server, ctx) {
|
|
|
3188
3190
|
"configure_notification_rule_questionnaire",
|
|
3189
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.",
|
|
3190
3192
|
{ answers: NcRuleAnswersSchema },
|
|
3191
|
-
async ({ answers }) =>
|
|
3193
|
+
async ({ answers }) => text(await ncRuleQuestionnaire(ctx, answers))
|
|
3192
3194
|
);
|
|
3193
3195
|
server.tool(
|
|
3194
3196
|
SAVE_RULE_TOOL,
|
|
@@ -3198,7 +3200,7 @@ function registerNcRuleTools(server, ctx) {
|
|
|
3198
3200
|
ruleId: z11.string().optional(),
|
|
3199
3201
|
confirm_token: ConfirmTokenSchema
|
|
3200
3202
|
},
|
|
3201
|
-
async ({ rule, ruleId, confirm_token }) =>
|
|
3203
|
+
async ({ rule, ruleId, confirm_token }) => text(
|
|
3202
3204
|
await saveRule(ctx, { rule, ...ruleId !== void 0 ? { ruleId } : {} }, confirm_token)
|
|
3203
3205
|
)
|
|
3204
3206
|
);
|
|
@@ -3216,9 +3218,6 @@ function registerConfigureP3bTools(server, ctx) {
|
|
|
3216
3218
|
}
|
|
3217
3219
|
|
|
3218
3220
|
// src/mcp/configure-addons.ts
|
|
3219
|
-
import {
|
|
3220
|
-
text as text8
|
|
3221
|
-
} from "@camstack/develop";
|
|
3222
3221
|
import { z as z13 } from "zod";
|
|
3223
3222
|
|
|
3224
3223
|
// src/mcp/configure-support.ts
|
|
@@ -3873,7 +3872,7 @@ function registerAddonTools(server, ctx) {
|
|
|
3873
3872
|
query: z13.string().max(100).optional(),
|
|
3874
3873
|
capability: z13.string().max(100).optional()
|
|
3875
3874
|
},
|
|
3876
|
-
async ({ query, capability }) =>
|
|
3875
|
+
async ({ query, capability }) => text(
|
|
3877
3876
|
await configureAddonsSearch(ctx, {
|
|
3878
3877
|
...query !== void 0 ? { query } : {},
|
|
3879
3878
|
...capability !== void 0 ? { capability } : {}
|
|
@@ -3888,7 +3887,7 @@ function registerAddonTools(server, ctx) {
|
|
|
3888
3887
|
version: z13.string().max(64).optional(),
|
|
3889
3888
|
confirm_token: z13.string().optional()
|
|
3890
3889
|
},
|
|
3891
|
-
async ({ packageName, version, confirm_token }) =>
|
|
3890
|
+
async ({ packageName, version, confirm_token }) => text(
|
|
3892
3891
|
await installAddon(
|
|
3893
3892
|
ctx,
|
|
3894
3893
|
{ packageName, ...version !== void 0 ? { version } : {} },
|
|
@@ -3905,7 +3904,7 @@ function registerAddonTools(server, ctx) {
|
|
|
3905
3904
|
nodeId: z13.string().max(100).optional(),
|
|
3906
3905
|
confirm_token: z13.string().optional()
|
|
3907
3906
|
},
|
|
3908
|
-
async ({ packageName, version, nodeId, confirm_token }) =>
|
|
3907
|
+
async ({ packageName, version, nodeId, confirm_token }) => text(
|
|
3909
3908
|
await updateAddon(
|
|
3910
3909
|
ctx,
|
|
3911
3910
|
{
|
|
@@ -3926,7 +3925,7 @@ function registerAddonTools(server, ctx) {
|
|
|
3926
3925
|
everywhere: z13.boolean().optional(),
|
|
3927
3926
|
confirm_token: z13.string().optional()
|
|
3928
3927
|
},
|
|
3929
|
-
async ({ packageName, nodeId, everywhere, confirm_token }) =>
|
|
3928
|
+
async ({ packageName, nodeId, everywhere, confirm_token }) => text(
|
|
3930
3929
|
await uninstallAddon(
|
|
3931
3930
|
ctx,
|
|
3932
3931
|
{
|
|
@@ -3941,9 +3940,6 @@ function registerAddonTools(server, ctx) {
|
|
|
3941
3940
|
}
|
|
3942
3941
|
|
|
3943
3942
|
// src/mcp/configure-cameras.ts
|
|
3944
|
-
import {
|
|
3945
|
-
text as text9
|
|
3946
|
-
} from "@camstack/develop";
|
|
3947
3943
|
import { z as z14 } from "zod";
|
|
3948
3944
|
var UNSUPPORTED_HINT = "use support_diagnose_camera / support_draft_issue";
|
|
3949
3945
|
var MAX_PROVIDERS = 40;
|
|
@@ -4270,7 +4266,7 @@ function registerCameraTools(server, ctx) {
|
|
|
4270
4266
|
"configure_camera_providers",
|
|
4271
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.",
|
|
4272
4268
|
{ query: z14.string().max(100).optional() },
|
|
4273
|
-
async ({ query }) =>
|
|
4269
|
+
async ({ query }) => text(await configureCameraProviders(ctx, query))
|
|
4274
4270
|
);
|
|
4275
4271
|
server.tool(
|
|
4276
4272
|
"configure_camera_discover",
|
|
@@ -4279,7 +4275,7 @@ function registerCameraTools(server, ctx) {
|
|
|
4279
4275
|
addonId: z14.string().min(1),
|
|
4280
4276
|
params: z14.record(z14.string(), z14.unknown()).optional()
|
|
4281
4277
|
},
|
|
4282
|
-
async ({ addonId, params }) =>
|
|
4278
|
+
async ({ addonId, params }) => text(
|
|
4283
4279
|
await configureCameraDiscover(ctx, {
|
|
4284
4280
|
addonId,
|
|
4285
4281
|
...params !== void 0 ? { params } : {}
|
|
@@ -4294,14 +4290,11 @@ function registerCameraTools(server, ctx) {
|
|
|
4294
4290
|
config: z14.record(z14.string(), z14.unknown()),
|
|
4295
4291
|
confirm_token: z14.string().optional()
|
|
4296
4292
|
},
|
|
4297
|
-
async ({ addonId, config, confirm_token }) =>
|
|
4293
|
+
async ({ addonId, config, confirm_token }) => text(await addCamera(ctx, { addonId, config }, confirm_token))
|
|
4298
4294
|
);
|
|
4299
4295
|
}
|
|
4300
4296
|
|
|
4301
4297
|
// src/mcp/configure-notifiers.ts
|
|
4302
|
-
import {
|
|
4303
|
-
text as text10
|
|
4304
|
-
} from "@camstack/develop";
|
|
4305
4298
|
import { z as z15 } from "zod";
|
|
4306
4299
|
var MAX_TARGETS = 50;
|
|
4307
4300
|
var TargetKindShape = z15.looseObject({
|
|
@@ -4604,13 +4597,13 @@ function registerNotifierTools(server, ctx) {
|
|
|
4604
4597
|
"configure_notifiers",
|
|
4605
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`.",
|
|
4606
4599
|
{},
|
|
4607
|
-
async () =>
|
|
4600
|
+
async () => text(await configureNotifiers(ctx))
|
|
4608
4601
|
);
|
|
4609
4602
|
server.tool(
|
|
4610
4603
|
"configure_notification_targets",
|
|
4611
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`.",
|
|
4612
4605
|
{},
|
|
4613
|
-
async () =>
|
|
4606
|
+
async () => text(await configureNotificationTargets(ctx))
|
|
4614
4607
|
);
|
|
4615
4608
|
server.tool(
|
|
4616
4609
|
UPSERT_TARGET_TOOL,
|
|
@@ -4623,7 +4616,7 @@ function registerNotifierTools(server, ctx) {
|
|
|
4623
4616
|
enabled: z15.boolean().optional(),
|
|
4624
4617
|
confirm_token: z15.string().optional()
|
|
4625
4618
|
},
|
|
4626
|
-
async ({ targetId, kind, name, config, enabled, confirm_token }) =>
|
|
4619
|
+
async ({ targetId, kind, name, config, enabled, confirm_token }) => text(
|
|
4627
4620
|
await upsertNotificationTarget(
|
|
4628
4621
|
ctx,
|
|
4629
4622
|
{
|
|
@@ -4641,13 +4634,13 @@ function registerNotifierTools(server, ctx) {
|
|
|
4641
4634
|
TEST_TARGET_TOOL,
|
|
4642
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.",
|
|
4643
4636
|
{ targetId: z15.string().min(1), confirm_token: z15.string().optional() },
|
|
4644
|
-
async ({ targetId, confirm_token }) =>
|
|
4637
|
+
async ({ targetId, confirm_token }) => text(await testNotificationTarget(ctx, { targetId }, confirm_token))
|
|
4645
4638
|
);
|
|
4646
4639
|
}
|
|
4647
4640
|
|
|
4648
4641
|
// src/mcp/ops-tools.ts
|
|
4649
|
-
import { z as
|
|
4650
|
-
import { jwtClaims
|
|
4642
|
+
import { z as z23 } from "zod";
|
|
4643
|
+
import { jwtClaims } from "@camstack/develop";
|
|
4651
4644
|
|
|
4652
4645
|
// src/mcp/ops-accelerators.ts
|
|
4653
4646
|
import { z as z16 } from "zod";
|
|
@@ -5540,7 +5533,7 @@ async function opsStatus(ctx) {
|
|
|
5540
5533
|
import {
|
|
5541
5534
|
runGated as runGated3
|
|
5542
5535
|
} from "@camstack/develop";
|
|
5543
|
-
import { z as
|
|
5536
|
+
import { z as z21 } from "zod";
|
|
5544
5537
|
|
|
5545
5538
|
// src/mcp/ops-update-model.ts
|
|
5546
5539
|
import { z as z20 } from "zod";
|
|
@@ -5713,790 +5706,6 @@ function addonDrift(installations) {
|
|
|
5713
5706
|
return drift;
|
|
5714
5707
|
}
|
|
5715
5708
|
|
|
5716
|
-
// src/mcp/ops-update-cli.ts
|
|
5717
|
-
import { execFile } from "child_process";
|
|
5718
|
-
|
|
5719
|
-
// src/mcp/setup-tools.ts
|
|
5720
|
-
import { text as text11 } from "@camstack/develop";
|
|
5721
|
-
import { z as z24 } from "zod";
|
|
5722
|
-
|
|
5723
|
-
// src/mcp/setup-hubs.ts
|
|
5724
|
-
import * as https from "https";
|
|
5725
|
-
import {
|
|
5726
|
-
createHubAgent,
|
|
5727
|
-
explainTlsFailure,
|
|
5728
|
-
HubAddressError,
|
|
5729
|
-
hubCaFileForServer,
|
|
5730
|
-
loadSession,
|
|
5731
|
-
normalizeHubAddress,
|
|
5732
|
-
resolveHubTls
|
|
5733
|
-
} from "@camstack/develop";
|
|
5734
|
-
import { z as z23 } from "zod";
|
|
5735
|
-
|
|
5736
|
-
// src/mcp/setup-install.ts
|
|
5737
|
-
import { z as z22 } from "zod";
|
|
5738
|
-
|
|
5739
|
-
// src/mcp/setup-install-rules.ts
|
|
5740
|
-
import { z as z21 } from "zod";
|
|
5741
|
-
var HOST_KINDS = [
|
|
5742
|
-
"docker-compose",
|
|
5743
|
-
"docker",
|
|
5744
|
-
"unraid",
|
|
5745
|
-
"proxmox-lxc",
|
|
5746
|
-
"macos-desktop"
|
|
5747
|
-
];
|
|
5748
|
-
var HostKindSchema = z21.enum(HOST_KINDS);
|
|
5749
|
-
var ROLES = ["hub", "agent"];
|
|
5750
|
-
var RoleSchema = z21.enum(ROLES);
|
|
5751
|
-
var ACCELERATORS = ["intel", "amd", "coral", "apple", "cpu"];
|
|
5752
|
-
var AcceleratorSchema = z21.enum(ACCELERATORS);
|
|
5753
|
-
var InstallPathsSchema = z21.object({
|
|
5754
|
-
data: z21.string().min(1),
|
|
5755
|
-
recordings: z21.string().min(1),
|
|
5756
|
-
config: z21.string().min(1).optional(),
|
|
5757
|
-
backups: z21.string().min(1).optional()
|
|
5758
|
-
});
|
|
5759
|
-
var InstallGuideInputSchema = z21.object({
|
|
5760
|
-
hostKind: HostKindSchema.optional(),
|
|
5761
|
-
role: RoleSchema.optional(),
|
|
5762
|
-
accelerator: AcceleratorSchema.optional(),
|
|
5763
|
-
paths: InstallPathsSchema.optional(),
|
|
5764
|
-
/** Agents only: the hub the agent should dial (host or IP). */
|
|
5765
|
-
hubAddress: z21.string().min(1).optional()
|
|
5766
|
-
});
|
|
5767
|
-
var RAW = "https://raw.githubusercontent.com/camstack";
|
|
5768
|
-
var TEMPLATE_URLS = {
|
|
5769
|
-
compose: {
|
|
5770
|
-
hub: `${RAW}/proxmox/main/compose/hub.yml`,
|
|
5771
|
-
agent: `${RAW}/proxmox/main/compose/agent.yml`
|
|
5772
|
-
},
|
|
5773
|
-
proxmoxInstaller: `${RAW}/proxmox/main/install-camstack-proxmox.sh`,
|
|
5774
|
-
unraid: {
|
|
5775
|
-
hub: `${RAW}/unraid-templates/main/templates/camstack.xml`,
|
|
5776
|
-
agent: `${RAW}/unraid-templates/main/templates/camstack-agent.xml`
|
|
5777
|
-
},
|
|
5778
|
-
latestRelease: "https://api.github.com/repos/camstack/server/releases/latest"
|
|
5779
|
-
};
|
|
5780
|
-
var IMAGE_REPOSITORY = "ghcr.io/camstack/camstack";
|
|
5781
|
-
var IMAGE_TAG_BY_ACCELERATOR = {
|
|
5782
|
-
intel: "intel-latest",
|
|
5783
|
-
amd: "amd-latest",
|
|
5784
|
-
// No Coral- or CPU-specific image: Intel hosts and the Unraid templates track intel-latest.
|
|
5785
|
-
coral: "intel-latest",
|
|
5786
|
-
cpu: "intel-latest",
|
|
5787
|
-
apple: "intel-latest"
|
|
5788
|
-
};
|
|
5789
|
-
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.";
|
|
5790
|
-
var MUST_NOT_OMIT = [
|
|
5791
|
-
"--pids-limit=8192 (pids_limit: 8192): the default 2048 counts threads and silently stops media on a busy hub.",
|
|
5792
|
-
"CAMSTACK_MEDIA_ROOT=/recordings: without it recordings land in /data and fill the cache drive.",
|
|
5793
|
-
"/config mapped: it is a VOLUME, so an unmapped path mints an anonymous volume on every re-create.",
|
|
5794
|
-
"--restart unless-stopped: the NVR must come back after a host reboot.",
|
|
5795
|
-
"CAMSTACK_MOLECULER_USE_HOSTNAME=false: otherwise a multi-machine cluster never forms.",
|
|
5796
|
-
"Coral USB: /dev/bus/usb AND the device-cgroup rule c 189:* rmw."
|
|
5797
|
-
];
|
|
5798
|
-
var COMPOSE_MARKERS_BY_ACCELERATOR = {
|
|
5799
|
-
intel: ["devices", "dri", "accel"],
|
|
5800
|
-
amd: ["devices", "dri"],
|
|
5801
|
-
coral: ["devices", "usb"],
|
|
5802
|
-
cpu: [],
|
|
5803
|
-
apple: []
|
|
5804
|
-
};
|
|
5805
|
-
function firstRunHub(host) {
|
|
5806
|
-
return [
|
|
5807
|
-
`Open https://${host}:4443 (a self-signed certificate: accept the browser warning).`,
|
|
5808
|
-
"Sign in as admin / changeme; CamStack asks for a new password at once. Change it now.",
|
|
5809
|
-
`Then, where your MCP client runs: camstack login ${host}`
|
|
5810
|
-
];
|
|
5811
|
-
}
|
|
5812
|
-
|
|
5813
|
-
// src/mcp/setup-compose.ts
|
|
5814
|
-
var MARKER_LINE = /^(\s*)# (.*#@([A-Za-z0-9_]+)\s*)$/;
|
|
5815
|
-
var VOLUME_HOST_PATHS = [
|
|
5816
|
-
{ container: "/data", key: "data" },
|
|
5817
|
-
{ container: "/recordings", key: "recordings" },
|
|
5818
|
-
{ container: "/config", key: "config" },
|
|
5819
|
-
{ container: "/backups", key: "backups" }
|
|
5820
|
-
];
|
|
5821
|
-
function swapImageTag(body, imageTag) {
|
|
5822
|
-
const escaped = IMAGE_REPOSITORY.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5823
|
-
return body.replace(new RegExp(`(${escaped}):[A-Za-z0-9._-]+`), `$1:${imageTag}`);
|
|
5824
|
-
}
|
|
5825
|
-
function enableMarkers(body, markers) {
|
|
5826
|
-
return body.split("\n").map((line) => {
|
|
5827
|
-
const match = MARKER_LINE.exec(line);
|
|
5828
|
-
if (match === null) return line;
|
|
5829
|
-
const tag = match[3];
|
|
5830
|
-
return tag !== void 0 && markers.includes(tag) ? `${match[1] ?? ""}${match[2] ?? ""}` : line;
|
|
5831
|
-
}).join("\n");
|
|
5832
|
-
}
|
|
5833
|
-
function setVolumeHosts(body, paths) {
|
|
5834
|
-
const edits = [];
|
|
5835
|
-
let out = body;
|
|
5836
|
-
for (const { container, key } of VOLUME_HOST_PATHS) {
|
|
5837
|
-
const wanted = paths?.[key];
|
|
5838
|
-
const volume = new RegExp(`^(\\s*- )([^\\s:]+)(:${container}\\s*)$`, "m");
|
|
5839
|
-
if (!volume.test(out)) {
|
|
5840
|
-
if (wanted !== void 0) {
|
|
5841
|
-
edits.push(
|
|
5842
|
-
`${container}: no volume line found in the template; map ${wanted} to it yourself`
|
|
5843
|
-
);
|
|
5844
|
-
}
|
|
5845
|
-
continue;
|
|
5846
|
-
}
|
|
5847
|
-
if (wanted === void 0) {
|
|
5848
|
-
edits.push(
|
|
5849
|
-
`${container}: left at the template's host path, change it if that is not where you want it`
|
|
5850
|
-
);
|
|
5851
|
-
continue;
|
|
5852
|
-
}
|
|
5853
|
-
out = out.replace(
|
|
5854
|
-
volume,
|
|
5855
|
-
(_m, pre, _host, post) => `${pre}${wanted}${post}`
|
|
5856
|
-
);
|
|
5857
|
-
}
|
|
5858
|
-
return { body: out, edits };
|
|
5859
|
-
}
|
|
5860
|
-
function setHubAddress(body, hubAddress) {
|
|
5861
|
-
const line = /^(\s*- CAMSTACK_HUB_ADDRESS=).*$/m;
|
|
5862
|
-
if (!line.test(body)) return { body, edits: [] };
|
|
5863
|
-
if (hubAddress === void 0) {
|
|
5864
|
-
return {
|
|
5865
|
-
body,
|
|
5866
|
-
edits: [
|
|
5867
|
-
"CAMSTACK_HUB_ADDRESS: export it before `docker compose up` (or edit the line) with the hub IP or hostname"
|
|
5868
|
-
]
|
|
5869
|
-
};
|
|
5870
|
-
}
|
|
5871
|
-
return {
|
|
5872
|
-
body: body.replace(line, (_m, pre) => `${pre}${hubAddress}`),
|
|
5873
|
-
edits: []
|
|
5874
|
-
};
|
|
5875
|
-
}
|
|
5876
|
-
function customizeCompose(source, c) {
|
|
5877
|
-
const tagged = enableMarkers(swapImageTag(source, c.imageTag), c.markers);
|
|
5878
|
-
const volumes = setVolumeHosts(tagged, c.paths);
|
|
5879
|
-
const hub = setHubAddress(volumes.body, c.hubAddress);
|
|
5880
|
-
return { body: hub.body, edits: [...volumes.edits, ...hub.edits] };
|
|
5881
|
-
}
|
|
5882
|
-
function unquote(value) {
|
|
5883
|
-
const v = value.trim();
|
|
5884
|
-
if (v.length >= 2 && (v.startsWith('"') || v.startsWith("'")) && v.endsWith(v[0] ?? "")) {
|
|
5885
|
-
return v.slice(1, -1);
|
|
5886
|
-
}
|
|
5887
|
-
return v;
|
|
5888
|
-
}
|
|
5889
|
-
function stripComment(line) {
|
|
5890
|
-
return line.replace(/\s+#.*$/, "");
|
|
5891
|
-
}
|
|
5892
|
-
function readService(body) {
|
|
5893
|
-
const fields = {
|
|
5894
|
-
scalars: /* @__PURE__ */ new Map(),
|
|
5895
|
-
lists: /* @__PURE__ */ new Map(),
|
|
5896
|
-
maps: /* @__PURE__ */ new Map()
|
|
5897
|
-
};
|
|
5898
|
-
let key = null;
|
|
5899
|
-
let sub = null;
|
|
5900
|
-
for (const raw of body.split("\n")) {
|
|
5901
|
-
if (raw.trim() === "" || raw.trim().startsWith("#")) continue;
|
|
5902
|
-
const line = stripComment(raw);
|
|
5903
|
-
const indent = line.length - line.trimStart().length;
|
|
5904
|
-
const text15 = line.trim();
|
|
5905
|
-
if (indent === 4) {
|
|
5906
|
-
sub = null;
|
|
5907
|
-
const m = /^([A-Za-z_]+):\s*(.*)$/.exec(text15);
|
|
5908
|
-
if (m === null) continue;
|
|
5909
|
-
key = m[1] ?? null;
|
|
5910
|
-
if (key !== null && (m[2] ?? "") !== "") fields.scalars.set(key, unquote(m[2] ?? ""));
|
|
5911
|
-
} else if (indent === 6 && key !== null) {
|
|
5912
|
-
if (text15.startsWith("- ")) {
|
|
5913
|
-
const list = fields.lists.get(key) ?? [];
|
|
5914
|
-
list.push(unquote(text15.slice(2)));
|
|
5915
|
-
fields.lists.set(key, list);
|
|
5916
|
-
} else {
|
|
5917
|
-
const m = /^([A-Za-z_-]+):\s*(.*)$/.exec(text15);
|
|
5918
|
-
if (m === null) continue;
|
|
5919
|
-
sub = m[1] ?? null;
|
|
5920
|
-
const map = fields.maps.get(key) ?? /* @__PURE__ */ new Map();
|
|
5921
|
-
if ((m[2] ?? "") !== "" && sub !== null) map.set(sub, unquote(m[2] ?? ""));
|
|
5922
|
-
fields.maps.set(key, map);
|
|
5923
|
-
}
|
|
5924
|
-
} else if (indent === 8 && key !== null && sub !== null) {
|
|
5925
|
-
const m = /^([A-Za-z_-]+):\s*(.*)$/.exec(text15);
|
|
5926
|
-
if (m === null) continue;
|
|
5927
|
-
const map = fields.maps.get(`${key}.${sub}`) ?? /* @__PURE__ */ new Map();
|
|
5928
|
-
map.set(m[1] ?? "", unquote(m[2] ?? ""));
|
|
5929
|
-
fields.maps.set(`${key}.${sub}`, map);
|
|
5930
|
-
}
|
|
5931
|
-
}
|
|
5932
|
-
return fields;
|
|
5933
|
-
}
|
|
5934
|
-
function shellQuote(value) {
|
|
5935
|
-
return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
|
|
5936
|
-
}
|
|
5937
|
-
function composeToDockerRun(body) {
|
|
5938
|
-
const f = readService(body);
|
|
5939
|
-
const image = f.scalars.get("image");
|
|
5940
|
-
if (image === void 0) return null;
|
|
5941
|
-
const args = ["docker run -d"];
|
|
5942
|
-
const name = f.scalars.get("container_name");
|
|
5943
|
-
if (name !== void 0) args.push(`--name ${shellQuote(name)}`);
|
|
5944
|
-
const restart = f.scalars.get("restart");
|
|
5945
|
-
if (restart !== void 0) args.push(`--restart ${shellQuote(restart)}`);
|
|
5946
|
-
const network = f.scalars.get("network_mode");
|
|
5947
|
-
if (network !== void 0) args.push(`--network ${shellQuote(network)}`);
|
|
5948
|
-
const pids = f.scalars.get("pids_limit");
|
|
5949
|
-
if (pids !== void 0) args.push(`--pids-limit=${pids}`);
|
|
5950
|
-
for (const d of f.lists.get("devices") ?? []) args.push(`--device ${shellQuote(d)}`);
|
|
5951
|
-
for (const r of f.lists.get("device_cgroup_rules") ?? []) {
|
|
5952
|
-
args.push(`--device-cgroup-rule ${shellQuote(r)}`);
|
|
5953
|
-
}
|
|
5954
|
-
for (const g of f.lists.get("group_add") ?? []) args.push(`--group-add ${shellQuote(g)}`);
|
|
5955
|
-
for (const v of f.lists.get("volumes") ?? []) args.push(`-v ${shellQuote(v)}`);
|
|
5956
|
-
for (const e of f.lists.get("environment") ?? []) {
|
|
5957
|
-
if (!e.includes("${")) args.push(`-e ${shellQuote(e)}`);
|
|
5958
|
-
}
|
|
5959
|
-
const driver = f.maps.get("logging")?.get("driver");
|
|
5960
|
-
if (driver !== void 0) args.push(`--log-driver ${shellQuote(driver)}`);
|
|
5961
|
-
for (const [k, v] of f.maps.get("logging.options") ?? []) {
|
|
5962
|
-
args.push(`--log-opt ${shellQuote(`${k}=${v}`)}`);
|
|
5963
|
-
}
|
|
5964
|
-
args.push(shellQuote(image));
|
|
5965
|
-
return args.join(" \\\n ");
|
|
5966
|
-
}
|
|
5967
|
-
|
|
5968
|
-
// src/mcp/setup-install.ts
|
|
5969
|
-
var FETCH_TIMEOUT_MS = 1e4;
|
|
5970
|
-
var CONTAINER_ROUTES = ["docker-compose", "docker"];
|
|
5971
|
-
var Q_HOST = {
|
|
5972
|
-
id: "hostKind",
|
|
5973
|
-
question: "Where will CamStack run?",
|
|
5974
|
-
options: ["docker-compose", "docker", "unraid", "proxmox-lxc", "macos-desktop"]
|
|
5975
|
-
};
|
|
5976
|
-
var Q_ROLE = {
|
|
5977
|
-
id: "role",
|
|
5978
|
-
question: "Which role? Start with a hub; an agent only adds a second machine to an existing hub.",
|
|
5979
|
-
options: ["hub", "agent"]
|
|
5980
|
-
};
|
|
5981
|
-
var Q_ACCEL = {
|
|
5982
|
-
id: "accelerator",
|
|
5983
|
-
question: "Which accelerator does this host have?",
|
|
5984
|
-
options: ["intel", "amd", "coral", "cpu"]
|
|
5985
|
-
};
|
|
5986
|
-
var Q_PATHS = {
|
|
5987
|
-
id: "paths",
|
|
5988
|
-
question: "Host paths as {data, recordings} (optionally config, backups). Recordings is the one that grows: put it on a roomy disk.",
|
|
5989
|
-
options: []
|
|
5990
|
-
};
|
|
5991
|
-
function missingQuestions(input) {
|
|
5992
|
-
const out = [];
|
|
5993
|
-
if (input.hostKind === void 0) out.push(Q_HOST);
|
|
5994
|
-
if (input.role === void 0) out.push(Q_ROLE);
|
|
5995
|
-
const desktop = input.hostKind === "macos-desktop";
|
|
5996
|
-
if (!desktop && (input.accelerator === void 0 || input.accelerator === "apple")) {
|
|
5997
|
-
out.push(Q_ACCEL);
|
|
5998
|
-
}
|
|
5999
|
-
if (input.hostKind !== void 0 && CONTAINER_ROUTES.includes(input.hostKind) && input.paths === void 0) {
|
|
6000
|
-
out.push(Q_PATHS);
|
|
6001
|
-
}
|
|
6002
|
-
return out;
|
|
6003
|
-
}
|
|
6004
|
-
async function fetchText(url, fetchFn) {
|
|
6005
|
-
try {
|
|
6006
|
-
const res = await fetchFn(url, {
|
|
6007
|
-
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
6008
|
-
});
|
|
6009
|
-
if (!res.ok) {
|
|
6010
|
-
return {
|
|
6011
|
-
ok: false,
|
|
6012
|
-
failure: {
|
|
6013
|
-
status: "fetch-failed",
|
|
6014
|
-
url,
|
|
6015
|
-
error: `answered ${res.status}`
|
|
6016
|
-
}
|
|
6017
|
-
};
|
|
6018
|
-
}
|
|
6019
|
-
return { ok: true, body: await res.text() };
|
|
6020
|
-
} catch (err) {
|
|
6021
|
-
const error = err instanceof Error ? err.message : String(err);
|
|
6022
|
-
return { ok: false, failure: { status: "fetch-failed", url, error } };
|
|
6023
|
-
}
|
|
6024
|
-
}
|
|
6025
|
-
var ReleaseSchema = z22.object({
|
|
6026
|
-
tag_name: z22.string(),
|
|
6027
|
-
assets: z22.array(z22.object({ name: z22.string(), browser_download_url: z22.string() }))
|
|
6028
|
-
});
|
|
6029
|
-
var DMG = /^CamStack-[0-9][0-9A-Za-z.-]*-(arm64|x64)\.dmg$/;
|
|
6030
|
-
function acceleratorEdits(accelerator, role) {
|
|
6031
|
-
const note = `Image tag ${IMAGE_TAG_BY_ACCELERATOR[accelerator]} for ${accelerator}. ${NO_BARE_LATEST}`;
|
|
6032
|
-
return role === "agent" ? [note, "Do not run a hub and an agent on the same host."] : [note];
|
|
6033
|
-
}
|
|
6034
|
-
async function composeGuide(input, fetchFn, asDockerRun) {
|
|
6035
|
-
const url = TEMPLATE_URLS.compose[input.role];
|
|
6036
|
-
const fetched = await fetchText(url, fetchFn);
|
|
6037
|
-
if (!fetched.ok) return fetched.failure;
|
|
6038
|
-
const imageTag = IMAGE_TAG_BY_ACCELERATOR[input.accelerator];
|
|
6039
|
-
const custom = customizeCompose(fetched.body, {
|
|
6040
|
-
imageTag,
|
|
6041
|
-
markers: COMPOSE_MARKERS_BY_ACCELERATOR[input.accelerator],
|
|
6042
|
-
paths: input.paths,
|
|
6043
|
-
hubAddress: input.hubAddress
|
|
6044
|
-
});
|
|
6045
|
-
const edits = [...custom.edits, ...acceleratorEdits(input.accelerator, input.role)];
|
|
6046
|
-
const first = firstRun(input);
|
|
6047
|
-
if (!asDockerRun) {
|
|
6048
|
-
return ready(input, imageTag, {
|
|
6049
|
-
steps: [
|
|
6050
|
-
"Save docker-compose.yml (below) in an empty directory on the host.",
|
|
6051
|
-
"docker compose up -d && docker compose logs -f camstack",
|
|
6052
|
-
"The first boot seeds addons and pulls models: allow a few minutes.",
|
|
6053
|
-
...first
|
|
6054
|
-
],
|
|
6055
|
-
files: [{ path: "docker-compose.yml", body: custom.body, source: url }],
|
|
6056
|
-
edits
|
|
6057
|
-
});
|
|
6058
|
-
}
|
|
6059
|
-
const run = composeToDockerRun(custom.body);
|
|
6060
|
-
if (run === null) {
|
|
6061
|
-
return {
|
|
6062
|
-
status: "fetch-failed",
|
|
6063
|
-
url,
|
|
6064
|
-
error: "the official compose file has no image line; cannot derive docker run"
|
|
6065
|
-
};
|
|
6066
|
-
}
|
|
6067
|
-
return ready(input, imageTag, {
|
|
6068
|
-
steps: [
|
|
6069
|
-
"Create the host directories named by the -v lines, then run the file below as a shell script.",
|
|
6070
|
-
"docker logs -f camstack (the first boot seeds addons and pulls models).",
|
|
6071
|
-
...first
|
|
6072
|
-
],
|
|
6073
|
-
files: [{ path: "run-camstack.sh", body: `#!/bin/sh
|
|
6074
|
-
${run}
|
|
6075
|
-
`, source: url }],
|
|
6076
|
-
edits
|
|
6077
|
-
});
|
|
6078
|
-
}
|
|
6079
|
-
async function unraidGuide(input, fetchFn) {
|
|
6080
|
-
const url = TEMPLATE_URLS.unraid[input.role];
|
|
6081
|
-
const fetched = await fetchText(url, fetchFn);
|
|
6082
|
-
if (!fetched.ok) return fetched.failure;
|
|
6083
|
-
const imageTag = IMAGE_TAG_BY_ACCELERATOR[input.accelerator];
|
|
6084
|
-
const edits = [
|
|
6085
|
-
`In the template form, set Repository to ${IMAGE_REPOSITORY}:${imageTag} (the template ships intel-latest). ${NO_BARE_LATEST}`,
|
|
6086
|
-
"Recordings: point it at a roomy array share, not the cache; leave Media root as /recordings.",
|
|
6087
|
-
"Keep ExtraParams as shipped: --device-cgroup-rule and --pids-limit=8192.",
|
|
6088
|
-
...unraidDeviceEdits(input.accelerator)
|
|
6089
|
-
];
|
|
6090
|
-
const first = firstRun(input);
|
|
6091
|
-
return ready(input, imageTag, {
|
|
6092
|
-
steps: [
|
|
6093
|
-
`Unraid: Docker > Add Container > Template, paste ${url}`,
|
|
6094
|
-
"Review the paths and Repository (edits below), then Apply.",
|
|
6095
|
-
...first
|
|
6096
|
-
],
|
|
6097
|
-
files: [{ path: urlName(url), body: fetched.body, source: url }],
|
|
6098
|
-
edits
|
|
6099
|
-
});
|
|
6100
|
-
}
|
|
6101
|
-
function unraidDeviceEdits(accelerator) {
|
|
6102
|
-
if (accelerator === "intel" || accelerator === "amd") {
|
|
6103
|
-
return ["Intel/AMD iGPU: enter /dev/dri in that field. Intel NPU: enter /dev/accel."];
|
|
6104
|
-
}
|
|
6105
|
-
if (accelerator === "coral") {
|
|
6106
|
-
return ["Coral USB: enter /dev/bus/usb (the whole bus). Coral PCIe/M.2: /dev/apex_0 (and _1)."];
|
|
6107
|
-
}
|
|
6108
|
-
return [];
|
|
6109
|
-
}
|
|
6110
|
-
function urlName(url) {
|
|
6111
|
-
return url.slice(url.lastIndexOf("/") + 1);
|
|
6112
|
-
}
|
|
6113
|
-
async function proxmoxGuide(input, fetchFn) {
|
|
6114
|
-
const url = TEMPLATE_URLS.proxmoxInstaller;
|
|
6115
|
-
const fetched = await fetchText(url, fetchFn);
|
|
6116
|
-
if (!fetched.ok) return fetched.failure;
|
|
6117
|
-
const imageTag = IMAGE_TAG_BY_ACCELERATOR[input.accelerator];
|
|
6118
|
-
const vars = [`ROLE=${input.role}`];
|
|
6119
|
-
vars.push(`IMAGE_TAG=${IMAGE_REPOSITORY}:${imageTag}`);
|
|
6120
|
-
if (input.accelerator === "cpu") vars.push("PASS_ACCEL=none");
|
|
6121
|
-
if (input.role === "agent") vars.push(`CAMSTACK_HUB_ADDRESS=${input.hubAddress ?? "<hub-ip>"}`);
|
|
6122
|
-
if (input.paths !== void 0) vars.push(`RECORDINGS_HOST_PATH=${input.paths.recordings}`);
|
|
6123
|
-
const header = fetched.body.split("\n").slice(1, 20).filter((l) => l.startsWith("#")).join("\n");
|
|
6124
|
-
const first = firstRun(input);
|
|
6125
|
-
return ready(input, imageTag, {
|
|
6126
|
-
steps: [
|
|
6127
|
-
"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).",
|
|
6128
|
-
`${vars.join(" ")} bash -c "$(curl -fsSL ${url})"`,
|
|
6129
|
-
"It builds an Ubuntu 24.04 LXC with Docker, passes through the accelerators the host has (PASS_ACCEL=none skips it) and starts CamStack.",
|
|
6130
|
-
...first
|
|
6131
|
-
],
|
|
6132
|
-
files: [
|
|
6133
|
-
{
|
|
6134
|
-
path: "install-camstack-proxmox.sh (header)",
|
|
6135
|
-
body: header,
|
|
6136
|
-
source: url
|
|
6137
|
-
}
|
|
6138
|
-
],
|
|
6139
|
-
edits: [
|
|
6140
|
-
`The script defaults to the amd image; IMAGE_TAG above sets ${imageTag}. ${NO_BARE_LATEST}`,
|
|
6141
|
-
"A hub and an agent on one machine need two CTs (host networking collides otherwise)."
|
|
6142
|
-
]
|
|
6143
|
-
});
|
|
6144
|
-
}
|
|
6145
|
-
async function macGuide(input, fetchFn) {
|
|
6146
|
-
const url = TEMPLATE_URLS.latestRelease;
|
|
6147
|
-
const fetched = await fetchText(url, fetchFn);
|
|
6148
|
-
if (!fetched.ok) return fetched.failure;
|
|
6149
|
-
let release;
|
|
6150
|
-
try {
|
|
6151
|
-
const parsed = ReleaseSchema.safeParse(JSON.parse(fetched.body));
|
|
6152
|
-
if (!parsed.success) {
|
|
6153
|
-
return {
|
|
6154
|
-
status: "fetch-failed",
|
|
6155
|
-
url,
|
|
6156
|
-
error: "the release answer had no assets"
|
|
6157
|
-
};
|
|
6158
|
-
}
|
|
6159
|
-
release = parsed.data;
|
|
6160
|
-
} catch {
|
|
6161
|
-
return {
|
|
6162
|
-
status: "fetch-failed",
|
|
6163
|
-
url,
|
|
6164
|
-
error: "the release answer was not JSON"
|
|
6165
|
-
};
|
|
6166
|
-
}
|
|
6167
|
-
const dmgs = release.assets.filter((a) => DMG.test(a.name));
|
|
6168
|
-
const first = firstRun(input);
|
|
6169
|
-
return ready(input, null, {
|
|
6170
|
-
steps: [
|
|
6171
|
-
`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.`,
|
|
6172
|
-
"Drag CamStack into Applications and open it.",
|
|
6173
|
-
`The setup window asks what this Mac should be: choose ${input.role === "hub" ? "Hub (full server)" : "Agent (worker)"}, then Save and start.`,
|
|
6174
|
-
...first
|
|
6175
|
-
],
|
|
6176
|
-
files: [],
|
|
6177
|
-
edits: ["A Mac uses the app, not Docker: a container there cannot reach the Neural Engine."]
|
|
6178
|
-
});
|
|
6179
|
-
}
|
|
6180
|
-
function firstRun(input) {
|
|
6181
|
-
const host = input.hostKind === "macos-desktop" ? "localhost" : "<host>";
|
|
6182
|
-
if (input.role === "hub") return firstRunHub(host);
|
|
6183
|
-
return [
|
|
6184
|
-
"The agent status dashboard answers on http://<agent-host>:4444.",
|
|
6185
|
-
"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."
|
|
6186
|
-
];
|
|
6187
|
-
}
|
|
6188
|
-
function ready(input, imageTag, body) {
|
|
6189
|
-
return {
|
|
6190
|
-
status: "ready",
|
|
6191
|
-
hostKind: input.hostKind,
|
|
6192
|
-
role: input.role,
|
|
6193
|
-
imageTag,
|
|
6194
|
-
steps: body.steps,
|
|
6195
|
-
files: body.files,
|
|
6196
|
-
edits: body.edits,
|
|
6197
|
-
mustNotOmit: input.hostKind === "macos-desktop" ? [] : MUST_NOT_OMIT
|
|
6198
|
-
};
|
|
6199
|
-
}
|
|
6200
|
-
async function installGuide(input, fetchFn) {
|
|
6201
|
-
const questions = missingQuestions(input);
|
|
6202
|
-
if (questions.length > 0) return { status: "need-answers", questions };
|
|
6203
|
-
const { hostKind, role } = input;
|
|
6204
|
-
if (hostKind === void 0 || role === void 0) {
|
|
6205
|
-
return { status: "need-answers", questions: missingQuestions({}) };
|
|
6206
|
-
}
|
|
6207
|
-
const resolved = {
|
|
6208
|
-
hostKind,
|
|
6209
|
-
role,
|
|
6210
|
-
accelerator: hostKind === "macos-desktop" ? "apple" : input.accelerator ?? "cpu",
|
|
6211
|
-
paths: input.paths,
|
|
6212
|
-
hubAddress: input.hubAddress
|
|
6213
|
-
};
|
|
6214
|
-
switch (hostKind) {
|
|
6215
|
-
case "docker-compose":
|
|
6216
|
-
return composeGuide(resolved, fetchFn, false);
|
|
6217
|
-
case "docker":
|
|
6218
|
-
return composeGuide(resolved, fetchFn, true);
|
|
6219
|
-
case "unraid":
|
|
6220
|
-
return unraidGuide(resolved, fetchFn);
|
|
6221
|
-
case "proxmox-lxc":
|
|
6222
|
-
return proxmoxGuide(resolved, fetchFn);
|
|
6223
|
-
case "macos-desktop":
|
|
6224
|
-
return macGuide(resolved, fetchFn);
|
|
6225
|
-
}
|
|
6226
|
-
}
|
|
6227
|
-
|
|
6228
|
-
// src/mcp/setup-hubs.ts
|
|
6229
|
-
var DEFAULT_DISCOVER_TIMEOUT_MS = 2500;
|
|
6230
|
-
var MAX_DISCOVER_TIMEOUT_MS = 15e3;
|
|
6231
|
-
var HEALTH_TIMEOUT_MS = 5e3;
|
|
6232
|
-
var LOGIN = (server) => `run \`camstack login ${server}\` in a terminal`;
|
|
6233
|
-
var httpsHealth = (url, tls) => new Promise((resolve) => {
|
|
6234
|
-
const req = https.get(
|
|
6235
|
-
`${url}/health`,
|
|
6236
|
-
{ agent: createHubAgent(tls), timeout: HEALTH_TIMEOUT_MS },
|
|
6237
|
-
(res) => {
|
|
6238
|
-
res.resume();
|
|
6239
|
-
resolve({ ok: true, status: res.statusCode ?? 0 });
|
|
6240
|
-
}
|
|
6241
|
-
);
|
|
6242
|
-
req.on("timeout", () => req.destroy(new Error("timed out")));
|
|
6243
|
-
req.on("error", (err) => {
|
|
6244
|
-
resolve({
|
|
6245
|
-
ok: false,
|
|
6246
|
-
error: err.message,
|
|
6247
|
-
tlsHint: explainTlsFailure(url, err)
|
|
6248
|
-
});
|
|
6249
|
-
});
|
|
6250
|
-
});
|
|
6251
|
-
async function discoverHubsTool(timeoutMs, deps) {
|
|
6252
|
-
const wait = Math.min(timeoutMs ?? DEFAULT_DISCOVER_TIMEOUT_MS, MAX_DISCOVER_TIMEOUT_MS);
|
|
6253
|
-
const finder = deps.findHubs ?? ((ms) => findHubs({
|
|
6254
|
-
mdnsTimeoutMs: ms,
|
|
6255
|
-
...deps.sessionDir !== void 0 ? { sessionDir: deps.sessionDir } : {},
|
|
6256
|
-
...deps.env !== void 0 ? { env: deps.env } : {}
|
|
6257
|
-
}));
|
|
6258
|
-
const found = await finder(wait);
|
|
6259
|
-
const hubs = found.map((h) => ({
|
|
6260
|
-
host: h.host,
|
|
6261
|
-
port: h.port,
|
|
6262
|
-
name: h.nodeId,
|
|
6263
|
-
version: h.version,
|
|
6264
|
-
sources: h.sources
|
|
6265
|
-
}));
|
|
6266
|
-
return {
|
|
6267
|
-
status: "ok",
|
|
6268
|
-
hubs,
|
|
6269
|
-
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"
|
|
6270
|
-
};
|
|
6271
|
-
}
|
|
6272
|
-
function pinnedTls(url, deps) {
|
|
6273
|
-
const caFile = hubCaFileForServer(url, deps.sessionDir);
|
|
6274
|
-
try {
|
|
6275
|
-
return {
|
|
6276
|
-
tls: resolveHubTls(url, deps.env ?? process.env, deps.sessionDir),
|
|
6277
|
-
caFile
|
|
6278
|
-
};
|
|
6279
|
-
} catch {
|
|
6280
|
-
return { tls: {}, caFile };
|
|
6281
|
-
}
|
|
6282
|
-
}
|
|
6283
|
-
async function connectTool(server, deps) {
|
|
6284
|
-
let url;
|
|
6285
|
-
try {
|
|
6286
|
-
url = normalizeHubAddress(server);
|
|
6287
|
-
} catch (err) {
|
|
6288
|
-
const message = err instanceof HubAddressError ? err.message : String(err);
|
|
6289
|
-
return { status: "invalid-address", server, message };
|
|
6290
|
-
}
|
|
6291
|
-
const { tls, caFile } = pinnedTls(url, deps);
|
|
6292
|
-
const outcome = await (deps.health ?? httpsHealth)(url, tls);
|
|
6293
|
-
const caPinned = tls.ca !== void 0;
|
|
6294
|
-
const session = loadSession(url, deps.sessionDir);
|
|
6295
|
-
const sessionInfo = {
|
|
6296
|
-
exists: session !== null,
|
|
6297
|
-
username: session?.username ?? null
|
|
6298
|
-
};
|
|
6299
|
-
if (!outcome.ok) {
|
|
6300
|
-
return {
|
|
6301
|
-
status: "unreachable",
|
|
6302
|
-
server: url,
|
|
6303
|
-
message: outcome.error,
|
|
6304
|
-
tls: { caPinned, caFile, hint: outcome.tlsHint },
|
|
6305
|
-
session: sessionInfo,
|
|
6306
|
-
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)`
|
|
6307
|
-
};
|
|
6308
|
-
}
|
|
6309
|
-
return {
|
|
6310
|
-
status: "reachable",
|
|
6311
|
-
server: url,
|
|
6312
|
-
health: { status: outcome.status },
|
|
6313
|
-
tls: { caPinned, caFile, hint: null },
|
|
6314
|
-
session: sessionInfo,
|
|
6315
|
-
next: session === null ? LOGIN(url) : "a session exists: the ops tools can use it"
|
|
6316
|
-
};
|
|
6317
|
-
}
|
|
6318
|
-
var DiscoveredAgentsSchema = z23.object({
|
|
6319
|
-
agents: z23.array(
|
|
6320
|
-
z23.object({
|
|
6321
|
-
nodeId: z23.string(),
|
|
6322
|
-
host: z23.string(),
|
|
6323
|
-
port: z23.number(),
|
|
6324
|
-
version: z23.string().nullable(),
|
|
6325
|
-
name: z23.string().nullable(),
|
|
6326
|
-
state: z23.enum(["unpaired", "pending", "paired"])
|
|
6327
|
-
})
|
|
6328
|
-
)
|
|
6329
|
-
});
|
|
6330
|
-
var PAIRING_STEPS = [
|
|
6331
|
-
"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).",
|
|
6332
|
-
"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.",
|
|
6333
|
-
"The agent installs its certificate only after its own operator confirmed. Nothing here (and no tool of this MCP) confirms a pairing for you.",
|
|
6334
|
-
"The agent then appears under Cluster > Nodes."
|
|
6335
|
-
];
|
|
6336
|
-
async function addNodeTool(input, ctx) {
|
|
6337
|
-
const guide = await installGuide({ ...input, role: "agent" }, ctx.fetch);
|
|
6338
|
-
const outcome = await ctx.hub();
|
|
6339
|
-
if (!outcome.ok) {
|
|
6340
|
-
return {
|
|
6341
|
-
guide,
|
|
6342
|
-
pairing: PAIRING_STEPS,
|
|
6343
|
-
agentsOnNetwork: { status: "no-session", action: outcome.action }
|
|
6344
|
-
};
|
|
6345
|
-
}
|
|
6346
|
-
try {
|
|
6347
|
-
const raw = await outcome.connection.client.call("GET", "agentPairing.listDiscoveredAgents", {});
|
|
6348
|
-
const parsed = DiscoveredAgentsSchema.safeParse(raw);
|
|
6349
|
-
return {
|
|
6350
|
-
guide,
|
|
6351
|
-
pairing: PAIRING_STEPS,
|
|
6352
|
-
agentsOnNetwork: parsed.success ? { status: "ok", agents: parsed.data.agents } : {
|
|
6353
|
-
status: "unavailable",
|
|
6354
|
-
error: "the hub answered in an unexpected shape"
|
|
6355
|
-
}
|
|
6356
|
-
};
|
|
6357
|
-
} catch (err) {
|
|
6358
|
-
return {
|
|
6359
|
-
guide,
|
|
6360
|
-
pairing: PAIRING_STEPS,
|
|
6361
|
-
agentsOnNetwork: {
|
|
6362
|
-
status: "unavailable",
|
|
6363
|
-
error: err instanceof Error ? err.message : String(err)
|
|
6364
|
-
}
|
|
6365
|
-
};
|
|
6366
|
-
}
|
|
6367
|
-
}
|
|
6368
|
-
|
|
6369
|
-
// src/mcp/setup-tools.ts
|
|
6370
|
-
var SUPPORTED_HOST_KINDS = HOST_KINDS;
|
|
6371
|
-
var REGISTRY2 = "https://registry.npmjs.org";
|
|
6372
|
-
var REGISTRY_TIMEOUT_MS2 = 5e3;
|
|
6373
|
-
var RegistryLatestSchema = z24.object({ version: z24.string() });
|
|
6374
|
-
async function latestOnNpm(name, fetchFn) {
|
|
6375
|
-
const url = `${REGISTRY2}/${name.replace("/", "%2f")}/latest`;
|
|
6376
|
-
try {
|
|
6377
|
-
const res = await fetchFn(url, {
|
|
6378
|
-
signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS2)
|
|
6379
|
-
});
|
|
6380
|
-
if (!res.ok) return { latest: null, error: `registry answered ${res.status}` };
|
|
6381
|
-
const parsed = RegistryLatestSchema.safeParse(await res.json());
|
|
6382
|
-
if (!parsed.success) return { latest: null, error: "registry answer had no version" };
|
|
6383
|
-
return { latest: parsed.data.version };
|
|
6384
|
-
} catch (err) {
|
|
6385
|
-
return {
|
|
6386
|
-
latest: null,
|
|
6387
|
-
error: err instanceof Error ? err.message : String(err)
|
|
6388
|
-
};
|
|
6389
|
-
}
|
|
6390
|
-
}
|
|
6391
|
-
async function about(ctx) {
|
|
6392
|
-
const [cli, server] = await Promise.all([
|
|
6393
|
-
latestOnNpm("camstack", ctx.fetch),
|
|
6394
|
-
latestOnNpm("@camstack/server", ctx.fetch)
|
|
6395
|
-
]);
|
|
6396
|
-
return {
|
|
6397
|
-
cli: { installed: ctx.cliVersion, ...cli },
|
|
6398
|
-
server,
|
|
6399
|
-
supportedHostKinds: SUPPORTED_HOST_KINDS
|
|
6400
|
-
};
|
|
6401
|
-
}
|
|
6402
|
-
function registerSetupTools(server, ctx, deps = {}) {
|
|
6403
|
-
server.tool(
|
|
6404
|
-
"setup_about",
|
|
6405
|
-
"Versions of the camstack CLI (installed and latest on npm), the latest @camstack/server on npm, and the host kinds CamStack supports. Needs no hub and no login.",
|
|
6406
|
-
{},
|
|
6407
|
-
async () => text11(await about(ctx))
|
|
6408
|
-
);
|
|
6409
|
-
server.tool(
|
|
6410
|
-
"setup_install_guide",
|
|
6411
|
-
'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.',
|
|
6412
|
-
{
|
|
6413
|
-
hostKind: HostKindSchema.optional(),
|
|
6414
|
-
role: RoleSchema.optional(),
|
|
6415
|
-
accelerator: AcceleratorSchema.optional(),
|
|
6416
|
-
paths: InstallPathsSchema.optional(),
|
|
6417
|
-
hubAddress: z24.string().min(1).optional().describe("Agents only: the hub to dial.")
|
|
6418
|
-
},
|
|
6419
|
-
async (input) => text11(await installGuide(input, ctx.fetch))
|
|
6420
|
-
);
|
|
6421
|
-
server.tool(
|
|
6422
|
-
"setup_discover_hubs",
|
|
6423
|
-
"Find CamStack hubs on the local network (mDNS) plus the hubs this machine already knows, with host, port, name and version. Needs no login.",
|
|
6424
|
-
{ timeoutMs: z24.number().int().min(200).max(15e3).optional() },
|
|
6425
|
-
async ({ timeoutMs }) => text11(await discoverHubsTool(timeoutMs, deps))
|
|
6426
|
-
);
|
|
6427
|
-
server.tool(
|
|
6428
|
-
"setup_connect",
|
|
6429
|
-
"Check that a hub answers on https://<server>:4443/health, whether its CA is already pinned for TLS, and whether a `camstack login` session exists. Never takes a password: when there is no session it says to run `camstack login <server>` in a terminal.",
|
|
6430
|
-
{ server: z24.string().min(1) },
|
|
6431
|
-
async ({ server: address }) => text11(await connectTool(address, deps))
|
|
6432
|
-
);
|
|
6433
|
-
server.tool(
|
|
6434
|
-
"setup_add_node",
|
|
6435
|
-
"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.",
|
|
6436
|
-
{
|
|
6437
|
-
hostKind: HostKindSchema.optional(),
|
|
6438
|
-
accelerator: AcceleratorSchema.optional(),
|
|
6439
|
-
paths: InstallPathsSchema.optional(),
|
|
6440
|
-
hubAddress: z24.string().min(1).optional()
|
|
6441
|
-
},
|
|
6442
|
-
async (input) => text11(await addNodeTool(input, ctx))
|
|
6443
|
-
);
|
|
6444
|
-
}
|
|
6445
|
-
|
|
6446
|
-
// src/mcp/ops-update-cli.ts
|
|
6447
|
-
var CLI_PACKAGE = "camstack";
|
|
6448
|
-
var NPM_GLOBAL = /[\\/]lib[\\/]node_modules[\\/]camstack[\\/]|[\\/]npm[\\/]node_modules[\\/]camstack[\\/]/;
|
|
6449
|
-
var OTHER_MANAGER = /[\\/]\.bun[\\/]|[\\/]pnpm[\\/]|[\\/]\.yarn[\\/]|[\\/]yarn[\\/]|[\\/]\.volta[\\/]/;
|
|
6450
|
-
function detectCliInstall(entryPath) {
|
|
6451
|
-
const manual = `Update it with the package manager it was installed with, for example \`npm install -g ${CLI_PACKAGE}@latest\` (no sudo: fix the npm prefix permissions instead).`;
|
|
6452
|
-
if (/[\\/]_npx[\\/]/.test(entryPath)) {
|
|
6453
|
-
return {
|
|
6454
|
-
method: "npx",
|
|
6455
|
-
command: null,
|
|
6456
|
-
instructions: `The CLI runs through npx: there is nothing installed to update. Start it as \`npx ${CLI_PACKAGE}@latest mcp\` (or clear the npx cache), then restart the MCP in your client.`
|
|
6457
|
-
};
|
|
6458
|
-
}
|
|
6459
|
-
if (OTHER_MANAGER.test(entryPath) || !NPM_GLOBAL.test(entryPath)) {
|
|
6460
|
-
return { method: "unknown", command: null, instructions: manual };
|
|
6461
|
-
}
|
|
6462
|
-
return {
|
|
6463
|
-
method: "npm-global",
|
|
6464
|
-
command: ["npm", "install", "-g", `${CLI_PACKAGE}@latest`],
|
|
6465
|
-
instructions: `npm install -g ${CLI_PACKAGE}@latest`
|
|
6466
|
-
};
|
|
6467
|
-
}
|
|
6468
|
-
var runCommand = (file, args) => new Promise((resolve) => {
|
|
6469
|
-
execFile(file, [...args], { timeout: 5 * 6e4 }, (err, stdout, stderr) => {
|
|
6470
|
-
const out = `${stdout}${stderr}`.trim().slice(-600);
|
|
6471
|
-
resolve({
|
|
6472
|
-
ok: err === null,
|
|
6473
|
-
output: err ? `${err.message} ${out}`.trim() : out
|
|
6474
|
-
});
|
|
6475
|
-
});
|
|
6476
|
-
});
|
|
6477
|
-
function parts(v) {
|
|
6478
|
-
return v.split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
6479
|
-
}
|
|
6480
|
-
function isOlder(a, b) {
|
|
6481
|
-
const x = parts(a);
|
|
6482
|
-
const y = parts(b);
|
|
6483
|
-
for (let i = 0; i < 3; i++) {
|
|
6484
|
-
const d = (x[i] ?? 0) - (y[i] ?? 0);
|
|
6485
|
-
if (d !== 0) return d < 0;
|
|
6486
|
-
}
|
|
6487
|
-
return false;
|
|
6488
|
-
}
|
|
6489
|
-
async function cliVerdict(installed, fetchFn, entryPath) {
|
|
6490
|
-
const latest = await latestOnNpm(CLI_PACKAGE, fetchFn);
|
|
6491
|
-
return {
|
|
6492
|
-
installed,
|
|
6493
|
-
latest: latest.latest,
|
|
6494
|
-
updateAvailable: latest.latest === null ? null : isOlder(installed, latest.latest),
|
|
6495
|
-
...latest.error !== void 0 ? { error: latest.error } : {},
|
|
6496
|
-
install: detectCliInstall(entryPath)
|
|
6497
|
-
};
|
|
6498
|
-
}
|
|
6499
|
-
|
|
6500
5709
|
// src/mcp/ops-update-apply.ts
|
|
6501
5710
|
var UPDATE_APPLY_TOOL = "ops_update_apply";
|
|
6502
5711
|
function timing(deps) {
|
|
@@ -6549,7 +5758,7 @@ async function readPlan(client, args, ctx, deps) {
|
|
|
6549
5758
|
wrapper: wrapperVerdict(status.value)
|
|
6550
5759
|
});
|
|
6551
5760
|
}
|
|
6552
|
-
const updates = args.addons === false ? null : await readQuery(client, "addons.listUpdates", {},
|
|
5761
|
+
const updates = args.addons === false ? null : await readQuery(client, "addons.listUpdates", {}, z21.array(AddonUpdateShape));
|
|
6553
5762
|
const cli = args.cli === false ? null : await cliVerdict(ctx.cliVersion, ctx.fetch, deps.entryPath ?? process.argv[1] ?? "");
|
|
6554
5763
|
const hub = plans.find((p) => p.node.isHub === true) ?? null;
|
|
6555
5764
|
return {
|
|
@@ -6865,14 +6074,14 @@ async function opsUpdateApply(ctx, args, confirmToken, deps = {}) {
|
|
|
6865
6074
|
}
|
|
6866
6075
|
|
|
6867
6076
|
// src/mcp/ops-update-check.ts
|
|
6868
|
-
import { z as
|
|
6077
|
+
import { z as z22 } from "zod";
|
|
6869
6078
|
var ADDON_LIMIT = 30;
|
|
6870
6079
|
async function opsUpdateCheck(ctx, deps = {}) {
|
|
6871
6080
|
return withAdminHub(ctx, async ({ client, server }) => {
|
|
6872
6081
|
const unavailable = {};
|
|
6873
6082
|
const [topology, updates, installs, cli] = await Promise.all([
|
|
6874
6083
|
readTopology(client),
|
|
6875
|
-
readQuery(client, "addons.listUpdates", {},
|
|
6084
|
+
readQuery(client, "addons.listUpdates", {}, z22.array(AddonUpdateShape)),
|
|
6876
6085
|
readInstallations(client),
|
|
6877
6086
|
cliVerdict(ctx.cliVersion, ctx.fetch, deps.entryPath ?? process.argv[1] ?? "")
|
|
6878
6087
|
]);
|
|
@@ -6990,40 +6199,40 @@ function registerOpsTools(server, ctx) {
|
|
|
6990
6199
|
"ops_whoami",
|
|
6991
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.",
|
|
6992
6201
|
{},
|
|
6993
|
-
async () =>
|
|
6202
|
+
async () => text(await whoami(ctx))
|
|
6994
6203
|
);
|
|
6995
6204
|
server.tool(
|
|
6996
6205
|
"ops_status",
|
|
6997
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`.',
|
|
6998
6207
|
{},
|
|
6999
|
-
async () =>
|
|
6208
|
+
async () => text(await opsStatus(ctx))
|
|
7000
6209
|
);
|
|
7001
6210
|
server.tool(
|
|
7002
6211
|
"ops_device",
|
|
7003
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.",
|
|
7004
|
-
{ deviceId:
|
|
7005
|
-
async ({ deviceId }) =>
|
|
6213
|
+
{ deviceId: z23.number().int() },
|
|
6214
|
+
async ({ deviceId }) => text(await opsDevice(ctx, deviceId))
|
|
7006
6215
|
);
|
|
7007
6216
|
server.tool(
|
|
7008
6217
|
"ops_unhealthy_devices",
|
|
7009
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`.",
|
|
7010
6219
|
{},
|
|
7011
|
-
async () =>
|
|
6220
|
+
async () => text(await opsUnhealthyDevices(ctx))
|
|
7012
6221
|
);
|
|
7013
6222
|
server.tool(
|
|
7014
6223
|
"ops_logs",
|
|
7015
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`.",
|
|
7016
6225
|
{
|
|
7017
|
-
deviceId:
|
|
7018
|
-
addonId:
|
|
7019
|
-
nodeId:
|
|
7020
|
-
level:
|
|
7021
|
-
windowMinutes:
|
|
7022
|
-
text:
|
|
7023
|
-
around:
|
|
7024
|
-
contextLines:
|
|
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()
|
|
7025
6234
|
},
|
|
7026
|
-
async (args) =>
|
|
6235
|
+
async (args) => text(
|
|
7027
6236
|
await opsLogs(ctx, {
|
|
7028
6237
|
...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
|
|
7029
6238
|
...args.addonId ? { addonId: args.addonId } : {},
|
|
@@ -7040,14 +6249,14 @@ function registerOpsTools(server, ctx) {
|
|
|
7040
6249
|
"ops_events",
|
|
7041
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.",
|
|
7042
6251
|
{
|
|
7043
|
-
deviceId:
|
|
7044
|
-
className:
|
|
7045
|
-
zone:
|
|
7046
|
-
text:
|
|
7047
|
-
windowMinutes:
|
|
7048
|
-
limit:
|
|
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()
|
|
7049
6258
|
},
|
|
7050
|
-
async (args) =>
|
|
6259
|
+
async (args) => text(
|
|
7051
6260
|
await opsEvents(ctx, {
|
|
7052
6261
|
...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
|
|
7053
6262
|
...args.className ? { className: args.className } : {},
|
|
@@ -7061,31 +6270,31 @@ function registerOpsTools(server, ctx) {
|
|
|
7061
6270
|
server.tool(
|
|
7062
6271
|
"ops_track",
|
|
7063
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.",
|
|
7064
|
-
{ deviceId:
|
|
7065
|
-
async ({ deviceId, trackId }) =>
|
|
6273
|
+
{ deviceId: z23.number().int(), trackId: z23.string().min(1) },
|
|
6274
|
+
async ({ deviceId, trackId }) => text(await opsTrack(ctx, { deviceId, trackId }))
|
|
7066
6275
|
);
|
|
7067
6276
|
server.tool(
|
|
7068
6277
|
"ops_accelerators",
|
|
7069
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`.',
|
|
7070
6279
|
{},
|
|
7071
|
-
async () =>
|
|
6280
|
+
async () => text(await opsAccelerators(ctx))
|
|
7072
6281
|
);
|
|
7073
6282
|
server.tool(
|
|
7074
6283
|
"ops_update_check",
|
|
7075
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`.',
|
|
7076
6285
|
{},
|
|
7077
|
-
async () =>
|
|
6286
|
+
async () => text(await opsUpdateCheck(ctx))
|
|
7078
6287
|
);
|
|
7079
6288
|
server.tool(
|
|
7080
6289
|
UPDATE_APPLY_TOOL,
|
|
7081
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`.",
|
|
7082
6291
|
{
|
|
7083
|
-
version:
|
|
7084
|
-
addons:
|
|
7085
|
-
cli:
|
|
7086
|
-
confirm_token:
|
|
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()
|
|
7087
6296
|
},
|
|
7088
|
-
async ({ version, addons, cli, confirm_token }) =>
|
|
6297
|
+
async ({ version, addons, cli, confirm_token }) => text(
|
|
7089
6298
|
await opsUpdateApply(
|
|
7090
6299
|
ctx,
|
|
7091
6300
|
{
|
|
@@ -7100,7 +6309,7 @@ function registerOpsTools(server, ctx) {
|
|
|
7100
6309
|
}
|
|
7101
6310
|
|
|
7102
6311
|
// src/mcp/configure-tools.ts
|
|
7103
|
-
var CurrentDeviceSchema =
|
|
6312
|
+
var CurrentDeviceSchema = z24.object({ name: z24.string() }).nullable();
|
|
7104
6313
|
var RENAME_DEVICE_TOOL = "configure_rename_device";
|
|
7105
6314
|
function renameDeviceSpec(client) {
|
|
7106
6315
|
return {
|
|
@@ -7162,11 +6371,11 @@ function registerConfigureTools(server, ctx) {
|
|
|
7162
6371
|
RENAME_DEVICE_TOOL,
|
|
7163
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.",
|
|
7164
6373
|
{
|
|
7165
|
-
deviceId:
|
|
7166
|
-
name:
|
|
7167
|
-
confirm_token:
|
|
6374
|
+
deviceId: z24.number().int(),
|
|
6375
|
+
name: z24.string().min(1),
|
|
6376
|
+
confirm_token: z24.string().optional()
|
|
7168
6377
|
},
|
|
7169
|
-
async ({ deviceId, name, confirm_token }) =>
|
|
6378
|
+
async ({ deviceId, name, confirm_token }) => text(await renameDevice(ctx, { deviceId, name }, confirm_token))
|
|
7170
6379
|
);
|
|
7171
6380
|
registerAddonTools(server, ctx);
|
|
7172
6381
|
registerCameraTools(server, ctx);
|
|
@@ -7175,11 +6384,10 @@ function registerConfigureTools(server, ctx) {
|
|
|
7175
6384
|
}
|
|
7176
6385
|
|
|
7177
6386
|
// src/mcp/support-tools.ts
|
|
7178
|
-
import {
|
|
7179
|
-
import { z as z33 } from "zod";
|
|
6387
|
+
import { z as z29 } from "zod";
|
|
7180
6388
|
|
|
7181
6389
|
// src/mcp/support-accelerator.ts
|
|
7182
|
-
import { z as
|
|
6390
|
+
import { z as z25 } from "zod";
|
|
7183
6391
|
var EXPECTED_ACCELERATORS = [
|
|
7184
6392
|
"intel-igpu",
|
|
7185
6393
|
"intel-npu",
|
|
@@ -7191,23 +6399,23 @@ var EXPECTED_ACCELERATORS = [
|
|
|
7191
6399
|
"apple-ane",
|
|
7192
6400
|
"nvidia"
|
|
7193
6401
|
];
|
|
7194
|
-
var ExpectedAcceleratorSchema =
|
|
7195
|
-
var HardwareSchema2 =
|
|
7196
|
-
platform:
|
|
7197
|
-
arch:
|
|
7198
|
-
cpuModel:
|
|
7199
|
-
gpu:
|
|
7200
|
-
npu:
|
|
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()
|
|
7201
6409
|
});
|
|
7202
|
-
var InferenceDevicesSchema2 =
|
|
7203
|
-
reachable:
|
|
7204
|
-
devices:
|
|
7205
|
-
|
|
7206
|
-
key:
|
|
7207
|
-
available:
|
|
7208
|
-
presence:
|
|
7209
|
-
enabled:
|
|
7210
|
-
exclusion:
|
|
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()
|
|
7211
6419
|
})
|
|
7212
6420
|
)
|
|
7213
6421
|
});
|
|
@@ -7403,11 +6611,11 @@ function diagnoseAccelerator(input) {
|
|
|
7403
6611
|
}
|
|
7404
6612
|
const matched = devices.filter((d) => expectation.deviceKeys(d.key));
|
|
7405
6613
|
const matchedKeys = matched.map((d) => d.key);
|
|
7406
|
-
const
|
|
7407
|
-
if (
|
|
6614
|
+
const ready = matched.filter((d) => d.available && d.enabled && d.exclusion === null);
|
|
6615
|
+
if (ready.length > 0) {
|
|
7408
6616
|
return finish(
|
|
7409
6617
|
"ok",
|
|
7410
|
-
`The node offers ${
|
|
6618
|
+
`The node offers ${ready.map((d) => d.key).join(", ")}, available and enabled.`,
|
|
7411
6619
|
matchedKeys,
|
|
7412
6620
|
[],
|
|
7413
6621
|
null
|
|
@@ -7486,7 +6694,7 @@ async function supportDiagnoseAccelerator(ctx, args) {
|
|
|
7486
6694
|
import {
|
|
7487
6695
|
runGated as runGated5
|
|
7488
6696
|
} from "@camstack/develop";
|
|
7489
|
-
import { z as
|
|
6697
|
+
import { z as z26 } from "zod";
|
|
7490
6698
|
var BENCHMARK_RUN_TOOL = "support_benchmark_run";
|
|
7491
6699
|
var AUTO_TUNE_MODELS = [
|
|
7492
6700
|
"yolov9t-320",
|
|
@@ -7508,64 +6716,64 @@ var CONTRIBUTE_HOW = [
|
|
|
7508
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.",
|
|
7509
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.'
|
|
7510
6718
|
];
|
|
7511
|
-
var HardwareSchema3 =
|
|
7512
|
-
platform:
|
|
7513
|
-
arch:
|
|
7514
|
-
cpuModel:
|
|
7515
|
-
cpuCores:
|
|
7516
|
-
totalRAM_MB:
|
|
7517
|
-
availableRAM_MB:
|
|
7518
|
-
gpu:
|
|
7519
|
-
type:
|
|
7520
|
-
name:
|
|
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()
|
|
7521
6729
|
}).nullable(),
|
|
7522
|
-
npu:
|
|
6730
|
+
npu: z26.looseObject({ type: z26.enum(["apple-ane", "intel-npu", "amd-xdna"]) }).nullable()
|
|
7523
6731
|
});
|
|
7524
|
-
var InferenceDevicesSchema3 =
|
|
7525
|
-
reachable:
|
|
7526
|
-
devices:
|
|
7527
|
-
|
|
7528
|
-
key:
|
|
7529
|
-
backend:
|
|
7530
|
-
available:
|
|
7531
|
-
enabled:
|
|
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()
|
|
7532
6740
|
})
|
|
7533
6741
|
)
|
|
7534
6742
|
});
|
|
7535
|
-
var RunStatusSchema =
|
|
7536
|
-
run:
|
|
7537
|
-
runId:
|
|
7538
|
-
nodeId:
|
|
7539
|
-
state:
|
|
7540
|
-
endReason:
|
|
7541
|
-
error:
|
|
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()
|
|
7542
6750
|
}).nullable()
|
|
7543
6751
|
});
|
|
7544
|
-
var ModelListSchema =
|
|
7545
|
-
|
|
7546
|
-
id:
|
|
7547
|
-
formats:
|
|
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()
|
|
7548
6756
|
})
|
|
7549
6757
|
);
|
|
7550
|
-
var PipelineTestSchema =
|
|
7551
|
-
kind:
|
|
7552
|
-
detections:
|
|
7553
|
-
|
|
7554
|
-
debug:
|
|
7555
|
-
stepLatencyMs:
|
|
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()
|
|
7556
6764
|
}).optional()
|
|
7557
6765
|
})
|
|
7558
6766
|
).optional(),
|
|
7559
|
-
discarded:
|
|
7560
|
-
debug:
|
|
7561
|
-
stepTimings:
|
|
7562
|
-
|
|
7563
|
-
source:
|
|
7564
|
-
ms:
|
|
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()
|
|
7565
6773
|
})
|
|
7566
6774
|
).optional(),
|
|
7567
|
-
predictMs:
|
|
7568
|
-
totalInferenceMs:
|
|
6775
|
+
predictMs: z26.number().optional(),
|
|
6776
|
+
totalInferenceMs: z26.number().optional()
|
|
7569
6777
|
}).optional()
|
|
7570
6778
|
});
|
|
7571
6779
|
var FORMAT_BY_BACKEND = {
|
|
@@ -7852,15 +7060,15 @@ async function supportBenchmarkRun(ctx, deps, args, confirmToken) {
|
|
|
7852
7060
|
}
|
|
7853
7061
|
return runGated5(ctx.gate, benchmarkRunSpec(outcome.connection.client, deps), args, confirmToken);
|
|
7854
7062
|
}
|
|
7855
|
-
var ResultsSchema =
|
|
7856
|
-
|
|
7857
|
-
nodeId:
|
|
7858
|
-
cell:
|
|
7859
|
-
phase:
|
|
7860
|
-
deviceKey:
|
|
7861
|
-
modelId:
|
|
7862
|
-
p50Ms:
|
|
7863
|
-
fps:
|
|
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()
|
|
7864
7072
|
})
|
|
7865
7073
|
})
|
|
7866
7074
|
);
|
|
@@ -7913,20 +7121,20 @@ async function supportBenchmarkStatus(ctx, args) {
|
|
|
7913
7121
|
}
|
|
7914
7122
|
|
|
7915
7123
|
// src/mcp/support-camera.ts
|
|
7916
|
-
import { z as
|
|
7917
|
-
var DeviceSchema2 =
|
|
7918
|
-
id:
|
|
7919
|
-
name:
|
|
7920
|
-
addonId:
|
|
7921
|
-
disabled:
|
|
7922
|
-
online:
|
|
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()
|
|
7923
7131
|
}).nullable();
|
|
7924
7132
|
var GENERIC = /onvif|rtsp/i;
|
|
7925
|
-
function words(
|
|
7926
|
-
return
|
|
7133
|
+
function words(text2) {
|
|
7134
|
+
return text2.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length >= 3);
|
|
7927
7135
|
}
|
|
7928
|
-
function isGeneric(
|
|
7929
|
-
return
|
|
7136
|
+
function isGeneric(parts) {
|
|
7137
|
+
return parts.some((p) => GENERIC.test(p));
|
|
7930
7138
|
}
|
|
7931
7139
|
function installedPick(p) {
|
|
7932
7140
|
return {
|
|
@@ -7947,9 +7155,9 @@ function availablePick(p) {
|
|
|
7947
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.";
|
|
7948
7156
|
function matchIn(pools, tokens) {
|
|
7949
7157
|
if (tokens.length === 0) return null;
|
|
7950
|
-
const hit = (
|
|
7951
|
-
if (isGeneric(
|
|
7952
|
-
const joined =
|
|
7158
|
+
const hit = (parts) => {
|
|
7159
|
+
if (isGeneric(parts)) return false;
|
|
7160
|
+
const joined = parts.join(" ").toLowerCase();
|
|
7953
7161
|
return tokens.some((t) => joined.includes(t));
|
|
7954
7162
|
};
|
|
7955
7163
|
const installed = pools.installed.find(
|
|
@@ -8085,10 +7293,10 @@ async function supportDiagnoseCamera(ctx, args) {
|
|
|
8085
7293
|
}
|
|
8086
7294
|
|
|
8087
7295
|
// src/mcp/support-deps.ts
|
|
8088
|
-
import { execFile
|
|
7296
|
+
import { execFile } from "child_process";
|
|
8089
7297
|
var RUN_TIMEOUT_MS = 6e4;
|
|
8090
7298
|
var spawnRunner = (command, args) => new Promise((resolve) => {
|
|
8091
|
-
|
|
7299
|
+
execFile(
|
|
8092
7300
|
command,
|
|
8093
7301
|
[...args],
|
|
8094
7302
|
{ timeout: RUN_TIMEOUT_MS, maxBuffer: 1e6 },
|
|
@@ -8120,7 +7328,7 @@ import {
|
|
|
8120
7328
|
digestArgs,
|
|
8121
7329
|
runGated as runGated6
|
|
8122
7330
|
} from "@camstack/develop";
|
|
8123
|
-
import { z as
|
|
7331
|
+
import { z as z28 } from "zod";
|
|
8124
7332
|
|
|
8125
7333
|
// src/mcp/support-scrub.ts
|
|
8126
7334
|
import { redactToolArgs, scrubSecrets } from "@camstack/develop";
|
|
@@ -8129,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]{
|
|
|
8129
7337
|
var MAC = /\b(?:[0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}\b/g;
|
|
8130
7338
|
var HOME_DIR = /(\/Users|\/home)\/[^/\s'"]+/g;
|
|
8131
7339
|
function octets(ip) {
|
|
8132
|
-
const
|
|
8133
|
-
if (
|
|
8134
|
-
if (
|
|
8135
|
-
return
|
|
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;
|
|
8136
7344
|
}
|
|
8137
7345
|
function maskIpv4(ip) {
|
|
8138
7346
|
const o = octets(ip);
|
|
@@ -8173,16 +7381,16 @@ var INFERENCE_PACKAGE = "@camstack/addon-pipeline";
|
|
|
8173
7381
|
var TITLE_MAX = 140;
|
|
8174
7382
|
var DIAGNOSIS_MAX = 4e3;
|
|
8175
7383
|
var DRAFT_KINDS = ["accelerator", "camera", "benchmark", "other"];
|
|
8176
|
-
var DraftKindSchema =
|
|
8177
|
-
var BenchmarkCellsSchema =
|
|
8178
|
-
|
|
8179
|
-
|
|
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() })
|
|
8180
7388
|
);
|
|
8181
7389
|
var PackageNameRe = /^(@[a-z0-9._-]+\/)?[a-z0-9._-]+$/;
|
|
8182
7390
|
var GithubRepoRe = /github\.com[:/]([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?(?:[/#?].*)?$/;
|
|
8183
7391
|
var ShorthandRe = /^(?:github:)?([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/;
|
|
8184
|
-
var RegistryDocSchema =
|
|
8185
|
-
repository:
|
|
7392
|
+
var RegistryDocSchema = z28.looseObject({
|
|
7393
|
+
repository: z28.union([z28.string(), z28.looseObject({ url: z28.string() })]).optional()
|
|
8186
7394
|
});
|
|
8187
7395
|
function githubRepoOf(value) {
|
|
8188
7396
|
const url = GithubRepoRe.exec(value);
|
|
@@ -8224,7 +7432,7 @@ async function resolveRepo(ctx, packageName) {
|
|
|
8224
7432
|
reason: null
|
|
8225
7433
|
};
|
|
8226
7434
|
}
|
|
8227
|
-
var ServerStatusSchema2 =
|
|
7435
|
+
var ServerStatusSchema2 = z28.looseObject({ runningVersion: z28.string() });
|
|
8228
7436
|
async function serverVersion(client, nodeId) {
|
|
8229
7437
|
const status = await readQuery(
|
|
8230
7438
|
client,
|
|
@@ -8339,11 +7547,11 @@ async function buildDraft(ctx, args, heading) {
|
|
|
8339
7547
|
].filter((s) => s !== "");
|
|
8340
7548
|
return { target, title: cleanTitle(args.title), body: sections.join("\n\n") };
|
|
8341
7549
|
}
|
|
8342
|
-
function
|
|
7550
|
+
function shellQuote(value) {
|
|
8343
7551
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
8344
7552
|
}
|
|
8345
7553
|
function issueCommand(draft, bodyFile) {
|
|
8346
|
-
return `gh issue create --repo ${draft.target.repo} --title ${
|
|
7554
|
+
return `gh issue create --repo ${draft.target.repo} --title ${shellQuote(draft.title)} --body-file ${bodyFile}`;
|
|
8347
7555
|
}
|
|
8348
7556
|
var ISSUE_BODY_FILE = "./camstack-issue.md";
|
|
8349
7557
|
async function issueDraft(ctx, args) {
|
|
@@ -8426,8 +7634,8 @@ var BENCHMARK_FILES = [
|
|
|
8426
7634
|
"docs/benchmarks/detector-latency.json",
|
|
8427
7635
|
"packages/types/src/pipeline/auto-tune/detector-performance.ts"
|
|
8428
7636
|
];
|
|
8429
|
-
function slug(
|
|
8430
|
-
const s =
|
|
7637
|
+
function slug(text2) {
|
|
7638
|
+
const s = text2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
8431
7639
|
return s === "" ? "change" : s;
|
|
8432
7640
|
}
|
|
8433
7641
|
function prProblem(args) {
|
|
@@ -8450,7 +7658,7 @@ function planFor(args, draft) {
|
|
|
8450
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.`,
|
|
8451
7659
|
"npx vitest run packages/types/src/pipeline/auto-tune/__tests__/detector-performance.spec.ts (it fails until the two files agree)."
|
|
8452
7660
|
] : ["Make the change with a spec that fails first, then passes."],
|
|
8453
|
-
`git commit -am ${
|
|
7661
|
+
`git commit -am ${shellQuote(benchmark ? `docs: measured detector latency for ${args.column ?? ""}` : `fix: ${draft.title}`)}`,
|
|
8454
7662
|
`git push -u origin ${branch}`,
|
|
8455
7663
|
`Save the body below as ${PR_BODY_FILE}, then run the command.`
|
|
8456
7664
|
];
|
|
@@ -8463,7 +7671,7 @@ function planFor(args, draft) {
|
|
|
8463
7671
|
branch,
|
|
8464
7672
|
files,
|
|
8465
7673
|
steps,
|
|
8466
|
-
command: `gh pr create --repo ${draft.target.repo} --head <your-github-user>:${branch} --title ${
|
|
7674
|
+
command: `gh pr create --repo ${draft.target.repo} --head <your-github-user>:${branch} --title ${shellQuote(draft.title)} --body-file ${PR_BODY_FILE}`,
|
|
8467
7675
|
note: "This tool pushes no code and calls no GitHub API; you run these steps with your own git and gh."
|
|
8468
7676
|
};
|
|
8469
7677
|
}
|
|
@@ -8490,20 +7698,20 @@ async function supportDraftPr(ctx, args, confirmToken) {
|
|
|
8490
7698
|
}
|
|
8491
7699
|
|
|
8492
7700
|
// src/mcp/support-tools.ts
|
|
8493
|
-
var ModelId =
|
|
7701
|
+
var ModelId = z29.string().regex(/^[a-z0-9][a-z0-9._-]{1,63}$/);
|
|
8494
7702
|
var DraftShape = {
|
|
8495
7703
|
kind: DraftKindSchema,
|
|
8496
|
-
title:
|
|
8497
|
-
summary:
|
|
8498
|
-
nodeId:
|
|
7704
|
+
title: z29.string().min(3).max(300),
|
|
7705
|
+
summary: z29.string().min(3).max(4e3),
|
|
7706
|
+
nodeId: z29.string().max(100).optional(),
|
|
8499
7707
|
expected: ExpectedAcceleratorSchema.optional(),
|
|
8500
|
-
brand:
|
|
8501
|
-
model:
|
|
8502
|
-
deviceId:
|
|
8503
|
-
packageName:
|
|
8504
|
-
column:
|
|
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(),
|
|
8505
7713
|
cells: BenchmarkCellsSchema.optional(),
|
|
8506
|
-
confirm_token:
|
|
7714
|
+
confirm_token: z29.string().optional()
|
|
8507
7715
|
};
|
|
8508
7716
|
function draftArgs(input) {
|
|
8509
7717
|
return {
|
|
@@ -8524,20 +7732,20 @@ function registerSupportTools(server, ctx, deps = defaultSupportDeps) {
|
|
|
8524
7732
|
server.tool(
|
|
8525
7733
|
"support_benchmark_status",
|
|
8526
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`.",
|
|
8527
|
-
{ nodeId:
|
|
8528
|
-
async ({ nodeId }) =>
|
|
7735
|
+
{ nodeId: z29.string().max(100).optional() },
|
|
7736
|
+
async ({ nodeId }) => text(await supportBenchmarkStatus(ctx, nodeId !== void 0 ? { nodeId } : {}))
|
|
8529
7737
|
);
|
|
8530
7738
|
server.tool(
|
|
8531
7739
|
BENCHMARK_RUN_TOOL,
|
|
8532
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`.",
|
|
8533
7741
|
{
|
|
8534
|
-
nodeId:
|
|
8535
|
-
deviceKey:
|
|
8536
|
-
models:
|
|
8537
|
-
warmRuns:
|
|
8538
|
-
confirm_token:
|
|
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()
|
|
8539
7747
|
},
|
|
8540
|
-
async ({ nodeId, deviceKey, models, warmRuns, confirm_token }) =>
|
|
7748
|
+
async ({ nodeId, deviceKey, models, warmRuns, confirm_token }) => text(
|
|
8541
7749
|
await supportBenchmarkRun(
|
|
8542
7750
|
ctx,
|
|
8543
7751
|
deps,
|
|
@@ -8554,18 +7762,18 @@ function registerSupportTools(server, ctx, deps = defaultSupportDeps) {
|
|
|
8554
7762
|
server.tool(
|
|
8555
7763
|
"support_diagnose_accelerator",
|
|
8556
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.",
|
|
8557
|
-
{ nodeId:
|
|
8558
|
-
async ({ nodeId, expected }) =>
|
|
7765
|
+
{ nodeId: z29.string().min(1).max(100), expected: ExpectedAcceleratorSchema },
|
|
7766
|
+
async ({ nodeId, expected }) => text(await supportDiagnoseAccelerator(ctx, { nodeId, expected }))
|
|
8559
7767
|
);
|
|
8560
7768
|
server.tool(
|
|
8561
7769
|
"support_diagnose_camera",
|
|
8562
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.",
|
|
8563
7771
|
{
|
|
8564
|
-
brand:
|
|
8565
|
-
model:
|
|
8566
|
-
deviceId:
|
|
7772
|
+
brand: z29.string().max(100).optional(),
|
|
7773
|
+
model: z29.string().max(100).optional(),
|
|
7774
|
+
deviceId: z29.number().int().optional()
|
|
8567
7775
|
},
|
|
8568
|
-
async ({ brand, model, deviceId }) =>
|
|
7776
|
+
async ({ brand, model, deviceId }) => text(
|
|
8569
7777
|
await supportDiagnoseCamera(ctx, {
|
|
8570
7778
|
...brand !== void 0 ? { brand } : {},
|
|
8571
7779
|
...model !== void 0 ? { model } : {},
|
|
@@ -8577,13 +7785,13 @@ function registerSupportTools(server, ctx, deps = defaultSupportDeps) {
|
|
|
8577
7785
|
DRAFT_ISSUE_TOOL,
|
|
8578
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.",
|
|
8579
7787
|
DraftShape,
|
|
8580
|
-
async (input) =>
|
|
7788
|
+
async (input) => text(await supportDraftIssue(ctx, deps, draftArgs(input), input.confirm_token))
|
|
8581
7789
|
);
|
|
8582
7790
|
server.tool(
|
|
8583
7791
|
DRAFT_PR_TOOL,
|
|
8584
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.',
|
|
8585
7793
|
DraftShape,
|
|
8586
|
-
async (input) =>
|
|
7794
|
+
async (input) => text(await supportDraftPr(ctx, draftArgs(input), input.confirm_token))
|
|
8587
7795
|
);
|
|
8588
7796
|
}
|
|
8589
7797
|
|