camstack 1.2.213 → 1.2.215
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +6 -0
- package/dist/chunk-37A556YF.js +822 -0
- package/dist/chunk-BYNP7S4W.js +56 -0
- package/dist/chunk-JPGJK5DC.js +75 -0
- package/dist/{chunk-MJJWVAEM.js → chunk-WGHOCSCW.js} +67526 -67068
- package/dist/cli.js +127 -70
- package/dist/doctor-command-S7Q2U6HO.js +227 -0
- package/dist/{launcher-6TBFW332.js → launcher-RUHSCWRY.js} +332159 -330087
- package/dist/{server-EFXGQ7FD.js → server-7C635XXX.js} +241 -1016
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
ALL_CAPABILITY_DEFINITIONS,
|
|
4
|
+
ActionTargetSchema,
|
|
4
5
|
AlarmArmModeSchema,
|
|
5
6
|
AutomationRecipeSchema,
|
|
6
7
|
DETECTOR_PERF,
|
|
@@ -13,13 +14,23 @@ import {
|
|
|
13
14
|
classifyInferenceHardware,
|
|
14
15
|
convertLegacyWhen,
|
|
15
16
|
countWhenLeaves,
|
|
16
|
-
findHubs,
|
|
17
17
|
generateAutomationBlock,
|
|
18
18
|
isReadingQuantity,
|
|
19
19
|
prepareRecipeUnits,
|
|
20
20
|
unitsForQuantity,
|
|
21
21
|
validateRecipeBounds
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-WGHOCSCW.js";
|
|
23
|
+
import {
|
|
24
|
+
IMAGE_TAG_BY_ACCELERATOR,
|
|
25
|
+
MUST_NOT_OMIT,
|
|
26
|
+
NO_BARE_LATEST,
|
|
27
|
+
cliVerdict,
|
|
28
|
+
isOlder,
|
|
29
|
+
registerSetupTools,
|
|
30
|
+
runCommand,
|
|
31
|
+
text
|
|
32
|
+
} from "./chunk-37A556YF.js";
|
|
33
|
+
import "./chunk-JPGJK5DC.js";
|
|
23
34
|
import "./chunk-LMMQX4CK.js";
|
|
24
35
|
|
|
25
36
|
// src/mcp/server.ts
|
|
@@ -29,14 +40,12 @@ import { MutationGate, tryConnectHub } from "@camstack/develop";
|
|
|
29
40
|
|
|
30
41
|
// src/mcp/configure-tools.ts
|
|
31
42
|
import {
|
|
32
|
-
runGated as runGated4
|
|
33
|
-
text as text13
|
|
43
|
+
runGated as runGated4
|
|
34
44
|
} from "@camstack/develop";
|
|
35
|
-
import { z as
|
|
45
|
+
import { z as z24 } from "zod";
|
|
36
46
|
|
|
37
47
|
// src/mcp/configure-automations.ts
|
|
38
48
|
import { z as z4 } from "zod";
|
|
39
|
-
import { text } from "@camstack/develop";
|
|
40
49
|
|
|
41
50
|
// src/mcp/configure-common.ts
|
|
42
51
|
import { runGated } from "@camstack/develop";
|
|
@@ -594,16 +603,29 @@ var ActionAnswerSchema = z4.discriminatedUnion("kind", [
|
|
|
594
603
|
z4.object({ kind: z4.literal("wait"), seconds: z4.number().min(0).max(300) }),
|
|
595
604
|
z4.object({
|
|
596
605
|
kind: z4.literal("cap"),
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
606
|
+
/** WHO the step acts on: named devices, or every device offering `actionId` (optionally in a room / of a type). */
|
|
607
|
+
target: ActionTargetSchema.optional(),
|
|
608
|
+
/** The vocabulary action (`cover.open`); required for a `domain` target. */
|
|
609
|
+
actionId: z4.string().min(1).optional(),
|
|
610
|
+
/** Shorthand for `target: { kind: 'devices', ids }`. */
|
|
611
|
+
deviceId: z4.union([z4.number().int().nonnegative(), z4.array(z4.number().int().nonnegative()).min(1)]).optional(),
|
|
601
612
|
cap: z4.string().min(1),
|
|
602
613
|
method: z4.string().min(1),
|
|
603
614
|
/** A quantity argument is `{ value, unit }`; a bare number is stamped with the declared unit. */
|
|
604
615
|
args: z4.record(z4.string(), z4.unknown()).optional()
|
|
616
|
+
}).refine((a) => a.target !== void 0 || a.deviceId !== void 0, {
|
|
617
|
+
message: "a cap step needs a target (or a deviceId)",
|
|
618
|
+
path: ["target"]
|
|
619
|
+
}).refine((a) => a.target?.kind !== "domain" || a.actionId !== void 0, {
|
|
620
|
+
message: "a domain target needs the vocabulary action it applies to (actionId)",
|
|
621
|
+
path: ["actionId"]
|
|
605
622
|
})
|
|
606
623
|
]);
|
|
624
|
+
function targetOfAnswer(a) {
|
|
625
|
+
if (a.target !== void 0) return a.target;
|
|
626
|
+
const ids = Array.isArray(a.deviceId) ? [...a.deviceId] : a.deviceId === void 0 ? [] : [a.deviceId];
|
|
627
|
+
return { kind: "devices", ids };
|
|
628
|
+
}
|
|
607
629
|
var AutomationTriggerAnswerSchema = z4.object({
|
|
608
630
|
kind: z4.enum(["detection", "device-state", "schedule"]),
|
|
609
631
|
// detection
|
|
@@ -653,7 +675,7 @@ var CONDITIONS_QUESTION = {
|
|
|
653
675
|
};
|
|
654
676
|
var ACTIONS_QUESTION = {
|
|
655
677
|
field: "actions",
|
|
656
|
-
ask: 'What should it do? A list of steps, in order: { kind: "cap",
|
|
678
|
+
ask: 'What should it do? A list of steps, in order: { kind: "cap", target, actionId?, cap, method, args? } or { kind: "wait", seconds }. target is { kind: "devices", ids: [...] } to name devices, or { kind: "domain", roomId?, deviceType? } with the vocabulary actionId (for example "cover.open") to act on every device that offers it \u2014 resolved when the step runs, so a device added later is included. deviceId (a number or a list) is still accepted as shorthand for named devices.'
|
|
657
679
|
};
|
|
658
680
|
function fieldChoices(fields) {
|
|
659
681
|
return fields.map((f) => {
|
|
@@ -941,7 +963,8 @@ async function checkActions(client, actions) {
|
|
|
941
963
|
const cache = /* @__PURE__ */ new Map();
|
|
942
964
|
for (const [idx, action] of actions.entries()) {
|
|
943
965
|
if (action.kind !== "cap") continue;
|
|
944
|
-
const
|
|
966
|
+
const target = targetOfAnswer(action);
|
|
967
|
+
const ids = target.kind === "devices" ? target.ids : [];
|
|
945
968
|
for (const id of ids) {
|
|
946
969
|
let caps = cache.get(id);
|
|
947
970
|
if (caps === void 0) {
|
|
@@ -979,7 +1002,8 @@ function toActions(actions) {
|
|
|
979
1002
|
if (a.kind === "wait") return { kind: "wait", seconds: a.seconds };
|
|
980
1003
|
return {
|
|
981
1004
|
kind: "cap",
|
|
982
|
-
|
|
1005
|
+
target: targetOfAnswer(a),
|
|
1006
|
+
...a.actionId !== void 0 ? { actionId: a.actionId } : {},
|
|
983
1007
|
cap: a.cap,
|
|
984
1008
|
method: a.method,
|
|
985
1009
|
...a.args !== void 0 ? { args: a.args } : {}
|
|
@@ -1023,9 +1047,10 @@ function summarize(trigger, conditions, actions, cooldown) {
|
|
|
1023
1047
|
for (const a of actions) {
|
|
1024
1048
|
if (a.kind === "wait") lines.push(` then wait ${a.seconds}s`);
|
|
1025
1049
|
else {
|
|
1026
|
-
const
|
|
1050
|
+
const t = targetOfAnswer(a);
|
|
1051
|
+
const who = t.kind === "devices" ? `device ${t.ids.join(", ")}` : t.kind === "var" ? `device $${t.name}` : `every device that can ${a.actionId ?? a.method}${t.roomId === void 0 ? "" : ` in ${t.roomId}`}${t.deviceType === void 0 ? "" : ` of type ${t.deviceType}`}`;
|
|
1027
1052
|
lines.push(
|
|
1028
|
-
` then ${a.cap}.${a.method} on
|
|
1053
|
+
` then ${a.cap}.${a.method} on ${who}${a.args ? ` with ${JSON.stringify(a.args)}` : ""}`
|
|
1029
1054
|
);
|
|
1030
1055
|
}
|
|
1031
1056
|
}
|
|
@@ -1236,7 +1261,6 @@ function registerAutomationTools(server, ctx) {
|
|
|
1236
1261
|
}
|
|
1237
1262
|
|
|
1238
1263
|
// src/mcp/configure-calibrate.ts
|
|
1239
|
-
import { text as text2 } from "@camstack/develop";
|
|
1240
1264
|
import { z as z5 } from "zod";
|
|
1241
1265
|
var CALIBRATE_APPLY_TOOL = "configure_calibrate_apply";
|
|
1242
1266
|
var WINDOW_MS = 24 * 36e5;
|
|
@@ -1602,7 +1626,7 @@ function registerCalibrateTools(server, ctx) {
|
|
|
1602
1626
|
"configure_calibrate_camera",
|
|
1603
1627
|
"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
1628
|
{ deviceId: z5.number().int().nonnegative() },
|
|
1605
|
-
async ({ deviceId }) =>
|
|
1629
|
+
async ({ deviceId }) => text(await calibrateCamera(ctx, { deviceId }))
|
|
1606
1630
|
);
|
|
1607
1631
|
server.tool(
|
|
1608
1632
|
CALIBRATE_APPLY_TOOL,
|
|
@@ -1613,13 +1637,12 @@ function registerCalibrateTools(server, ctx) {
|
|
|
1613
1637
|
patch: z5.record(z5.string(), z5.union([z5.number(), z5.string(), z5.boolean()])),
|
|
1614
1638
|
confirm_token: ConfirmTokenSchema
|
|
1615
1639
|
},
|
|
1616
|
-
async ({ deviceId, cap: cap2, patch, confirm_token }) =>
|
|
1640
|
+
async ({ deviceId, cap: cap2, patch, confirm_token }) => text(await calibrateApply(ctx, { deviceId, cap: cap2, patch }, confirm_token))
|
|
1617
1641
|
);
|
|
1618
1642
|
}
|
|
1619
1643
|
|
|
1620
1644
|
// src/mcp/configure-zones-alarm.ts
|
|
1621
1645
|
import { randomUUID } from "crypto";
|
|
1622
|
-
import { text as text3 } from "@camstack/develop";
|
|
1623
1646
|
import { z as z6 } from "zod";
|
|
1624
1647
|
var ZONE_UPSERT_TOOL = "configure_zone_upsert";
|
|
1625
1648
|
var ALARM_SET_TOOL = "configure_alarm_set";
|
|
@@ -1910,7 +1933,7 @@ function registerZonesAlarmTools(server, ctx) {
|
|
|
1910
1933
|
"configure_zones",
|
|
1911
1934
|
"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
1935
|
{ deviceId: z6.number().int().nonnegative() },
|
|
1913
|
-
async ({ deviceId }) =>
|
|
1936
|
+
async ({ deviceId }) => text(await readZonesTool(ctx, { deviceId }))
|
|
1914
1937
|
);
|
|
1915
1938
|
server.tool(
|
|
1916
1939
|
ZONE_UPSERT_TOOL,
|
|
@@ -1923,7 +1946,7 @@ function registerZonesAlarmTools(server, ctx) {
|
|
|
1923
1946
|
points: z6.array(PointSchema).min(2).max(64),
|
|
1924
1947
|
confirm_token: ConfirmTokenSchema
|
|
1925
1948
|
},
|
|
1926
|
-
async ({ deviceId, zoneId, name, kind, points, confirm_token }) =>
|
|
1949
|
+
async ({ deviceId, zoneId, name, kind, points, confirm_token }) => text(
|
|
1927
1950
|
await zoneUpsert(
|
|
1928
1951
|
ctx,
|
|
1929
1952
|
{
|
|
@@ -1941,7 +1964,7 @@ function registerZonesAlarmTools(server, ctx) {
|
|
|
1941
1964
|
"configure_alarm",
|
|
1942
1965
|
"Read the alarm panels of the hub: state, armed mode, the modes each accepts and whether it needs a PIN. Writes nothing.",
|
|
1943
1966
|
{ deviceId: z6.number().int().nonnegative().optional() },
|
|
1944
|
-
async ({ deviceId }) =>
|
|
1967
|
+
async ({ deviceId }) => text(await readAlarm(ctx, deviceId !== void 0 ? { deviceId } : {}))
|
|
1945
1968
|
);
|
|
1946
1969
|
server.tool(
|
|
1947
1970
|
ALARM_SET_TOOL,
|
|
@@ -1952,7 +1975,7 @@ function registerZonesAlarmTools(server, ctx) {
|
|
|
1952
1975
|
mode: AlarmArmModeSchema.optional(),
|
|
1953
1976
|
confirm_token: ConfirmTokenSchema
|
|
1954
1977
|
},
|
|
1955
|
-
async ({ deviceId, action, mode, confirm_token }) =>
|
|
1978
|
+
async ({ deviceId, action, mode, confirm_token }) => text(
|
|
1956
1979
|
await alarmSet(
|
|
1957
1980
|
ctx,
|
|
1958
1981
|
{ deviceId, action, ...mode !== void 0 ? { mode } : {} },
|
|
@@ -1963,7 +1986,6 @@ function registerZonesAlarmTools(server, ctx) {
|
|
|
1963
1986
|
}
|
|
1964
1987
|
|
|
1965
1988
|
// src/mcp/configure-network.ts
|
|
1966
|
-
import { text as text4 } from "@camstack/develop";
|
|
1967
1989
|
import { z as z7 } from "zod";
|
|
1968
1990
|
var REMOTE_ACCESS_SET_TOOL = "configure_remote_access_set";
|
|
1969
1991
|
var RemoteAddonSchema = z7.enum(["cloudflare-tunnel", "cloudflare-turn", "static-turn"]);
|
|
@@ -2147,7 +2169,7 @@ function registerNetworkTools(server, ctx) {
|
|
|
2147
2169
|
"configure_remote_access",
|
|
2148
2170
|
"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
2171
|
{},
|
|
2150
|
-
async () =>
|
|
2172
|
+
async () => text(await readRemoteAccess(ctx))
|
|
2151
2173
|
);
|
|
2152
2174
|
server.tool(
|
|
2153
2175
|
REMOTE_ACCESS_SET_TOOL,
|
|
@@ -2157,12 +2179,11 @@ function registerNetworkTools(server, ctx) {
|
|
|
2157
2179
|
patch: z7.record(z7.string(), z7.union([z7.string(), z7.number(), z7.boolean()])),
|
|
2158
2180
|
confirm_token: ConfirmTokenSchema
|
|
2159
2181
|
},
|
|
2160
|
-
async ({ addonId, patch, confirm_token }) =>
|
|
2182
|
+
async ({ addonId, patch, confirm_token }) => text(await remoteAccessSet(ctx, { addonId, patch }, confirm_token))
|
|
2161
2183
|
);
|
|
2162
2184
|
}
|
|
2163
2185
|
|
|
2164
2186
|
// src/mcp/configure-exports.ts
|
|
2165
|
-
import { text as text5 } from "@camstack/develop";
|
|
2166
2187
|
import { z as z8 } from "zod";
|
|
2167
2188
|
var EXPORT_SET_TOOL = "configure_export_set";
|
|
2168
2189
|
var ProvidersSchema = z8.array(z8.looseObject({ addonId: z8.string() }));
|
|
@@ -2325,7 +2346,7 @@ function registerExportTools(server, ctx) {
|
|
|
2325
2346
|
"configure_exports",
|
|
2326
2347
|
"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
2348
|
{ deviceId: z8.number().int().nonnegative().optional() },
|
|
2328
|
-
async ({ deviceId }) =>
|
|
2349
|
+
async ({ deviceId }) => text(await readExports(ctx, deviceId !== void 0 ? { deviceId } : {}))
|
|
2329
2350
|
);
|
|
2330
2351
|
server.tool(
|
|
2331
2352
|
EXPORT_SET_TOOL,
|
|
@@ -2336,18 +2357,17 @@ function registerExportTools(server, ctx) {
|
|
|
2336
2357
|
exposed: z8.boolean(),
|
|
2337
2358
|
confirm_token: ConfirmTokenSchema
|
|
2338
2359
|
},
|
|
2339
|
-
async ({ addonId, deviceId, exposed, confirm_token }) =>
|
|
2360
|
+
async ({ addonId, deviceId, exposed, confirm_token }) => text(await exportSet(ctx, { addonId, deviceId, exposed }, confirm_token))
|
|
2340
2361
|
);
|
|
2341
2362
|
server.tool(
|
|
2342
2363
|
"configure_ha_guide",
|
|
2343
2364
|
"Read-only guide for showing CamStack cameras in Home Assistant, including the HACS route. Needs no hub.",
|
|
2344
2365
|
{},
|
|
2345
|
-
async () =>
|
|
2366
|
+
async () => text({ status: "ok", guide: HA_GUIDE })
|
|
2346
2367
|
);
|
|
2347
2368
|
}
|
|
2348
2369
|
|
|
2349
2370
|
// src/mcp/configure-oidc.ts
|
|
2350
|
-
import { text as text6 } from "@camstack/develop";
|
|
2351
2371
|
import { z as z10 } from "zod";
|
|
2352
2372
|
|
|
2353
2373
|
// src/mcp/configure-oidc-providers.ts
|
|
@@ -2836,13 +2856,12 @@ function registerOidcTools(server, ctx) {
|
|
|
2836
2856
|
OIDC_TOOL,
|
|
2837
2857
|
"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
2858
|
OIDC_TOOL_SHAPE,
|
|
2839
|
-
async ({ confirm_token, ...args }) =>
|
|
2859
|
+
async ({ confirm_token, ...args }) => text(await configureOidc(ctx, args, confirm_token))
|
|
2840
2860
|
);
|
|
2841
2861
|
}
|
|
2842
2862
|
|
|
2843
2863
|
// src/mcp/configure-nc-rules.ts
|
|
2844
2864
|
import { z as z11 } from "zod";
|
|
2845
|
-
import { text as text7 } from "@camstack/develop";
|
|
2846
2865
|
var TIME = /^\d{1,2}:\d{2}$/;
|
|
2847
2866
|
var ALL_DAYS = [0, 1, 2, 3, 4, 5, 6];
|
|
2848
2867
|
var QuietHoursAnswerSchema = z11.object({
|
|
@@ -3188,7 +3207,7 @@ function registerNcRuleTools(server, ctx) {
|
|
|
3188
3207
|
"configure_notification_rule_questionnaire",
|
|
3189
3208
|
"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
3209
|
{ answers: NcRuleAnswersSchema },
|
|
3191
|
-
async ({ answers }) =>
|
|
3210
|
+
async ({ answers }) => text(await ncRuleQuestionnaire(ctx, answers))
|
|
3192
3211
|
);
|
|
3193
3212
|
server.tool(
|
|
3194
3213
|
SAVE_RULE_TOOL,
|
|
@@ -3198,7 +3217,7 @@ function registerNcRuleTools(server, ctx) {
|
|
|
3198
3217
|
ruleId: z11.string().optional(),
|
|
3199
3218
|
confirm_token: ConfirmTokenSchema
|
|
3200
3219
|
},
|
|
3201
|
-
async ({ rule, ruleId, confirm_token }) =>
|
|
3220
|
+
async ({ rule, ruleId, confirm_token }) => text(
|
|
3202
3221
|
await saveRule(ctx, { rule, ...ruleId !== void 0 ? { ruleId } : {} }, confirm_token)
|
|
3203
3222
|
)
|
|
3204
3223
|
);
|
|
@@ -3216,9 +3235,6 @@ function registerConfigureP3bTools(server, ctx) {
|
|
|
3216
3235
|
}
|
|
3217
3236
|
|
|
3218
3237
|
// src/mcp/configure-addons.ts
|
|
3219
|
-
import {
|
|
3220
|
-
text as text8
|
|
3221
|
-
} from "@camstack/develop";
|
|
3222
3238
|
import { z as z13 } from "zod";
|
|
3223
3239
|
|
|
3224
3240
|
// src/mcp/configure-support.ts
|
|
@@ -3873,7 +3889,7 @@ function registerAddonTools(server, ctx) {
|
|
|
3873
3889
|
query: z13.string().max(100).optional(),
|
|
3874
3890
|
capability: z13.string().max(100).optional()
|
|
3875
3891
|
},
|
|
3876
|
-
async ({ query, capability }) =>
|
|
3892
|
+
async ({ query, capability }) => text(
|
|
3877
3893
|
await configureAddonsSearch(ctx, {
|
|
3878
3894
|
...query !== void 0 ? { query } : {},
|
|
3879
3895
|
...capability !== void 0 ? { capability } : {}
|
|
@@ -3888,7 +3904,7 @@ function registerAddonTools(server, ctx) {
|
|
|
3888
3904
|
version: z13.string().max(64).optional(),
|
|
3889
3905
|
confirm_token: z13.string().optional()
|
|
3890
3906
|
},
|
|
3891
|
-
async ({ packageName, version, confirm_token }) =>
|
|
3907
|
+
async ({ packageName, version, confirm_token }) => text(
|
|
3892
3908
|
await installAddon(
|
|
3893
3909
|
ctx,
|
|
3894
3910
|
{ packageName, ...version !== void 0 ? { version } : {} },
|
|
@@ -3905,7 +3921,7 @@ function registerAddonTools(server, ctx) {
|
|
|
3905
3921
|
nodeId: z13.string().max(100).optional(),
|
|
3906
3922
|
confirm_token: z13.string().optional()
|
|
3907
3923
|
},
|
|
3908
|
-
async ({ packageName, version, nodeId, confirm_token }) =>
|
|
3924
|
+
async ({ packageName, version, nodeId, confirm_token }) => text(
|
|
3909
3925
|
await updateAddon(
|
|
3910
3926
|
ctx,
|
|
3911
3927
|
{
|
|
@@ -3926,7 +3942,7 @@ function registerAddonTools(server, ctx) {
|
|
|
3926
3942
|
everywhere: z13.boolean().optional(),
|
|
3927
3943
|
confirm_token: z13.string().optional()
|
|
3928
3944
|
},
|
|
3929
|
-
async ({ packageName, nodeId, everywhere, confirm_token }) =>
|
|
3945
|
+
async ({ packageName, nodeId, everywhere, confirm_token }) => text(
|
|
3930
3946
|
await uninstallAddon(
|
|
3931
3947
|
ctx,
|
|
3932
3948
|
{
|
|
@@ -3941,9 +3957,6 @@ function registerAddonTools(server, ctx) {
|
|
|
3941
3957
|
}
|
|
3942
3958
|
|
|
3943
3959
|
// src/mcp/configure-cameras.ts
|
|
3944
|
-
import {
|
|
3945
|
-
text as text9
|
|
3946
|
-
} from "@camstack/develop";
|
|
3947
3960
|
import { z as z14 } from "zod";
|
|
3948
3961
|
var UNSUPPORTED_HINT = "use support_diagnose_camera / support_draft_issue";
|
|
3949
3962
|
var MAX_PROVIDERS = 40;
|
|
@@ -4270,7 +4283,7 @@ function registerCameraTools(server, ctx) {
|
|
|
4270
4283
|
"configure_camera_providers",
|
|
4271
4284
|
"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
4285
|
{ query: z14.string().max(100).optional() },
|
|
4273
|
-
async ({ query }) =>
|
|
4286
|
+
async ({ query }) => text(await configureCameraProviders(ctx, query))
|
|
4274
4287
|
);
|
|
4275
4288
|
server.tool(
|
|
4276
4289
|
"configure_camera_discover",
|
|
@@ -4279,7 +4292,7 @@ function registerCameraTools(server, ctx) {
|
|
|
4279
4292
|
addonId: z14.string().min(1),
|
|
4280
4293
|
params: z14.record(z14.string(), z14.unknown()).optional()
|
|
4281
4294
|
},
|
|
4282
|
-
async ({ addonId, params }) =>
|
|
4295
|
+
async ({ addonId, params }) => text(
|
|
4283
4296
|
await configureCameraDiscover(ctx, {
|
|
4284
4297
|
addonId,
|
|
4285
4298
|
...params !== void 0 ? { params } : {}
|
|
@@ -4294,14 +4307,11 @@ function registerCameraTools(server, ctx) {
|
|
|
4294
4307
|
config: z14.record(z14.string(), z14.unknown()),
|
|
4295
4308
|
confirm_token: z14.string().optional()
|
|
4296
4309
|
},
|
|
4297
|
-
async ({ addonId, config, confirm_token }) =>
|
|
4310
|
+
async ({ addonId, config, confirm_token }) => text(await addCamera(ctx, { addonId, config }, confirm_token))
|
|
4298
4311
|
);
|
|
4299
4312
|
}
|
|
4300
4313
|
|
|
4301
4314
|
// src/mcp/configure-notifiers.ts
|
|
4302
|
-
import {
|
|
4303
|
-
text as text10
|
|
4304
|
-
} from "@camstack/develop";
|
|
4305
4315
|
import { z as z15 } from "zod";
|
|
4306
4316
|
var MAX_TARGETS = 50;
|
|
4307
4317
|
var TargetKindShape = z15.looseObject({
|
|
@@ -4604,13 +4614,13 @@ function registerNotifierTools(server, ctx) {
|
|
|
4604
4614
|
"configure_notifiers",
|
|
4605
4615
|
"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
4616
|
{},
|
|
4607
|
-
async () =>
|
|
4617
|
+
async () => text(await configureNotifiers(ctx))
|
|
4608
4618
|
);
|
|
4609
4619
|
server.tool(
|
|
4610
4620
|
"configure_notification_targets",
|
|
4611
4621
|
"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
4622
|
{},
|
|
4613
|
-
async () =>
|
|
4623
|
+
async () => text(await configureNotificationTargets(ctx))
|
|
4614
4624
|
);
|
|
4615
4625
|
server.tool(
|
|
4616
4626
|
UPSERT_TARGET_TOOL,
|
|
@@ -4623,7 +4633,7 @@ function registerNotifierTools(server, ctx) {
|
|
|
4623
4633
|
enabled: z15.boolean().optional(),
|
|
4624
4634
|
confirm_token: z15.string().optional()
|
|
4625
4635
|
},
|
|
4626
|
-
async ({ targetId, kind, name, config, enabled, confirm_token }) =>
|
|
4636
|
+
async ({ targetId, kind, name, config, enabled, confirm_token }) => text(
|
|
4627
4637
|
await upsertNotificationTarget(
|
|
4628
4638
|
ctx,
|
|
4629
4639
|
{
|
|
@@ -4641,13 +4651,13 @@ function registerNotifierTools(server, ctx) {
|
|
|
4641
4651
|
TEST_TARGET_TOOL,
|
|
4642
4652
|
"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
4653
|
{ targetId: z15.string().min(1), confirm_token: z15.string().optional() },
|
|
4644
|
-
async ({ targetId, confirm_token }) =>
|
|
4654
|
+
async ({ targetId, confirm_token }) => text(await testNotificationTarget(ctx, { targetId }, confirm_token))
|
|
4645
4655
|
);
|
|
4646
4656
|
}
|
|
4647
4657
|
|
|
4648
4658
|
// src/mcp/ops-tools.ts
|
|
4649
|
-
import { z as
|
|
4650
|
-
import { jwtClaims
|
|
4659
|
+
import { z as z23 } from "zod";
|
|
4660
|
+
import { jwtClaims } from "@camstack/develop";
|
|
4651
4661
|
|
|
4652
4662
|
// src/mcp/ops-accelerators.ts
|
|
4653
4663
|
import { z as z16 } from "zod";
|
|
@@ -5540,7 +5550,7 @@ async function opsStatus(ctx) {
|
|
|
5540
5550
|
import {
|
|
5541
5551
|
runGated as runGated3
|
|
5542
5552
|
} from "@camstack/develop";
|
|
5543
|
-
import { z as
|
|
5553
|
+
import { z as z21 } from "zod";
|
|
5544
5554
|
|
|
5545
5555
|
// src/mcp/ops-update-model.ts
|
|
5546
5556
|
import { z as z20 } from "zod";
|
|
@@ -5713,790 +5723,6 @@ function addonDrift(installations) {
|
|
|
5713
5723
|
return drift;
|
|
5714
5724
|
}
|
|
5715
5725
|
|
|
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
5726
|
// src/mcp/ops-update-apply.ts
|
|
6501
5727
|
var UPDATE_APPLY_TOOL = "ops_update_apply";
|
|
6502
5728
|
function timing(deps) {
|
|
@@ -6549,7 +5775,7 @@ async function readPlan(client, args, ctx, deps) {
|
|
|
6549
5775
|
wrapper: wrapperVerdict(status.value)
|
|
6550
5776
|
});
|
|
6551
5777
|
}
|
|
6552
|
-
const updates = args.addons === false ? null : await readQuery(client, "addons.listUpdates", {},
|
|
5778
|
+
const updates = args.addons === false ? null : await readQuery(client, "addons.listUpdates", {}, z21.array(AddonUpdateShape));
|
|
6553
5779
|
const cli = args.cli === false ? null : await cliVerdict(ctx.cliVersion, ctx.fetch, deps.entryPath ?? process.argv[1] ?? "");
|
|
6554
5780
|
const hub = plans.find((p) => p.node.isHub === true) ?? null;
|
|
6555
5781
|
return {
|
|
@@ -6865,14 +6091,14 @@ async function opsUpdateApply(ctx, args, confirmToken, deps = {}) {
|
|
|
6865
6091
|
}
|
|
6866
6092
|
|
|
6867
6093
|
// src/mcp/ops-update-check.ts
|
|
6868
|
-
import { z as
|
|
6094
|
+
import { z as z22 } from "zod";
|
|
6869
6095
|
var ADDON_LIMIT = 30;
|
|
6870
6096
|
async function opsUpdateCheck(ctx, deps = {}) {
|
|
6871
6097
|
return withAdminHub(ctx, async ({ client, server }) => {
|
|
6872
6098
|
const unavailable = {};
|
|
6873
6099
|
const [topology, updates, installs, cli] = await Promise.all([
|
|
6874
6100
|
readTopology(client),
|
|
6875
|
-
readQuery(client, "addons.listUpdates", {},
|
|
6101
|
+
readQuery(client, "addons.listUpdates", {}, z22.array(AddonUpdateShape)),
|
|
6876
6102
|
readInstallations(client),
|
|
6877
6103
|
cliVerdict(ctx.cliVersion, ctx.fetch, deps.entryPath ?? process.argv[1] ?? "")
|
|
6878
6104
|
]);
|
|
@@ -6990,40 +6216,40 @@ function registerOpsTools(server, ctx) {
|
|
|
6990
6216
|
"ops_whoami",
|
|
6991
6217
|
"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
6218
|
{},
|
|
6993
|
-
async () =>
|
|
6219
|
+
async () => text(await whoami(ctx))
|
|
6994
6220
|
);
|
|
6995
6221
|
server.tool(
|
|
6996
6222
|
"ops_status",
|
|
6997
6223
|
'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
6224
|
{},
|
|
6999
|
-
async () =>
|
|
6225
|
+
async () => text(await opsStatus(ctx))
|
|
7000
6226
|
);
|
|
7001
6227
|
server.tool(
|
|
7002
6228
|
"ops_device",
|
|
7003
6229
|
"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 }) =>
|
|
6230
|
+
{ deviceId: z23.number().int() },
|
|
6231
|
+
async ({ deviceId }) => text(await opsDevice(ctx, deviceId))
|
|
7006
6232
|
);
|
|
7007
6233
|
server.tool(
|
|
7008
6234
|
"ops_unhealthy_devices",
|
|
7009
6235
|
"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
6236
|
{},
|
|
7011
|
-
async () =>
|
|
6237
|
+
async () => text(await opsUnhealthyDevices(ctx))
|
|
7012
6238
|
);
|
|
7013
6239
|
server.tool(
|
|
7014
6240
|
"ops_logs",
|
|
7015
6241
|
"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
6242
|
{
|
|
7017
|
-
deviceId:
|
|
7018
|
-
addonId:
|
|
7019
|
-
nodeId:
|
|
7020
|
-
level:
|
|
7021
|
-
windowMinutes:
|
|
7022
|
-
text:
|
|
7023
|
-
around:
|
|
7024
|
-
contextLines:
|
|
6243
|
+
deviceId: z23.number().int().optional(),
|
|
6244
|
+
addonId: z23.string().optional(),
|
|
6245
|
+
nodeId: z23.string().optional(),
|
|
6246
|
+
level: z23.enum(["debug", "info", "warn", "error"]).optional(),
|
|
6247
|
+
windowMinutes: z23.number().int().min(1).max(1440).optional(),
|
|
6248
|
+
text: z23.string().max(200).optional(),
|
|
6249
|
+
around: z23.string().max(200).optional(),
|
|
6250
|
+
contextLines: z23.number().int().min(1).max(50).optional()
|
|
7025
6251
|
},
|
|
7026
|
-
async (args) =>
|
|
6252
|
+
async (args) => text(
|
|
7027
6253
|
await opsLogs(ctx, {
|
|
7028
6254
|
...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
|
|
7029
6255
|
...args.addonId ? { addonId: args.addonId } : {},
|
|
@@ -7040,14 +6266,14 @@ function registerOpsTools(server, ctx) {
|
|
|
7040
6266
|
"ops_events",
|
|
7041
6267
|
"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
6268
|
{
|
|
7043
|
-
deviceId:
|
|
7044
|
-
className:
|
|
7045
|
-
zone:
|
|
7046
|
-
text:
|
|
7047
|
-
windowMinutes:
|
|
7048
|
-
limit:
|
|
6269
|
+
deviceId: z23.number().int().optional(),
|
|
6270
|
+
className: z23.string().optional(),
|
|
6271
|
+
zone: z23.string().optional(),
|
|
6272
|
+
text: z23.string().max(200).optional(),
|
|
6273
|
+
windowMinutes: z23.number().int().min(1).max(10080).optional(),
|
|
6274
|
+
limit: z23.number().int().min(1).max(50).optional()
|
|
7049
6275
|
},
|
|
7050
|
-
async (args) =>
|
|
6276
|
+
async (args) => text(
|
|
7051
6277
|
await opsEvents(ctx, {
|
|
7052
6278
|
...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
|
|
7053
6279
|
...args.className ? { className: args.className } : {},
|
|
@@ -7061,31 +6287,31 @@ function registerOpsTools(server, ctx) {
|
|
|
7061
6287
|
server.tool(
|
|
7062
6288
|
"ops_track",
|
|
7063
6289
|
"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 }) =>
|
|
6290
|
+
{ deviceId: z23.number().int(), trackId: z23.string().min(1) },
|
|
6291
|
+
async ({ deviceId, trackId }) => text(await opsTrack(ctx, { deviceId, trackId }))
|
|
7066
6292
|
);
|
|
7067
6293
|
server.tool(
|
|
7068
6294
|
"ops_accelerators",
|
|
7069
6295
|
'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
6296
|
{},
|
|
7071
|
-
async () =>
|
|
6297
|
+
async () => text(await opsAccelerators(ctx))
|
|
7072
6298
|
);
|
|
7073
6299
|
server.tool(
|
|
7074
6300
|
"ops_update_check",
|
|
7075
6301
|
'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
6302
|
{},
|
|
7077
|
-
async () =>
|
|
6303
|
+
async () => text(await opsUpdateCheck(ctx))
|
|
7078
6304
|
);
|
|
7079
6305
|
server.tool(
|
|
7080
6306
|
UPDATE_APPLY_TOOL,
|
|
7081
6307
|
"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
6308
|
{
|
|
7083
|
-
version:
|
|
7084
|
-
addons:
|
|
7085
|
-
cli:
|
|
7086
|
-
confirm_token:
|
|
6309
|
+
version: z23.string().min(1).optional().describe("Server target; default = the hub's latest."),
|
|
6310
|
+
addons: z23.boolean().optional().describe("Update addons too (default true)."),
|
|
6311
|
+
cli: z23.boolean().optional().describe("Update this CLI last (default true)."),
|
|
6312
|
+
confirm_token: z23.string().optional()
|
|
7087
6313
|
},
|
|
7088
|
-
async ({ version, addons, cli, confirm_token }) =>
|
|
6314
|
+
async ({ version, addons, cli, confirm_token }) => text(
|
|
7089
6315
|
await opsUpdateApply(
|
|
7090
6316
|
ctx,
|
|
7091
6317
|
{
|
|
@@ -7100,7 +6326,7 @@ function registerOpsTools(server, ctx) {
|
|
|
7100
6326
|
}
|
|
7101
6327
|
|
|
7102
6328
|
// src/mcp/configure-tools.ts
|
|
7103
|
-
var CurrentDeviceSchema =
|
|
6329
|
+
var CurrentDeviceSchema = z24.object({ name: z24.string() }).nullable();
|
|
7104
6330
|
var RENAME_DEVICE_TOOL = "configure_rename_device";
|
|
7105
6331
|
function renameDeviceSpec(client) {
|
|
7106
6332
|
return {
|
|
@@ -7162,11 +6388,11 @@ function registerConfigureTools(server, ctx) {
|
|
|
7162
6388
|
RENAME_DEVICE_TOOL,
|
|
7163
6389
|
"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
6390
|
{
|
|
7165
|
-
deviceId:
|
|
7166
|
-
name:
|
|
7167
|
-
confirm_token:
|
|
6391
|
+
deviceId: z24.number().int(),
|
|
6392
|
+
name: z24.string().min(1),
|
|
6393
|
+
confirm_token: z24.string().optional()
|
|
7168
6394
|
},
|
|
7169
|
-
async ({ deviceId, name, confirm_token }) =>
|
|
6395
|
+
async ({ deviceId, name, confirm_token }) => text(await renameDevice(ctx, { deviceId, name }, confirm_token))
|
|
7170
6396
|
);
|
|
7171
6397
|
registerAddonTools(server, ctx);
|
|
7172
6398
|
registerCameraTools(server, ctx);
|
|
@@ -7175,11 +6401,10 @@ function registerConfigureTools(server, ctx) {
|
|
|
7175
6401
|
}
|
|
7176
6402
|
|
|
7177
6403
|
// src/mcp/support-tools.ts
|
|
7178
|
-
import {
|
|
7179
|
-
import { z as z33 } from "zod";
|
|
6404
|
+
import { z as z29 } from "zod";
|
|
7180
6405
|
|
|
7181
6406
|
// src/mcp/support-accelerator.ts
|
|
7182
|
-
import { z as
|
|
6407
|
+
import { z as z25 } from "zod";
|
|
7183
6408
|
var EXPECTED_ACCELERATORS = [
|
|
7184
6409
|
"intel-igpu",
|
|
7185
6410
|
"intel-npu",
|
|
@@ -7191,23 +6416,23 @@ var EXPECTED_ACCELERATORS = [
|
|
|
7191
6416
|
"apple-ane",
|
|
7192
6417
|
"nvidia"
|
|
7193
6418
|
];
|
|
7194
|
-
var ExpectedAcceleratorSchema =
|
|
7195
|
-
var HardwareSchema2 =
|
|
7196
|
-
platform:
|
|
7197
|
-
arch:
|
|
7198
|
-
cpuModel:
|
|
7199
|
-
gpu:
|
|
7200
|
-
npu:
|
|
6419
|
+
var ExpectedAcceleratorSchema = z25.enum(EXPECTED_ACCELERATORS);
|
|
6420
|
+
var HardwareSchema2 = z25.looseObject({
|
|
6421
|
+
platform: z25.enum(["darwin", "linux", "win32"]),
|
|
6422
|
+
arch: z25.enum(["arm64", "x64"]),
|
|
6423
|
+
cpuModel: z25.string(),
|
|
6424
|
+
gpu: z25.looseObject({ type: z25.string(), name: z25.string() }).nullable(),
|
|
6425
|
+
npu: z25.looseObject({ type: z25.string() }).nullable()
|
|
7201
6426
|
});
|
|
7202
|
-
var InferenceDevicesSchema2 =
|
|
7203
|
-
reachable:
|
|
7204
|
-
devices:
|
|
7205
|
-
|
|
7206
|
-
key:
|
|
7207
|
-
available:
|
|
7208
|
-
presence:
|
|
7209
|
-
enabled:
|
|
7210
|
-
exclusion:
|
|
6427
|
+
var InferenceDevicesSchema2 = z25.looseObject({
|
|
6428
|
+
reachable: z25.boolean(),
|
|
6429
|
+
devices: z25.array(
|
|
6430
|
+
z25.looseObject({
|
|
6431
|
+
key: z25.string(),
|
|
6432
|
+
available: z25.boolean(),
|
|
6433
|
+
presence: z25.enum(["present", "absent", "unknown"]).optional(),
|
|
6434
|
+
enabled: z25.boolean(),
|
|
6435
|
+
exclusion: z25.string().nullable()
|
|
7211
6436
|
})
|
|
7212
6437
|
)
|
|
7213
6438
|
});
|
|
@@ -7403,11 +6628,11 @@ function diagnoseAccelerator(input) {
|
|
|
7403
6628
|
}
|
|
7404
6629
|
const matched = devices.filter((d) => expectation.deviceKeys(d.key));
|
|
7405
6630
|
const matchedKeys = matched.map((d) => d.key);
|
|
7406
|
-
const
|
|
7407
|
-
if (
|
|
6631
|
+
const ready = matched.filter((d) => d.available && d.enabled && d.exclusion === null);
|
|
6632
|
+
if (ready.length > 0) {
|
|
7408
6633
|
return finish(
|
|
7409
6634
|
"ok",
|
|
7410
|
-
`The node offers ${
|
|
6635
|
+
`The node offers ${ready.map((d) => d.key).join(", ")}, available and enabled.`,
|
|
7411
6636
|
matchedKeys,
|
|
7412
6637
|
[],
|
|
7413
6638
|
null
|
|
@@ -7486,7 +6711,7 @@ async function supportDiagnoseAccelerator(ctx, args) {
|
|
|
7486
6711
|
import {
|
|
7487
6712
|
runGated as runGated5
|
|
7488
6713
|
} from "@camstack/develop";
|
|
7489
|
-
import { z as
|
|
6714
|
+
import { z as z26 } from "zod";
|
|
7490
6715
|
var BENCHMARK_RUN_TOOL = "support_benchmark_run";
|
|
7491
6716
|
var AUTO_TUNE_MODELS = [
|
|
7492
6717
|
"yolov9t-320",
|
|
@@ -7508,64 +6733,64 @@ var CONTRIBUTE_HOW = [
|
|
|
7508
6733
|
"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
6734
|
'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
6735
|
];
|
|
7511
|
-
var HardwareSchema3 =
|
|
7512
|
-
platform:
|
|
7513
|
-
arch:
|
|
7514
|
-
cpuModel:
|
|
7515
|
-
cpuCores:
|
|
7516
|
-
totalRAM_MB:
|
|
7517
|
-
availableRAM_MB:
|
|
7518
|
-
gpu:
|
|
7519
|
-
type:
|
|
7520
|
-
name:
|
|
6736
|
+
var HardwareSchema3 = z26.looseObject({
|
|
6737
|
+
platform: z26.enum(["darwin", "linux", "win32"]),
|
|
6738
|
+
arch: z26.enum(["arm64", "x64"]),
|
|
6739
|
+
cpuModel: z26.string(),
|
|
6740
|
+
cpuCores: z26.number(),
|
|
6741
|
+
totalRAM_MB: z26.number(),
|
|
6742
|
+
availableRAM_MB: z26.number(),
|
|
6743
|
+
gpu: z26.looseObject({
|
|
6744
|
+
type: z26.enum(["nvidia", "amd", "intel", "apple"]),
|
|
6745
|
+
name: z26.string()
|
|
7521
6746
|
}).nullable(),
|
|
7522
|
-
npu:
|
|
6747
|
+
npu: z26.looseObject({ type: z26.enum(["apple-ane", "intel-npu", "amd-xdna"]) }).nullable()
|
|
7523
6748
|
});
|
|
7524
|
-
var InferenceDevicesSchema3 =
|
|
7525
|
-
reachable:
|
|
7526
|
-
devices:
|
|
7527
|
-
|
|
7528
|
-
key:
|
|
7529
|
-
backend:
|
|
7530
|
-
available:
|
|
7531
|
-
enabled:
|
|
6749
|
+
var InferenceDevicesSchema3 = z26.looseObject({
|
|
6750
|
+
reachable: z26.boolean(),
|
|
6751
|
+
devices: z26.array(
|
|
6752
|
+
z26.looseObject({
|
|
6753
|
+
key: z26.string(),
|
|
6754
|
+
backend: z26.string(),
|
|
6755
|
+
available: z26.boolean(),
|
|
6756
|
+
enabled: z26.boolean()
|
|
7532
6757
|
})
|
|
7533
6758
|
)
|
|
7534
6759
|
});
|
|
7535
|
-
var RunStatusSchema =
|
|
7536
|
-
run:
|
|
7537
|
-
runId:
|
|
7538
|
-
nodeId:
|
|
7539
|
-
state:
|
|
7540
|
-
endReason:
|
|
7541
|
-
error:
|
|
6760
|
+
var RunStatusSchema = z26.looseObject({
|
|
6761
|
+
run: z26.looseObject({
|
|
6762
|
+
runId: z26.string(),
|
|
6763
|
+
nodeId: z26.string(),
|
|
6764
|
+
state: z26.string(),
|
|
6765
|
+
endReason: z26.string().nullable().optional(),
|
|
6766
|
+
error: z26.string().nullable().optional()
|
|
7542
6767
|
}).nullable()
|
|
7543
6768
|
});
|
|
7544
|
-
var ModelListSchema =
|
|
7545
|
-
|
|
7546
|
-
id:
|
|
7547
|
-
formats:
|
|
6769
|
+
var ModelListSchema = z26.array(
|
|
6770
|
+
z26.looseObject({
|
|
6771
|
+
id: z26.string(),
|
|
6772
|
+
formats: z26.record(z26.string(), z26.looseObject({ downloaded: z26.boolean().optional() })).optional()
|
|
7548
6773
|
})
|
|
7549
6774
|
);
|
|
7550
|
-
var PipelineTestSchema =
|
|
7551
|
-
kind:
|
|
7552
|
-
detections:
|
|
7553
|
-
|
|
7554
|
-
debug:
|
|
7555
|
-
stepLatencyMs:
|
|
6775
|
+
var PipelineTestSchema = z26.looseObject({
|
|
6776
|
+
kind: z26.string().optional(),
|
|
6777
|
+
detections: z26.array(
|
|
6778
|
+
z26.looseObject({
|
|
6779
|
+
debug: z26.looseObject({
|
|
6780
|
+
stepLatencyMs: z26.record(z26.string(), z26.number()).optional()
|
|
7556
6781
|
}).optional()
|
|
7557
6782
|
})
|
|
7558
6783
|
).optional(),
|
|
7559
|
-
discarded:
|
|
7560
|
-
debug:
|
|
7561
|
-
stepTimings:
|
|
7562
|
-
|
|
7563
|
-
source:
|
|
7564
|
-
ms:
|
|
6784
|
+
discarded: z26.array(z26.unknown()).optional(),
|
|
6785
|
+
debug: z26.looseObject({
|
|
6786
|
+
stepTimings: z26.array(
|
|
6787
|
+
z26.looseObject({
|
|
6788
|
+
source: z26.string().optional(),
|
|
6789
|
+
ms: z26.number().optional()
|
|
7565
6790
|
})
|
|
7566
6791
|
).optional(),
|
|
7567
|
-
predictMs:
|
|
7568
|
-
totalInferenceMs:
|
|
6792
|
+
predictMs: z26.number().optional(),
|
|
6793
|
+
totalInferenceMs: z26.number().optional()
|
|
7569
6794
|
}).optional()
|
|
7570
6795
|
});
|
|
7571
6796
|
var FORMAT_BY_BACKEND = {
|
|
@@ -7852,15 +7077,15 @@ async function supportBenchmarkRun(ctx, deps, args, confirmToken) {
|
|
|
7852
7077
|
}
|
|
7853
7078
|
return runGated5(ctx.gate, benchmarkRunSpec(outcome.connection.client, deps), args, confirmToken);
|
|
7854
7079
|
}
|
|
7855
|
-
var ResultsSchema =
|
|
7856
|
-
|
|
7857
|
-
nodeId:
|
|
7858
|
-
cell:
|
|
7859
|
-
phase:
|
|
7860
|
-
deviceKey:
|
|
7861
|
-
modelId:
|
|
7862
|
-
p50Ms:
|
|
7863
|
-
fps:
|
|
7080
|
+
var ResultsSchema = z26.array(
|
|
7081
|
+
z26.looseObject({
|
|
7082
|
+
nodeId: z26.string(),
|
|
7083
|
+
cell: z26.looseObject({
|
|
7084
|
+
phase: z26.string(),
|
|
7085
|
+
deviceKey: z26.string(),
|
|
7086
|
+
modelId: z26.string(),
|
|
7087
|
+
p50Ms: z26.number().optional(),
|
|
7088
|
+
fps: z26.number().optional()
|
|
7864
7089
|
})
|
|
7865
7090
|
})
|
|
7866
7091
|
);
|
|
@@ -7913,20 +7138,20 @@ async function supportBenchmarkStatus(ctx, args) {
|
|
|
7913
7138
|
}
|
|
7914
7139
|
|
|
7915
7140
|
// src/mcp/support-camera.ts
|
|
7916
|
-
import { z as
|
|
7917
|
-
var DeviceSchema2 =
|
|
7918
|
-
id:
|
|
7919
|
-
name:
|
|
7920
|
-
addonId:
|
|
7921
|
-
disabled:
|
|
7922
|
-
online:
|
|
7141
|
+
import { z as z27 } from "zod";
|
|
7142
|
+
var DeviceSchema2 = z27.looseObject({
|
|
7143
|
+
id: z27.number(),
|
|
7144
|
+
name: z27.string(),
|
|
7145
|
+
addonId: z27.string().optional(),
|
|
7146
|
+
disabled: z27.boolean().optional(),
|
|
7147
|
+
online: z27.boolean().optional()
|
|
7923
7148
|
}).nullable();
|
|
7924
7149
|
var GENERIC = /onvif|rtsp/i;
|
|
7925
|
-
function words(
|
|
7926
|
-
return
|
|
7150
|
+
function words(text2) {
|
|
7151
|
+
return text2.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length >= 3);
|
|
7927
7152
|
}
|
|
7928
|
-
function isGeneric(
|
|
7929
|
-
return
|
|
7153
|
+
function isGeneric(parts) {
|
|
7154
|
+
return parts.some((p) => GENERIC.test(p));
|
|
7930
7155
|
}
|
|
7931
7156
|
function installedPick(p) {
|
|
7932
7157
|
return {
|
|
@@ -7947,9 +7172,9 @@ function availablePick(p) {
|
|
|
7947
7172
|
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
7173
|
function matchIn(pools, tokens) {
|
|
7949
7174
|
if (tokens.length === 0) return null;
|
|
7950
|
-
const hit = (
|
|
7951
|
-
if (isGeneric(
|
|
7952
|
-
const joined =
|
|
7175
|
+
const hit = (parts) => {
|
|
7176
|
+
if (isGeneric(parts)) return false;
|
|
7177
|
+
const joined = parts.join(" ").toLowerCase();
|
|
7953
7178
|
return tokens.some((t) => joined.includes(t));
|
|
7954
7179
|
};
|
|
7955
7180
|
const installed = pools.installed.find(
|
|
@@ -8085,10 +7310,10 @@ async function supportDiagnoseCamera(ctx, args) {
|
|
|
8085
7310
|
}
|
|
8086
7311
|
|
|
8087
7312
|
// src/mcp/support-deps.ts
|
|
8088
|
-
import { execFile
|
|
7313
|
+
import { execFile } from "child_process";
|
|
8089
7314
|
var RUN_TIMEOUT_MS = 6e4;
|
|
8090
7315
|
var spawnRunner = (command, args) => new Promise((resolve) => {
|
|
8091
|
-
|
|
7316
|
+
execFile(
|
|
8092
7317
|
command,
|
|
8093
7318
|
[...args],
|
|
8094
7319
|
{ timeout: RUN_TIMEOUT_MS, maxBuffer: 1e6 },
|
|
@@ -8120,7 +7345,7 @@ import {
|
|
|
8120
7345
|
digestArgs,
|
|
8121
7346
|
runGated as runGated6
|
|
8122
7347
|
} from "@camstack/develop";
|
|
8123
|
-
import { z as
|
|
7348
|
+
import { z as z28 } from "zod";
|
|
8124
7349
|
|
|
8125
7350
|
// src/mcp/support-scrub.ts
|
|
8126
7351
|
import { redactToolArgs, scrubSecrets } from "@camstack/develop";
|
|
@@ -8129,10 +7354,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
7354
|
var MAC = /\b(?:[0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}\b/g;
|
|
8130
7355
|
var HOME_DIR = /(\/Users|\/home)\/[^/\s'"]+/g;
|
|
8131
7356
|
function octets(ip) {
|
|
8132
|
-
const
|
|
8133
|
-
if (
|
|
8134
|
-
if (
|
|
8135
|
-
return
|
|
7357
|
+
const parts = ip.split(".").map((p) => Number(p));
|
|
7358
|
+
if (parts.length !== 4) return null;
|
|
7359
|
+
if (parts.some((p) => !Number.isInteger(p) || p < 0 || p > 255)) return null;
|
|
7360
|
+
return parts;
|
|
8136
7361
|
}
|
|
8137
7362
|
function maskIpv4(ip) {
|
|
8138
7363
|
const o = octets(ip);
|
|
@@ -8173,16 +7398,16 @@ var INFERENCE_PACKAGE = "@camstack/addon-pipeline";
|
|
|
8173
7398
|
var TITLE_MAX = 140;
|
|
8174
7399
|
var DIAGNOSIS_MAX = 4e3;
|
|
8175
7400
|
var DRAFT_KINDS = ["accelerator", "camera", "benchmark", "other"];
|
|
8176
|
-
var DraftKindSchema =
|
|
8177
|
-
var BenchmarkCellsSchema =
|
|
8178
|
-
|
|
8179
|
-
|
|
7401
|
+
var DraftKindSchema = z28.enum(DRAFT_KINDS);
|
|
7402
|
+
var BenchmarkCellsSchema = z28.record(
|
|
7403
|
+
z28.string().regex(/^[a-z0-9][a-z0-9._-]{1,63}$/),
|
|
7404
|
+
z28.object({ p50Ms: z28.number().positive(), fps: z28.number().positive() })
|
|
8180
7405
|
);
|
|
8181
7406
|
var PackageNameRe = /^(@[a-z0-9._-]+\/)?[a-z0-9._-]+$/;
|
|
8182
7407
|
var GithubRepoRe = /github\.com[:/]([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?(?:[/#?].*)?$/;
|
|
8183
7408
|
var ShorthandRe = /^(?:github:)?([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/;
|
|
8184
|
-
var RegistryDocSchema =
|
|
8185
|
-
repository:
|
|
7409
|
+
var RegistryDocSchema = z28.looseObject({
|
|
7410
|
+
repository: z28.union([z28.string(), z28.looseObject({ url: z28.string() })]).optional()
|
|
8186
7411
|
});
|
|
8187
7412
|
function githubRepoOf(value) {
|
|
8188
7413
|
const url = GithubRepoRe.exec(value);
|
|
@@ -8224,7 +7449,7 @@ async function resolveRepo(ctx, packageName) {
|
|
|
8224
7449
|
reason: null
|
|
8225
7450
|
};
|
|
8226
7451
|
}
|
|
8227
|
-
var ServerStatusSchema2 =
|
|
7452
|
+
var ServerStatusSchema2 = z28.looseObject({ runningVersion: z28.string() });
|
|
8228
7453
|
async function serverVersion(client, nodeId) {
|
|
8229
7454
|
const status = await readQuery(
|
|
8230
7455
|
client,
|
|
@@ -8339,11 +7564,11 @@ async function buildDraft(ctx, args, heading) {
|
|
|
8339
7564
|
].filter((s) => s !== "");
|
|
8340
7565
|
return { target, title: cleanTitle(args.title), body: sections.join("\n\n") };
|
|
8341
7566
|
}
|
|
8342
|
-
function
|
|
7567
|
+
function shellQuote(value) {
|
|
8343
7568
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
8344
7569
|
}
|
|
8345
7570
|
function issueCommand(draft, bodyFile) {
|
|
8346
|
-
return `gh issue create --repo ${draft.target.repo} --title ${
|
|
7571
|
+
return `gh issue create --repo ${draft.target.repo} --title ${shellQuote(draft.title)} --body-file ${bodyFile}`;
|
|
8347
7572
|
}
|
|
8348
7573
|
var ISSUE_BODY_FILE = "./camstack-issue.md";
|
|
8349
7574
|
async function issueDraft(ctx, args) {
|
|
@@ -8426,8 +7651,8 @@ var BENCHMARK_FILES = [
|
|
|
8426
7651
|
"docs/benchmarks/detector-latency.json",
|
|
8427
7652
|
"packages/types/src/pipeline/auto-tune/detector-performance.ts"
|
|
8428
7653
|
];
|
|
8429
|
-
function slug(
|
|
8430
|
-
const s =
|
|
7654
|
+
function slug(text2) {
|
|
7655
|
+
const s = text2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
8431
7656
|
return s === "" ? "change" : s;
|
|
8432
7657
|
}
|
|
8433
7658
|
function prProblem(args) {
|
|
@@ -8450,7 +7675,7 @@ function planFor(args, draft) {
|
|
|
8450
7675
|
`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
7676
|
"npx vitest run packages/types/src/pipeline/auto-tune/__tests__/detector-performance.spec.ts (it fails until the two files agree)."
|
|
8452
7677
|
] : ["Make the change with a spec that fails first, then passes."],
|
|
8453
|
-
`git commit -am ${
|
|
7678
|
+
`git commit -am ${shellQuote(benchmark ? `docs: measured detector latency for ${args.column ?? ""}` : `fix: ${draft.title}`)}`,
|
|
8454
7679
|
`git push -u origin ${branch}`,
|
|
8455
7680
|
`Save the body below as ${PR_BODY_FILE}, then run the command.`
|
|
8456
7681
|
];
|
|
@@ -8463,7 +7688,7 @@ function planFor(args, draft) {
|
|
|
8463
7688
|
branch,
|
|
8464
7689
|
files,
|
|
8465
7690
|
steps,
|
|
8466
|
-
command: `gh pr create --repo ${draft.target.repo} --head <your-github-user>:${branch} --title ${
|
|
7691
|
+
command: `gh pr create --repo ${draft.target.repo} --head <your-github-user>:${branch} --title ${shellQuote(draft.title)} --body-file ${PR_BODY_FILE}`,
|
|
8467
7692
|
note: "This tool pushes no code and calls no GitHub API; you run these steps with your own git and gh."
|
|
8468
7693
|
};
|
|
8469
7694
|
}
|
|
@@ -8490,20 +7715,20 @@ async function supportDraftPr(ctx, args, confirmToken) {
|
|
|
8490
7715
|
}
|
|
8491
7716
|
|
|
8492
7717
|
// src/mcp/support-tools.ts
|
|
8493
|
-
var ModelId =
|
|
7718
|
+
var ModelId = z29.string().regex(/^[a-z0-9][a-z0-9._-]{1,63}$/);
|
|
8494
7719
|
var DraftShape = {
|
|
8495
7720
|
kind: DraftKindSchema,
|
|
8496
|
-
title:
|
|
8497
|
-
summary:
|
|
8498
|
-
nodeId:
|
|
7721
|
+
title: z29.string().min(3).max(300),
|
|
7722
|
+
summary: z29.string().min(3).max(4e3),
|
|
7723
|
+
nodeId: z29.string().max(100).optional(),
|
|
8499
7724
|
expected: ExpectedAcceleratorSchema.optional(),
|
|
8500
|
-
brand:
|
|
8501
|
-
model:
|
|
8502
|
-
deviceId:
|
|
8503
|
-
packageName:
|
|
8504
|
-
column:
|
|
7725
|
+
brand: z29.string().max(100).optional(),
|
|
7726
|
+
model: z29.string().max(100).optional(),
|
|
7727
|
+
deviceId: z29.number().int().optional(),
|
|
7728
|
+
packageName: z29.string().max(214).optional(),
|
|
7729
|
+
column: z29.string().max(100).optional(),
|
|
8505
7730
|
cells: BenchmarkCellsSchema.optional(),
|
|
8506
|
-
confirm_token:
|
|
7731
|
+
confirm_token: z29.string().optional()
|
|
8507
7732
|
};
|
|
8508
7733
|
function draftArgs(input) {
|
|
8509
7734
|
return {
|
|
@@ -8524,20 +7749,20 @@ function registerSupportTools(server, ctx, deps = defaultSupportDeps) {
|
|
|
8524
7749
|
server.tool(
|
|
8525
7750
|
"support_benchmark_status",
|
|
8526
7751
|
"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 }) =>
|
|
7752
|
+
{ nodeId: z29.string().max(100).optional() },
|
|
7753
|
+
async ({ nodeId }) => text(await supportBenchmarkStatus(ctx, nodeId !== void 0 ? { nodeId } : {}))
|
|
8529
7754
|
);
|
|
8530
7755
|
server.tool(
|
|
8531
7756
|
BENCHMARK_RUN_TOOL,
|
|
8532
7757
|
"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
7758
|
{
|
|
8534
|
-
nodeId:
|
|
8535
|
-
deviceKey:
|
|
8536
|
-
models:
|
|
8537
|
-
warmRuns:
|
|
8538
|
-
confirm_token:
|
|
7759
|
+
nodeId: z29.string().min(1).max(100),
|
|
7760
|
+
deviceKey: z29.string().min(1).max(60),
|
|
7761
|
+
models: z29.array(ModelId).min(1).max(MAX_MODELS).optional(),
|
|
7762
|
+
warmRuns: z29.number().int().min(1).max(MAX_WARM_RUNS).optional(),
|
|
7763
|
+
confirm_token: z29.string().optional()
|
|
8539
7764
|
},
|
|
8540
|
-
async ({ nodeId, deviceKey, models, warmRuns, confirm_token }) =>
|
|
7765
|
+
async ({ nodeId, deviceKey, models, warmRuns, confirm_token }) => text(
|
|
8541
7766
|
await supportBenchmarkRun(
|
|
8542
7767
|
ctx,
|
|
8543
7768
|
deps,
|
|
@@ -8554,18 +7779,18 @@ function registerSupportTools(server, ctx, deps = defaultSupportDeps) {
|
|
|
8554
7779
|
server.tool(
|
|
8555
7780
|
"support_diagnose_accelerator",
|
|
8556
7781
|
"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 }) =>
|
|
7782
|
+
{ nodeId: z29.string().min(1).max(100), expected: ExpectedAcceleratorSchema },
|
|
7783
|
+
async ({ nodeId, expected }) => text(await supportDiagnoseAccelerator(ctx, { nodeId, expected }))
|
|
8559
7784
|
);
|
|
8560
7785
|
server.tool(
|
|
8561
7786
|
"support_diagnose_camera",
|
|
8562
7787
|
"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
7788
|
{
|
|
8564
|
-
brand:
|
|
8565
|
-
model:
|
|
8566
|
-
deviceId:
|
|
7789
|
+
brand: z29.string().max(100).optional(),
|
|
7790
|
+
model: z29.string().max(100).optional(),
|
|
7791
|
+
deviceId: z29.number().int().optional()
|
|
8567
7792
|
},
|
|
8568
|
-
async ({ brand, model, deviceId }) =>
|
|
7793
|
+
async ({ brand, model, deviceId }) => text(
|
|
8569
7794
|
await supportDiagnoseCamera(ctx, {
|
|
8570
7795
|
...brand !== void 0 ? { brand } : {},
|
|
8571
7796
|
...model !== void 0 ? { model } : {},
|
|
@@ -8577,13 +7802,13 @@ function registerSupportTools(server, ctx, deps = defaultSupportDeps) {
|
|
|
8577
7802
|
DRAFT_ISSUE_TOOL,
|
|
8578
7803
|
"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
7804
|
DraftShape,
|
|
8580
|
-
async (input) =>
|
|
7805
|
+
async (input) => text(await supportDraftIssue(ctx, deps, draftArgs(input), input.confirm_token))
|
|
8581
7806
|
);
|
|
8582
7807
|
server.tool(
|
|
8583
7808
|
DRAFT_PR_TOOL,
|
|
8584
7809
|
'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
7810
|
DraftShape,
|
|
8586
|
-
async (input) =>
|
|
7811
|
+
async (input) => text(await supportDraftPr(ctx, draftArgs(input), input.confirm_token))
|
|
8587
7812
|
);
|
|
8588
7813
|
}
|
|
8589
7814
|
|