@camstack/addon-tailscale 1.0.7 → 1.1.1

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.
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  enumerable: true
25
25
  }) : target, mod));
26
26
  //#endregion
27
- const require_dist = require("../dist-_yzt354G.js");
27
+ const require_dist = require("../dist-DxOA3Ddo.js");
28
28
  let node_child_process = require("node:child_process");
29
29
  let node_util = require("node:util");
30
30
  let node_fs = require("node:fs");
@@ -1,4 +1,4 @@
1
- import { i as EventCategory, n as networkAccessCapability, r as BaseAddon, t as meshNetworkCapability } from "../dist-C3gBIyb4.mjs";
1
+ import { i as EventCategory, n as networkAccessCapability, r as BaseAddon, t as meshNetworkCapability } from "../dist-BTq7tvIJ.mjs";
2
2
  import { execFile, spawn } from "node:child_process";
3
3
  import { promisify } from "node:util";
4
4
  import * as fs from "node:fs";
@@ -4627,7 +4627,7 @@ function _instanceof(cls, params = {}) {
4627
4627
  return inst;
4628
4628
  }
4629
4629
  //#endregion
4630
- //#region ../types/dist/sleep-B1dKJAMJ.mjs
4630
+ //#region ../types/dist/sleep-BV7rLc6Y.mjs
4631
4631
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4632
4632
  EventCategory["SystemBoot"] = "system.boot";
4633
4633
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -5106,6 +5106,12 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
5106
5106
  * Payload: `{ deviceId, childDeviceIds, hiddenChildIds }`.
5107
5107
  */
5108
5108
  EventCategory["AccessoriesChanged"] = "accessories.onAccessoriesChanged";
5109
+ /**
5110
+ * Progress update from a running model conversion job.
5111
+ * Payload: `{ kind: 'model-convert', phase, sessionId?, pct?, detail? }`.
5112
+ * Emitted by `addon-model-studio` on the converting node.
5113
+ */
5114
+ EventCategory["ModelConvertProgress"] = "model-convert.progress";
5109
5115
  return EventCategory;
5110
5116
  }({});
5111
5117
  Object.fromEntries([
@@ -6627,6 +6633,13 @@ object({
6627
6633
  unreachable: number()
6628
6634
  })
6629
6635
  });
6636
+ var LabelDefinitionSchema = object({
6637
+ id: string(),
6638
+ name: string(),
6639
+ category: string().optional(),
6640
+ description: string().optional(),
6641
+ icon: string().optional()
6642
+ });
6630
6643
  var MODEL_FORMATS = [
6631
6644
  "onnx",
6632
6645
  "coreml",
@@ -6635,6 +6648,120 @@ var MODEL_FORMATS = [
6635
6648
  "pt"
6636
6649
  ];
6637
6650
  /**
6651
+ * Multi-file format payload.
6652
+ *
6653
+ * - Directory formats (`isDirectory: true`, e.g. `.mlpackage`): files
6654
+ * relative to the directory root — the downloader fetches each from
6655
+ * `{url}/{file}` into `{modelDir}/{file}`. If omitted, it probes the
6656
+ * HuggingFace API (slower).
6657
+ * - Single-file formats (no `isDirectory`, e.g. OpenVINO IR): sibling
6658
+ * files fetched from the SAME remote directory as `url` and stored flat
6659
+ * alongside the main file — e.g. `['camstack-yolov9t.bin']` for the IR
6660
+ * weights next to `camstack-yolov9t.xml`.
6661
+ */
6662
+ var ModelFormatEntrySchema = object({
6663
+ url: string(),
6664
+ sizeMB: number(),
6665
+ /** Whether this format is a directory bundle (e.g., .mlpackage) rather than a single file */
6666
+ isDirectory: boolean().optional(),
6667
+ /** Multi-file payload (directory members or sibling files). */
6668
+ files: array(string()).readonly().optional(),
6669
+ /** Runtime(s) that can use this format. If omitted, inferred from ModelFormat key */
6670
+ runtimes: array(_enum(["node", "python"])).readonly().optional()
6671
+ });
6672
+ /**
6673
+ * Extra file that must be downloaded alongside the model (e.g., labels JSON, dict.txt).
6674
+ * The downloader fetches from `url` and saves to `{modelsDir}/{filename}`.
6675
+ */
6676
+ var ModelExtraFileSchema = object({
6677
+ url: string(),
6678
+ filename: string(),
6679
+ sizeMB: number()
6680
+ });
6681
+ /**
6682
+ * Per-format payload map. Modelled as an explicit object (one optional key
6683
+ * per `ModelFormat`) rather than `z.record(enum, …)` — zod v4's enum-keyed
6684
+ * record requires every key, but a catalog entry only ships a subset of
6685
+ * formats.
6686
+ */
6687
+ var ModelFormatsSchema = object({
6688
+ onnx: ModelFormatEntrySchema.optional(),
6689
+ coreml: ModelFormatEntrySchema.optional(),
6690
+ openvino: ModelFormatEntrySchema.optional(),
6691
+ tflite: ModelFormatEntrySchema.optional(),
6692
+ pt: ModelFormatEntrySchema.optional()
6693
+ });
6694
+ var ModelCatalogEntrySchema = object({
6695
+ id: string(),
6696
+ name: string(),
6697
+ description: string(),
6698
+ formats: ModelFormatsSchema,
6699
+ inputSize: object({
6700
+ width: number(),
6701
+ height: number()
6702
+ }),
6703
+ labels: array(LabelDefinitionSchema).readonly(),
6704
+ inputLayout: _enum(["nchw", "nhwc"]).optional(),
6705
+ inputNormalization: _enum([
6706
+ "zero-one",
6707
+ "imagenet",
6708
+ "none"
6709
+ ]).optional(),
6710
+ preprocessMode: _enum(["letterbox", "resize"]).optional(),
6711
+ /**
6712
+ * When true, the executor produces a landmark-aligned crop (similarity warp
6713
+ * onto the canonical template) before this step runs, instead of a plain
6714
+ * axis-aligned bbox crop. Required for face-recognition embedders (ArcFace):
6715
+ * their embeddings are only discriminative on an aligned input. The face
6716
+ * detector that produced the parent detail must emit 5 landmarks.
6717
+ */
6718
+ faceAlignment: boolean().optional(),
6719
+ /**
6720
+ * Auxiliary files required at runtime (labels JSON, charset dict, etc.).
6721
+ * Downloaded into the same modelsDir alongside the model file.
6722
+ */
6723
+ extraFiles: array(ModelExtraFileSchema).readonly().optional()
6724
+ });
6725
+ var ConvertTargetSchema = discriminatedUnion("format", [object({
6726
+ format: literal("openvino"),
6727
+ precisions: array(_enum(["fp16", "int8"])).min(1).readonly()
6728
+ }), object({ format: literal("coreml") })]);
6729
+ var ModelConvertMetadataSchema = object({
6730
+ id: string().regex(/^[a-zA-Z0-9._-]+$/),
6731
+ name: string(),
6732
+ labels: array(LabelDefinitionSchema).readonly(),
6733
+ inputSize: object({
6734
+ width: number(),
6735
+ height: number()
6736
+ }),
6737
+ inputLayout: _enum(["nchw", "nhwc"]).optional(),
6738
+ inputNormalization: _enum([
6739
+ "zero-one",
6740
+ "imagenet",
6741
+ "none"
6742
+ ]).optional(),
6743
+ preprocessMode: _enum(["letterbox", "resize"]).optional(),
6744
+ outputFormat: _enum([
6745
+ "yolo",
6746
+ "ssd",
6747
+ "embedding",
6748
+ "classification",
6749
+ "ocr",
6750
+ "segmentation"
6751
+ ]),
6752
+ faceAlignment: boolean().optional()
6753
+ });
6754
+ var ConvertResultSchema = object({
6755
+ entry: ModelCatalogEntrySchema,
6756
+ artifacts: array(object({
6757
+ format: _enum(MODEL_FORMATS),
6758
+ precision: _enum(["fp16", "int8"]).optional(),
6759
+ sizeMB: number(),
6760
+ validated: boolean(),
6761
+ files: array(string()).readonly()
6762
+ })).readonly()
6763
+ });
6764
+ /**
6638
6765
  * Numeric day-of-week: 0 = Sunday … 6 = Saturday (matches `Date.getDay`).
6639
6766
  * Named `RecordingWeekday` to avoid collision with the string-union
6640
6767
  * `Weekday` exported from `interfaces/timezones.ts`.
@@ -12473,6 +12600,54 @@ var EnrichedWidgetMetadataSchema = WidgetMetadataSchema.extend({
12473
12600
  bundleUrl: string()
12474
12601
  });
12475
12602
  method(_void(), array(EnrichedWidgetMetadataSchema).readonly());
12603
+ /**
12604
+ * `custom-model-registry` — collection cap exposing operator-registered
12605
+ * custom detection models. Each provider (today: `addon-model-studio`)
12606
+ * contributes a list of `CustomModelDescriptor`s; the hub auto-concatenates
12607
+ * them across providers (`concatCollection`).
12608
+ *
12609
+ * The detection-pipeline is *aware* of this cap: when at least one provider
12610
+ * exists it unions these descriptors into the per-step model picker and the
12611
+ * runtime model-resolution path, alongside the static catalog. When no
12612
+ * provider exists the consumer no-ops entirely (identical to the catalog-only
12613
+ * behaviour).
12614
+ *
12615
+ * A descriptor carries a full `ModelCatalogEntry` directly — the same shape
12616
+ * the static catalog uses — so the existing download/resolution code consumes
12617
+ * it unchanged. `stepId` is the detection step the model targets
12618
+ * (e.g. `'object-detection'`).
12619
+ */
12620
+ var CustomModelDescriptorSchema = object({
12621
+ stepId: string(),
12622
+ entry: ModelCatalogEntrySchema
12623
+ });
12624
+ method(_void(), array(CustomModelDescriptorSchema).readonly());
12625
+ method(object({
12626
+ nodeId: string(),
12627
+ modelId: string(),
12628
+ format: _enum(MODEL_FORMATS),
12629
+ entry: ModelCatalogEntrySchema
12630
+ }), object({
12631
+ ok: boolean(),
12632
+ /** sha256 of the staged tarball (empty for a hub-local no-op). */
12633
+ sha256: string(),
12634
+ bytes: number(),
12635
+ /** The target node's modelsDir the artifact landed in. */
12636
+ path: string()
12637
+ }), {
12638
+ kind: "mutation",
12639
+ auth: "admin"
12640
+ });
12641
+ method(object({
12642
+ sourceUrl: string(),
12643
+ metadata: ModelConvertMetadataSchema,
12644
+ targets: array(ConvertTargetSchema).min(1).readonly(),
12645
+ calibrationRef: string().optional(),
12646
+ sessionId: string().optional()
12647
+ }), ConvertResultSchema, {
12648
+ kind: "mutation",
12649
+ auth: "admin"
12650
+ });
12476
12651
  var AddonHttpRouteSchema = object({
12477
12652
  method: _enum([
12478
12653
  "GET",
@@ -17445,6 +17620,12 @@ Object.freeze({
17445
17620
  addonId: null,
17446
17621
  access: "create"
17447
17622
  },
17623
+ "customModelRegistry.listModels": {
17624
+ capName: "custom-model-registry",
17625
+ capScope: "system",
17626
+ addonId: null,
17627
+ access: "view"
17628
+ },
17448
17629
  "decoder.createSession": {
17449
17630
  capName: "decoder",
17450
17631
  capScope: "system",
@@ -18669,6 +18850,18 @@ Object.freeze({
18669
18850
  addonId: null,
18670
18851
  access: "view"
18671
18852
  },
18853
+ "modelConvert.convert": {
18854
+ capName: "model-convert",
18855
+ capScope: "system",
18856
+ addonId: null,
18857
+ access: "create"
18858
+ },
18859
+ "modelDistributor.distributeModel": {
18860
+ capName: "model-distributor",
18861
+ capScope: "system",
18862
+ addonId: null,
18863
+ access: "create"
18864
+ },
18672
18865
  "motion.isDetected": {
18673
18866
  capName: "motion",
18674
18867
  capScope: "device",
@@ -4627,7 +4627,7 @@ function _instanceof(cls, params = {}) {
4627
4627
  return inst;
4628
4628
  }
4629
4629
  //#endregion
4630
- //#region ../types/dist/sleep-B1dKJAMJ.mjs
4630
+ //#region ../types/dist/sleep-BV7rLc6Y.mjs
4631
4631
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4632
4632
  EventCategory["SystemBoot"] = "system.boot";
4633
4633
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -5106,6 +5106,12 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
5106
5106
  * Payload: `{ deviceId, childDeviceIds, hiddenChildIds }`.
5107
5107
  */
5108
5108
  EventCategory["AccessoriesChanged"] = "accessories.onAccessoriesChanged";
5109
+ /**
5110
+ * Progress update from a running model conversion job.
5111
+ * Payload: `{ kind: 'model-convert', phase, sessionId?, pct?, detail? }`.
5112
+ * Emitted by `addon-model-studio` on the converting node.
5113
+ */
5114
+ EventCategory["ModelConvertProgress"] = "model-convert.progress";
5109
5115
  return EventCategory;
5110
5116
  }({});
5111
5117
  Object.fromEntries([
@@ -6627,6 +6633,13 @@ object({
6627
6633
  unreachable: number()
6628
6634
  })
6629
6635
  });
6636
+ var LabelDefinitionSchema = object({
6637
+ id: string(),
6638
+ name: string(),
6639
+ category: string().optional(),
6640
+ description: string().optional(),
6641
+ icon: string().optional()
6642
+ });
6630
6643
  var MODEL_FORMATS = [
6631
6644
  "onnx",
6632
6645
  "coreml",
@@ -6635,6 +6648,120 @@ var MODEL_FORMATS = [
6635
6648
  "pt"
6636
6649
  ];
6637
6650
  /**
6651
+ * Multi-file format payload.
6652
+ *
6653
+ * - Directory formats (`isDirectory: true`, e.g. `.mlpackage`): files
6654
+ * relative to the directory root — the downloader fetches each from
6655
+ * `{url}/{file}` into `{modelDir}/{file}`. If omitted, it probes the
6656
+ * HuggingFace API (slower).
6657
+ * - Single-file formats (no `isDirectory`, e.g. OpenVINO IR): sibling
6658
+ * files fetched from the SAME remote directory as `url` and stored flat
6659
+ * alongside the main file — e.g. `['camstack-yolov9t.bin']` for the IR
6660
+ * weights next to `camstack-yolov9t.xml`.
6661
+ */
6662
+ var ModelFormatEntrySchema = object({
6663
+ url: string(),
6664
+ sizeMB: number(),
6665
+ /** Whether this format is a directory bundle (e.g., .mlpackage) rather than a single file */
6666
+ isDirectory: boolean().optional(),
6667
+ /** Multi-file payload (directory members or sibling files). */
6668
+ files: array(string()).readonly().optional(),
6669
+ /** Runtime(s) that can use this format. If omitted, inferred from ModelFormat key */
6670
+ runtimes: array(_enum(["node", "python"])).readonly().optional()
6671
+ });
6672
+ /**
6673
+ * Extra file that must be downloaded alongside the model (e.g., labels JSON, dict.txt).
6674
+ * The downloader fetches from `url` and saves to `{modelsDir}/{filename}`.
6675
+ */
6676
+ var ModelExtraFileSchema = object({
6677
+ url: string(),
6678
+ filename: string(),
6679
+ sizeMB: number()
6680
+ });
6681
+ /**
6682
+ * Per-format payload map. Modelled as an explicit object (one optional key
6683
+ * per `ModelFormat`) rather than `z.record(enum, …)` — zod v4's enum-keyed
6684
+ * record requires every key, but a catalog entry only ships a subset of
6685
+ * formats.
6686
+ */
6687
+ var ModelFormatsSchema = object({
6688
+ onnx: ModelFormatEntrySchema.optional(),
6689
+ coreml: ModelFormatEntrySchema.optional(),
6690
+ openvino: ModelFormatEntrySchema.optional(),
6691
+ tflite: ModelFormatEntrySchema.optional(),
6692
+ pt: ModelFormatEntrySchema.optional()
6693
+ });
6694
+ var ModelCatalogEntrySchema = object({
6695
+ id: string(),
6696
+ name: string(),
6697
+ description: string(),
6698
+ formats: ModelFormatsSchema,
6699
+ inputSize: object({
6700
+ width: number(),
6701
+ height: number()
6702
+ }),
6703
+ labels: array(LabelDefinitionSchema).readonly(),
6704
+ inputLayout: _enum(["nchw", "nhwc"]).optional(),
6705
+ inputNormalization: _enum([
6706
+ "zero-one",
6707
+ "imagenet",
6708
+ "none"
6709
+ ]).optional(),
6710
+ preprocessMode: _enum(["letterbox", "resize"]).optional(),
6711
+ /**
6712
+ * When true, the executor produces a landmark-aligned crop (similarity warp
6713
+ * onto the canonical template) before this step runs, instead of a plain
6714
+ * axis-aligned bbox crop. Required for face-recognition embedders (ArcFace):
6715
+ * their embeddings are only discriminative on an aligned input. The face
6716
+ * detector that produced the parent detail must emit 5 landmarks.
6717
+ */
6718
+ faceAlignment: boolean().optional(),
6719
+ /**
6720
+ * Auxiliary files required at runtime (labels JSON, charset dict, etc.).
6721
+ * Downloaded into the same modelsDir alongside the model file.
6722
+ */
6723
+ extraFiles: array(ModelExtraFileSchema).readonly().optional()
6724
+ });
6725
+ var ConvertTargetSchema = discriminatedUnion("format", [object({
6726
+ format: literal("openvino"),
6727
+ precisions: array(_enum(["fp16", "int8"])).min(1).readonly()
6728
+ }), object({ format: literal("coreml") })]);
6729
+ var ModelConvertMetadataSchema = object({
6730
+ id: string().regex(/^[a-zA-Z0-9._-]+$/),
6731
+ name: string(),
6732
+ labels: array(LabelDefinitionSchema).readonly(),
6733
+ inputSize: object({
6734
+ width: number(),
6735
+ height: number()
6736
+ }),
6737
+ inputLayout: _enum(["nchw", "nhwc"]).optional(),
6738
+ inputNormalization: _enum([
6739
+ "zero-one",
6740
+ "imagenet",
6741
+ "none"
6742
+ ]).optional(),
6743
+ preprocessMode: _enum(["letterbox", "resize"]).optional(),
6744
+ outputFormat: _enum([
6745
+ "yolo",
6746
+ "ssd",
6747
+ "embedding",
6748
+ "classification",
6749
+ "ocr",
6750
+ "segmentation"
6751
+ ]),
6752
+ faceAlignment: boolean().optional()
6753
+ });
6754
+ var ConvertResultSchema = object({
6755
+ entry: ModelCatalogEntrySchema,
6756
+ artifacts: array(object({
6757
+ format: _enum(MODEL_FORMATS),
6758
+ precision: _enum(["fp16", "int8"]).optional(),
6759
+ sizeMB: number(),
6760
+ validated: boolean(),
6761
+ files: array(string()).readonly()
6762
+ })).readonly()
6763
+ });
6764
+ /**
6638
6765
  * Numeric day-of-week: 0 = Sunday … 6 = Saturday (matches `Date.getDay`).
6639
6766
  * Named `RecordingWeekday` to avoid collision with the string-union
6640
6767
  * `Weekday` exported from `interfaces/timezones.ts`.
@@ -12473,6 +12600,54 @@ var EnrichedWidgetMetadataSchema = WidgetMetadataSchema.extend({
12473
12600
  bundleUrl: string()
12474
12601
  });
12475
12602
  method(_void(), array(EnrichedWidgetMetadataSchema).readonly());
12603
+ /**
12604
+ * `custom-model-registry` — collection cap exposing operator-registered
12605
+ * custom detection models. Each provider (today: `addon-model-studio`)
12606
+ * contributes a list of `CustomModelDescriptor`s; the hub auto-concatenates
12607
+ * them across providers (`concatCollection`).
12608
+ *
12609
+ * The detection-pipeline is *aware* of this cap: when at least one provider
12610
+ * exists it unions these descriptors into the per-step model picker and the
12611
+ * runtime model-resolution path, alongside the static catalog. When no
12612
+ * provider exists the consumer no-ops entirely (identical to the catalog-only
12613
+ * behaviour).
12614
+ *
12615
+ * A descriptor carries a full `ModelCatalogEntry` directly — the same shape
12616
+ * the static catalog uses — so the existing download/resolution code consumes
12617
+ * it unchanged. `stepId` is the detection step the model targets
12618
+ * (e.g. `'object-detection'`).
12619
+ */
12620
+ var CustomModelDescriptorSchema = object({
12621
+ stepId: string(),
12622
+ entry: ModelCatalogEntrySchema
12623
+ });
12624
+ method(_void(), array(CustomModelDescriptorSchema).readonly());
12625
+ method(object({
12626
+ nodeId: string(),
12627
+ modelId: string(),
12628
+ format: _enum(MODEL_FORMATS),
12629
+ entry: ModelCatalogEntrySchema
12630
+ }), object({
12631
+ ok: boolean(),
12632
+ /** sha256 of the staged tarball (empty for a hub-local no-op). */
12633
+ sha256: string(),
12634
+ bytes: number(),
12635
+ /** The target node's modelsDir the artifact landed in. */
12636
+ path: string()
12637
+ }), {
12638
+ kind: "mutation",
12639
+ auth: "admin"
12640
+ });
12641
+ method(object({
12642
+ sourceUrl: string(),
12643
+ metadata: ModelConvertMetadataSchema,
12644
+ targets: array(ConvertTargetSchema).min(1).readonly(),
12645
+ calibrationRef: string().optional(),
12646
+ sessionId: string().optional()
12647
+ }), ConvertResultSchema, {
12648
+ kind: "mutation",
12649
+ auth: "admin"
12650
+ });
12476
12651
  var AddonHttpRouteSchema = object({
12477
12652
  method: _enum([
12478
12653
  "GET",
@@ -17445,6 +17620,12 @@ Object.freeze({
17445
17620
  addonId: null,
17446
17621
  access: "create"
17447
17622
  },
17623
+ "customModelRegistry.listModels": {
17624
+ capName: "custom-model-registry",
17625
+ capScope: "system",
17626
+ addonId: null,
17627
+ access: "view"
17628
+ },
17448
17629
  "decoder.createSession": {
17449
17630
  capName: "decoder",
17450
17631
  capScope: "system",
@@ -18669,6 +18850,18 @@ Object.freeze({
18669
18850
  addonId: null,
18670
18851
  access: "view"
18671
18852
  },
18853
+ "modelConvert.convert": {
18854
+ capName: "model-convert",
18855
+ capScope: "system",
18856
+ addonId: null,
18857
+ access: "create"
18858
+ },
18859
+ "modelDistributor.distributeModel": {
18860
+ capName: "model-distributor",
18861
+ capScope: "system",
18862
+ addonId: null,
18863
+ access: "create"
18864
+ },
18672
18865
  "motion.isDetected": {
18673
18866
  capName: "motion",
18674
18867
  capScope: "device",
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-_yzt354G.js");
5
+ const require_dist = require("../dist-DxOA3Ddo.js");
6
6
  let node_child_process = require("node:child_process");
7
7
  let node_util = require("node:util");
8
8
  let node_crypto = require("node:crypto");
@@ -1,4 +1,4 @@
1
- import { i as EventCategory, n as networkAccessCapability, r as BaseAddon } from "../dist-C3gBIyb4.mjs";
1
+ import { i as EventCategory, n as networkAccessCapability, r as BaseAddon } from "../dist-BTq7tvIJ.mjs";
2
2
  import { execFile } from "node:child_process";
3
3
  import { promisify } from "node:util";
4
4
  import { randomUUID } from "node:crypto";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-tailscale",
3
- "version": "1.0.7",
3
+ "version": "1.1.1",
4
4
  "description": "Tailscale bundle for CamStack — joins the host to a tailnet (client) and exposes the hub via Serve/Funnel (ingress). Multi-entry npm package shipping 2 addons under a single bundle.",
5
5
  "keywords": [
6
6
  "camstack",