@bike4mind/cli 0.15.3 → 0.17.0

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.
@@ -291,9 +291,6 @@ let ImageModels = /* @__PURE__ */ function(ImageModels) {
291
291
  ImageModels["FLUX_PRO_1_1"] = "flux-pro-1.1";
292
292
  ImageModels["FLUX_PRO_ULTRA"] = "flux-pro-1.1-ultra";
293
293
  ImageModels["FLUX_PRO_FILL"] = "flux-pro-1.0-fill";
294
- ImageModels["FLUX_PRO_CANNY"] = "flux-pro-1.0-canny";
295
- ImageModels["FLUX_PRO_DEPTH"] = "flux-pro-1.0-depth";
296
- ImageModels["FLUX_DEV"] = "flux-dev";
297
294
  ImageModels["FLUX_KONTEXT_PRO"] = "flux-kontext-pro";
298
295
  ImageModels["FLUX_KONTEXT_MAX"] = "flux-kontext-max";
299
296
  ImageModels["GROK_IMAGINE_IMAGE_QUALITY"] = "grok-imagine-image-quality";
@@ -593,7 +590,8 @@ const b4mLLMTools = z.enum([
593
590
  "navigate_view",
594
591
  "generate_jupyter_notebook",
595
592
  "excel_generation",
596
- "fmp_financial_data"
593
+ "fmp_financial_data",
594
+ "skill"
597
595
  ]);
598
596
  b4mLLMTools.options.map((tool) => tool);
599
597
  /**
@@ -905,6 +903,45 @@ const ClaudeArtifactMimeTypes = {
905
903
  PYTHON: "application/vnd.ant.python",
906
904
  BLOG_DRAFT: "application/vnd.b4m.blog-draft"
907
905
  };
906
+ /**
907
+ * Map a MIME type (or AI-provider artifact-type string) to an internal {@link ArtifactType}.
908
+ *
909
+ * Single source of truth — consumed by the artifact parsers (b4m-core/utils + client) and the
910
+ * tool_result dedup in ChatCompletionProcess. Exact blessed-type matches first (case-insensitive,
911
+ * since MIME types are), then language/format inference; returns null if unrecognized.
912
+ *
913
+ * #8907: this previously lived as three hand-maintained copies that drifted — e.g. the lattice
914
+ * tool emits `application/vnd.b4m.lattice` but a copy matched `application/vnd.ant.lattice`,
915
+ * letting lattice tool_result artifacts dodge the dedup set (#8905).
916
+ */
917
+ function mapMimeTypeToArtifactType(mimeType) {
918
+ if (!mimeType) return null;
919
+ const normalized = mimeType.toLowerCase().trim();
920
+ switch (normalized) {
921
+ case ClaudeArtifactMimeTypes.REACT.toLowerCase(): return "react";
922
+ case ClaudeArtifactMimeTypes.HTML.toLowerCase(): return "html";
923
+ case ClaudeArtifactMimeTypes.SVG.toLowerCase(): return "svg";
924
+ case ClaudeArtifactMimeTypes.MERMAID.toLowerCase(): return "mermaid";
925
+ case ClaudeArtifactMimeTypes.RECHARTS.toLowerCase(): return "recharts";
926
+ case ClaudeArtifactMimeTypes.CHESS.toLowerCase(): return "chess";
927
+ case ClaudeArtifactMimeTypes.CODE.toLowerCase(): return "code";
928
+ case ClaudeArtifactMimeTypes.MARKDOWN.toLowerCase(): return "code";
929
+ case ClaudeArtifactMimeTypes.LATTICE.toLowerCase(): return "lattice";
930
+ case ClaudeArtifactMimeTypes.PYTHON.toLowerCase(): return "python";
931
+ case ClaudeArtifactMimeTypes.BLOG_DRAFT.toLowerCase(): return "blog-draft";
932
+ }
933
+ if (normalized.includes("jsx") || normalized.includes("react")) return "react";
934
+ if (normalized.includes("javascript") || normalized.includes("typescript")) return "code";
935
+ if (normalized.includes("python") || normalized === "text/x-python") return "python";
936
+ if (normalized.includes("java") || normalized.includes("c++") || normalized.includes("rust") || normalized.includes("go") || normalized.includes("ruby") || normalized.includes("php") || normalized.includes("swift") || normalized.includes("kotlin") || normalized.includes("csharp") || normalized.includes("c#")) return "code";
937
+ if (normalized.includes("html") || normalized.includes("xhtml")) return "html";
938
+ if (normalized.includes("svg")) return "svg";
939
+ if (normalized.includes("markdown") || normalized.includes("md")) return "code";
940
+ if (normalized.includes("mermaid")) return "mermaid";
941
+ if (normalized.includes("recharts") || normalized.includes("chart")) return "recharts";
942
+ if (normalized.includes("chess")) return "chess";
943
+ return null;
944
+ }
908
945
  let KnowledgeType = /* @__PURE__ */ function(KnowledgeType) {
909
946
  /**
910
947
  * A knowledge that is from a URL.
@@ -1278,7 +1315,7 @@ const SreRepoConfigSchema = z.object({
1278
1315
  /** Whether this repo's SRE pipeline is active */
1279
1316
  enabled: z.boolean().default(false),
1280
1317
  /** LLM model ID for Diagnostician */
1281
- modelId: z.string().default("claude-sonnet-4-20250514"),
1318
+ modelId: z.string().default("claude-sonnet-4-6"),
1282
1319
  /** Max diff lines per fix */
1283
1320
  maxDiffLines: z.number().min(1).default(50),
1284
1321
  /** Max fixes dispatched per day for this repo */
@@ -2663,7 +2700,8 @@ const AgentCompletedAction = z.object({
2663
2700
  executionId: z.string(),
2664
2701
  answer: z.string().optional(),
2665
2702
  totalIterations: z.number(),
2666
- totalCreditsUsed: z.number()
2703
+ totalCreditsUsed: z.number(),
2704
+ mementoIds: z.array(z.string()).optional()
2667
2705
  });
2668
2706
  const AgentFailedAction = z.object({
2669
2707
  action: z.literal("failed"),
@@ -3615,10 +3653,37 @@ const OpenAIImageStyleSchema = z.enum(["vivid", "natural"]);
3615
3653
  /**
3616
3654
  * Maps legacy/removed image model IDs to their current replacements.
3617
3655
  * Prevents Zod validation failures when clients send stale persisted model names.
3656
+ *
3657
+ * Values are constrained to `ALL_IMAGE_MODELS` so a remap target can never point at
3658
+ * a model that the schema would itself reject. If a target model is later retired,
3659
+ * re-point its entry to the current replacement rather than deleting it — clients may
3660
+ * still hold the legacy key in persisted state. When adding an entry, bump the
3661
+ * `llm-settings` persist `version` in LLMContext so existing clients re-run the remap.
3662
+ *
3663
+ * Only `flux-dev` is aliased among the recently removed Flux ids. It was a general
3664
+ * text-to-image model with a confirmed stale client (#8853), so remapping to the
3665
+ * current BFL standard is safe and faithful. `flux-pro-1.0-canny` and
3666
+ * `flux-pro-1.0-depth` are intentionally left as hard validation errors: they were
3667
+ * never UI-selectable (nothing persists them, and no alerts show callers sending them)
3668
+ * and they are structural-control models — silently remapping them to a plain
3669
+ * text-to-image model would drop the control image and return the wrong kind of result,
3670
+ * so a clear "unsupported model" error is the more honest response.
3671
+ *
3672
+ * The three `grok-2-image*` ids are the prior xAI image-model ids, all superseded
3673
+ * in the enum by `grok-imagine-image-quality`. The id went through an un-aliased
3674
+ * rename chain — `grok-2-image-1212` (original) → `grok-2-image` → `grok-2-image-gen`
3675
+ * → `grok-imagine-image-quality` — so any of the three could survive in stale
3676
+ * persisted client state (`grok-2-image-1212` was the one observed in #9211). All are
3677
+ * xAI text-to-image models, so remapping to the current id is a faithful
3678
+ * same-provider, same-modality replacement.
3618
3679
  */
3619
3680
  const LEGACY_IMAGE_MODEL_MAP = {
3620
3681
  "dall-e-3": "gpt-image-1",
3621
- "dall-e-2": "gpt-image-1"
3682
+ "dall-e-2": "gpt-image-1",
3683
+ "flux-dev": "flux-pro-1.1",
3684
+ "grok-2-image-1212": "grok-imagine-image-quality",
3685
+ "grok-2-image": "grok-imagine-image-quality",
3686
+ "grok-2-image-gen": "grok-imagine-image-quality"
3622
3687
  };
3623
3688
  const OpenAIImageGenerationInput = z.object({
3624
3689
  prompt: z.string(),
@@ -3829,6 +3894,8 @@ z.enum([
3829
3894
  "logoSettings",
3830
3895
  "enforceCredits",
3831
3896
  "enableTeamPlan",
3897
+ "allowOpenRegistration",
3898
+ "defaultFreeCredits",
3832
3899
  "enableGoogleCalendar",
3833
3900
  "googleCalendarServiceAccountEmail",
3834
3901
  "googleCalendarServiceAccountSecret",
@@ -3913,8 +3980,36 @@ const OrchestrationDefaultsSchema = z.object({
3913
3980
  "code_execute",
3914
3981
  "coordinate_task"
3915
3982
  ]),
3916
- /** Tool names explicitly forbidden. Empty by default — `allowedTools` is the whitelist. */
3917
- deniedTools: z.array(z.string()).default([]),
3983
+ /**
3984
+ * Tool names explicitly forbidden. Enforced as a final subtraction in
3985
+ * `pickEffectiveEnabledTools` — wins even over payload-pinned tools — so this
3986
+ * is the defense-in-depth backstop for the case where an admin broadens
3987
+ * `allowedTools` without realizing a parallel denylist is also needed.
3988
+ *
3989
+ * Seeded with every tool that mutates user data (#8945, the spec's
3990
+ * "anything tagged `mutates_user_data`"): destructive/overwriting filesystem
3991
+ * writes, arbitrary shell execution, blog authoring/publishing, persisted
3992
+ * lattice-model writes, and storage-backed artifact generation. New mutating
3993
+ * tools should be added here until the tag-derived denylist (Option B, #8923/
3994
+ * #8924) replaces this hand-curated list.
3995
+ */
3996
+ deniedTools: z.array(z.string()).default([
3997
+ "create_file",
3998
+ "edit_file",
3999
+ "edit_local_file",
4000
+ "delete_file",
4001
+ "bash_execute",
4002
+ "blog_draft",
4003
+ "blog_edit",
4004
+ "blog_publish",
4005
+ "lattice_create_model",
4006
+ "lattice_add_entity",
4007
+ "lattice_set_value",
4008
+ "lattice_create_rule",
4009
+ "image_generation",
4010
+ "edit_image",
4011
+ "excel_generation"
4012
+ ]),
3918
4013
  /** Per-thoroughness iteration ceiling. Matches `IAgent.maxIterations`. */
3919
4014
  maxIterations: z.object({
3920
4015
  quick: z.number().int().positive(),
@@ -5765,6 +5860,22 @@ const settingsMap = {
5765
5860
  group: API_SERVICE_GROUPS.CREDITS.id,
5766
5861
  category: "Users"
5767
5862
  }),
5863
+ allowOpenRegistration: makeBooleanSetting({
5864
+ key: "allowOpenRegistration",
5865
+ name: "Allow Open Registration",
5866
+ defaultValue: false,
5867
+ description: "Master switch for self-serve signup. When OFF (default), a valid invite code is required to register. When ON, users may register without an invite code; the Default Free Credits grant is then applied after they verify their email (anti-spam — see Default Free Credits). Safe to enable: the pre-request credit reservation caps every free user at the credits they are granted.",
5868
+ group: API_SERVICE_GROUPS.CREDITS.id,
5869
+ category: "Users"
5870
+ }),
5871
+ defaultFreeCredits: makeNumberSetting({
5872
+ key: "defaultFreeCredits",
5873
+ name: "Default Free Credits",
5874
+ defaultValue: 0,
5875
+ description: "Credits granted to a user who registers WITHOUT an invite code (only applies when Allow Open Registration is ON). Granted after the user verifies their email, NOT at signup — an unverified throwaway account gets 0 credits (anti-spam). A free user can never spend more than this — their hard ceiling of real model cost is roughly credits ÷ 1500 USD.",
5876
+ group: API_SERVICE_GROUPS.CREDITS.id,
5877
+ category: "Users"
5878
+ }),
5768
5879
  FacebookLink: makeStringSetting({
5769
5880
  key: "FacebookLink",
5770
5881
  name: "Facebook Link",
@@ -7229,6 +7340,13 @@ const PromptMetaZodSchema = z.object({
7229
7340
  generatedImageReferences: z.array(z.string()).optional(),
7230
7341
  promptErrors: z.array(z.string()).optional(),
7231
7342
  warnings: z.array(z.string()).optional(),
7343
+ /**
7344
+ * The provider's reason for ending generation (Anthropic vocabulary:
7345
+ * 'end_turn' | 'max_tokens' | 'tool_use' | 'stop_sequence' | 'pause_turn').
7346
+ * 'max_tokens' signals the response was truncated against the output ceiling,
7347
+ * letting the client render a truncated-artifact recovery affordance (#9259).
7348
+ */
7349
+ finishReason: z.string().optional(),
7232
7350
  statusLog: z.array(z.object({
7233
7351
  status: z.string(),
7234
7352
  timestamp: z.date().or(z.string())
@@ -7950,12 +8068,14 @@ z$1.object({
7950
8068
  slug: z$1.string().min(2).max(60).regex(slugRegex, "Slug must be lowercase alphanumeric with hyphens (e.g. \"my-data-lake\")"),
7951
8069
  description: z$1.string().max(2e3).optional(),
7952
8070
  fileTagPrefix: z$1.string().min(2).max(30).refine((s) => s.endsWith(":"), "Tag prefix must end with \":\" (e.g. \"acme:\")"),
7953
- requiredUserTag: z$1.string().min(1).max(100).optional()
8071
+ requiredUserTag: z$1.string().min(1).max(100).optional(),
8072
+ requiredEntitlement: z$1.string().min(3).max(100).refine((s) => s.includes(":") && s.split(":").every((part) => part.length > 0), "Entitlement key must be namespaced with non-empty parts (e.g. \"product:pro\")").optional()
7954
8073
  });
7955
8074
  z$1.object({
7956
8075
  name: z$1.string().min(1).max(200).optional(),
7957
8076
  description: z$1.string().max(2e3).optional(),
7958
- requiredUserTag: z$1.string().min(1).max(100).optional()
8077
+ requiredUserTag: z$1.string().min(1).max(100).optional(),
8078
+ requiredEntitlement: z$1.string().min(3).max(100).refine((s) => s.includes(":") && s.split(":").every((part) => part.length > 0), "Entitlement key must be namespaced with non-empty parts (e.g. \"product:pro\")").optional()
7959
8079
  });
7960
8080
  z$1.object({
7961
8081
  organizationId: z$1.string().optional(),
@@ -8101,37 +8221,68 @@ const DATA_LAKES = [{
8101
8221
  id: "ionq-sales",
8102
8222
  name: "IonQ Sales Intelligence",
8103
8223
  requiredUserTag: "Opti",
8224
+ requiredEntitlement: "optihashi:pro",
8104
8225
  fileTagPrefix: "ionq:",
8105
8226
  datalakeTag: "datalake:ionq-sales"
8106
8227
  }, {
8107
8228
  id: "opti-knowledge",
8108
8229
  name: "Optimization Knowledge Base",
8109
8230
  requiredUserTag: "Opti",
8231
+ requiredEntitlement: "optihashi:pro",
8110
8232
  fileTagPrefix: "opti:",
8111
8233
  datalakeTag: "datalake:opti-knowledge"
8112
8234
  }];
8113
8235
  /**
8114
- * Returns data lakes accessible to a user based on their tags.
8115
- * Data lakes without a requiredUserTag are accessible to all authenticated users.
8116
- * When dynamicDataLakes is provided (fetched from DB), those take precedence
8117
- * over hardcoded DATA_LAKES entries with the same id.
8236
+ * Canonical normalization for entitlement keys + `requiredEntitlement` values the ONE
8237
+ * rule, applied at write time (create/update/stamp) and at match time. Mirrors the
8238
+ * entitlement registry's `normalizeTag` (trim + lowercase) so a value authored in any
8239
+ * casing matches the lowercase keys the resolver produces.
8240
+ */
8241
+ const normalizeEntitlementKey = (key) => key.trim().toLowerCase();
8242
+ /**
8243
+ * Single projection from a persisted lake document to the lightweight DataLakeConfig
8244
+ * the access filters operate on. Centralized so the `requiredEntitlement` field (and any
8245
+ * future field) cannot be silently dropped at one of the many former inline projections.
8246
+ */
8247
+ function toDataLakeConfig(dl) {
8248
+ return {
8249
+ id: dl.id,
8250
+ name: dl.name,
8251
+ requiredUserTag: dl.requiredUserTag,
8252
+ requiredEntitlement: dl.requiredEntitlement,
8253
+ fileTagPrefix: dl.fileTagPrefix,
8254
+ datalakeTag: dl.datalakeTag
8255
+ };
8256
+ }
8257
+ /**
8258
+ * Returns data lakes accessible to a user.
8259
+ *
8260
+ * Access rule (generic, any-of declared requirements): a lake is accessible iff it
8261
+ * declares NO access requirement, OR the user satisfies ANY declared requirement —
8262
+ * either `requiredUserTag` (matched against the user's tags) or `requiredEntitlement`
8263
+ * (matched against the caller's resolved `entitlementKeys`). A lake declaring an
8264
+ * entitlement but no tag is therefore NOT public (it is gated by the key).
8265
+ *
8266
+ * Data lakes without any requirement are accessible to all authenticated users.
8267
+ * When dynamicDataLakes is provided (fetched from DB), those take precedence over
8268
+ * hardcoded DATA_LAKES entries with the same id. `entitlementKeys` is optional — callers
8269
+ * that don't resolve entitlements (tag-only surfaces) omit it and get tag-only matching.
8118
8270
  */
8119
- function getAccessibleDataLakes(userTags, dynamicDataLakes) {
8271
+ function getAccessibleDataLakes(userTags, dynamicDataLakes, entitlementKeys) {
8120
8272
  const normalizedUserTags = userTags.map((tag) => tag.toLowerCase());
8273
+ const normalizedKeys = (entitlementKeys ?? []).map(normalizeEntitlementKey);
8121
8274
  let allLakes;
8122
8275
  if (dynamicDataLakes && dynamicDataLakes.length > 0) {
8123
8276
  const dynamicIds = new Set(dynamicDataLakes.map((d) => d.id));
8124
8277
  const fallbacks = DATA_LAKES.filter((dl) => !dynamicIds.has(dl.id));
8125
8278
  allLakes = [...dynamicDataLakes, ...fallbacks];
8126
8279
  } else allLakes = DATA_LAKES;
8127
- return allLakes.filter((dl) => !dl.requiredUserTag || normalizedUserTags.includes(dl.requiredUserTag.toLowerCase()));
8128
- }
8129
- /**
8130
- * Returns the datalake: meta-tags for all data lakes a user can access.
8131
- * Pass dynamicDataLakes from DB for runtime-registered data lakes.
8132
- */
8133
- function getDataLakeTags(userTags, dynamicDataLakes) {
8134
- return getAccessibleDataLakes(userTags, dynamicDataLakes).map((dl) => dl.datalakeTag);
8280
+ return allLakes.filter((dl) => {
8281
+ if (!(!!dl.requiredUserTag || !!dl.requiredEntitlement)) return true;
8282
+ const tagMatch = !!dl.requiredUserTag && normalizedUserTags.includes(dl.requiredUserTag.toLowerCase());
8283
+ const entMatch = !!dl.requiredEntitlement && normalizedKeys.includes(normalizeEntitlementKey(dl.requiredEntitlement));
8284
+ return tagMatch || entMatch;
8285
+ });
8135
8286
  }
8136
8287
  /**
8137
8288
  * Jupyter Kernel Constants and Validation
@@ -8284,6 +8435,8 @@ z.object({
8284
8435
  /** Notebook session ID */
8285
8436
  sessionId: z.string(),
8286
8437
  historyCount: z.number(),
8438
+ /** Epoch ms when the client submitted the prompt (for the request-lifecycle status log). */
8439
+ clientSubmittedAt: z.number().optional(),
8287
8440
  imageConfig: GenerateImageToolCallSchema.optional(),
8288
8441
  deepResearchConfig: z.object({
8289
8442
  maxDepth: z.number().optional(),
@@ -8564,6 +8717,114 @@ BaseArtifactSchema.extend({
8564
8717
  })
8565
8718
  });
8566
8719
  /**
8720
+ * Annotation schemas — the collaboration layer that sits on top of a
8721
+ * PublishedArtifact. An annotation is a structured input keyed to an artifact
8722
+ * by its `publicId`. The `kind` discriminator is deliberately generic: v1 ships
8723
+ * only `comment`, but `approval`/`vote`/`signature` are reserved so the layer
8724
+ * can grow into approval workflows and e-signature without a model migration.
8725
+ *
8726
+ * Anchoring is PINS-FIRST: an annotation may carry normalized viewport
8727
+ * coordinates (0..1) and a best-effort CSS selector. Robust element-range
8728
+ * anchoring is deferred. An annotation with no anchor is document-level.
8729
+ *
8730
+ * Every annotation records the artifact version it was made against
8731
+ * (`artifactVersionSha`, the artifact's `sha256Index` at the time) so feedback
8732
+ * stays pinned to the version it critiqued — this is also the input the AI
8733
+ * revision loop (Phase C) consumes.
8734
+ */
8735
+ const AnnotationKindSchema = z.enum([
8736
+ "comment",
8737
+ "approval",
8738
+ "vote",
8739
+ "signature"
8740
+ ]);
8741
+ /** Owner-controlled gate on WHO may annotate, orthogonal to artifact visibility
8742
+ * (which controls who may VIEW). `none` = read-only (today's behavior, default).
8743
+ * `open` = any authenticated viewer who passes the visibility gate. `restricted`
8744
+ * = owner + explicit allowlist (allowlist UI deferred). */
8745
+ const CommentPolicySchema = z.enum([
8746
+ "none",
8747
+ "open",
8748
+ "restricted"
8749
+ ]);
8750
+ const AnnotationAnchorSchema = z.object({
8751
+ /** Normalized horizontal position within the artifact viewport, 0..1. */
8752
+ x: z.number().min(0).max(1).optional(),
8753
+ /** Normalized vertical position within the artifact document, 0..1. */
8754
+ y: z.number().min(0).max(1).optional(),
8755
+ /** Best-effort CSS selector path captured by the widget (may be stale). */
8756
+ selector: z.string().max(1024).optional(),
8757
+ /** Optional scroll-section label / fragment the pin was dropped in. */
8758
+ scrollSection: z.string().max(256).optional()
8759
+ });
8760
+ const AnnotationPayloadSchema = z.object({
8761
+ /** kind === 'approval' */
8762
+ decision: z.enum(["approve", "reject"]).optional(),
8763
+ /** kind === 'vote' */
8764
+ choice: z.string().max(256).optional(),
8765
+ /** kind === 'signature' */
8766
+ signedName: z.string().max(200).optional(),
8767
+ signatureHash: z.string().max(128).optional()
8768
+ }).optional();
8769
+ const ANNOTATION_BODY_MAX = 5e3;
8770
+ z.object({
8771
+ id: z.string(),
8772
+ /** FK → PublishedArtifact.publicId. */
8773
+ publicId: z.string(),
8774
+ /** PublishedArtifact.sha256Index at the time the annotation was made. */
8775
+ artifactVersionSha: z.string().optional(),
8776
+ kind: AnnotationKindSchema.prefault("comment"),
8777
+ authorId: z.string(),
8778
+ /** Denormalized for render — annotation authorship is immutable. */
8779
+ authorDisplayName: z.string().max(200),
8780
+ body: z.string().max(ANNOTATION_BODY_MAX),
8781
+ anchor: AnnotationAnchorSchema.optional(),
8782
+ /** null/absent = top-level; else this is a reply to another annotation. */
8783
+ threadRootId: z.string().nullish(),
8784
+ payload: AnnotationPayloadSchema,
8785
+ resolvedAt: z.date().nullish(),
8786
+ resolvedBy: z.string().nullish(),
8787
+ createdAt: z.date(),
8788
+ updatedAt: z.date(),
8789
+ deletedAt: z.date().nullish(),
8790
+ deletedBy: z.string().nullish()
8791
+ });
8792
+ z.object({
8793
+ body: z.string().min(1).max(ANNOTATION_BODY_MAX),
8794
+ /** v1 accepts only 'comment'; defaulted server-side. */
8795
+ kind: AnnotationKindSchema.optional(),
8796
+ anchor: AnnotationAnchorSchema.optional(),
8797
+ threadRootId: z.string().optional(),
8798
+ artifactVersionSha: z.string().optional(),
8799
+ payload: AnnotationPayloadSchema
8800
+ });
8801
+ z.object({
8802
+ body: z.string().min(1).max(ANNOTATION_BODY_MAX).optional(),
8803
+ /** Toggle resolution. true → resolve, false → reopen. */
8804
+ resolved: z.boolean().optional()
8805
+ }).refine((v) => v.body !== void 0 || v.resolved !== void 0, { message: "Nothing to update" });
8806
+ const AnnotationDtoSchema = z.object({
8807
+ id: z.string(),
8808
+ publicId: z.string(),
8809
+ kind: AnnotationKindSchema,
8810
+ authorId: z.string(),
8811
+ authorDisplayName: z.string(),
8812
+ body: z.string(),
8813
+ anchor: AnnotationAnchorSchema.optional(),
8814
+ threadRootId: z.string().nullish(),
8815
+ payload: AnnotationPayloadSchema,
8816
+ artifactVersionSha: z.string().optional(),
8817
+ resolvedAt: z.string().nullish(),
8818
+ createdAt: z.string()
8819
+ });
8820
+ z.object({
8821
+ annotations: z.array(AnnotationDtoSchema),
8822
+ /** Echoed so the widget can render the right affordance without a 2nd call. */
8823
+ commentPolicy: CommentPolicySchema,
8824
+ /** Whether the CURRENT caller may create an annotation right now. */
8825
+ canComment: z.boolean()
8826
+ });
8827
+ /**
8567
8828
  * Published-artifact schemas — the B4M instantiation of the `artifact-publishing`
8568
8829
  * blueprint (MillionOnMars/blueprints, extracted from Polaris Publish v1).
8569
8830
  *
@@ -8645,6 +8906,9 @@ z.object({
8645
8906
  visibility: VisibilitySchema.prefault("private"),
8646
8907
  /** Group id a viewer must belong to when gated cross-scope. */
8647
8908
  gatedToGroupId: z.string().optional(),
8909
+ /** Collaboration gate: who (among viewers) may annotate. Orthogonal to
8910
+ * `visibility`. Defaults to `none` (read-only) until the owner opts in. */
8911
+ commentPolicy: CommentPolicySchema.prefault("none"),
8648
8912
  ownerId: z.string(),
8649
8913
  lastPublishedBy: z.string().optional(),
8650
8914
  source: PublishSourceSchema,
@@ -8662,6 +8926,8 @@ z.object({
8662
8926
  publishedAt: z.date(),
8663
8927
  previousVersionMeta: ArtifactVersionMetaSchema.optional(),
8664
8928
  viewCount: z.int().nonnegative().prefault(0),
8929
+ /** Concurrency lock for AI revise (set while a revision is in flight). */
8930
+ revisingAt: z.date().nullish(),
8665
8931
  createdAt: z.date(),
8666
8932
  updatedAt: z.date(),
8667
8933
  deletedAt: z.date().nullish(),
@@ -8696,6 +8962,8 @@ z.object({
8696
8962
  description: z.string().max(1e3).optional(),
8697
8963
  visibility: VisibilitySchema.optional(),
8698
8964
  gatedToGroupId: z.string().optional(),
8965
+ /** Who may annotate the published artifact. Defaults to `none` (read-only). */
8966
+ commentPolicy: CommentPolicySchema.optional(),
8699
8967
  source: PublishSourceSchema.optional(),
8700
8968
  files: z.array(FileDescriptorSchema).min(1).max(PUBLISH_LIMITS.maxFiles)
8701
8969
  });
@@ -8851,6 +9119,33 @@ function isGPTImage2Model(model) {
8851
9119
  return model === "gpt-image-2" || model.startsWith("gpt-image-2");
8852
9120
  }
8853
9121
  /**
9122
+ * Whether a user can access a model. Access is any-of (mirrors the Q3b data-lake
9123
+ * rule, `getAccessibleDataLakes`): a non-admin reaches the model via
9124
+ * `allowedUserTags ∩ userTags` OR `allowedEntitlements ∩ entitlementKeys`.
9125
+ *
9126
+ * `entitlementKeys` is optional — when omitted/empty the entitlement branch is
9127
+ * inert, so a model with no `allowedEntitlements` behaves exactly as before
9128
+ * (tag-only). This lets a tag-less subscriber reach an entitlement-gated model
9129
+ * while leaving every existing tag-gated model unchanged (zero regression).
9130
+ *
9131
+ * Pure + zero-dependency (only types + the shared key normalizer), so it lives
9132
+ * in `@bike4mind/common` as the SINGLE source of truth — imported by the core
9133
+ * `@bike4mind/utils` re-export (server/services) AND the client
9134
+ * `useAccessibleModels` hook, which previously kept a hand-rolled twin "to avoid
9135
+ * AWS SDK imports". Common is browser-safe, so there is no longer any reason to
9136
+ * duplicate the logic.
9137
+ */
9138
+ function isModelAccessible(model, userTags, isAdmin = false, entitlementKeys = []) {
9139
+ if (!model.enabled) return false;
9140
+ if (isAdmin) return true;
9141
+ const normalizedUserTags = userTags.map((tag) => tag.toLowerCase());
9142
+ const normalizedAllowedTags = (model.allowedUserTags ?? []).map((tag) => tag.toLowerCase());
9143
+ if (normalizedUserTags.some((tag) => normalizedAllowedTags.includes(tag))) return true;
9144
+ const normalizedKeys = entitlementKeys.map(normalizeEntitlementKey);
9145
+ const normalizedAllowedEntitlements = (model.allowedEntitlements ?? []).map(normalizeEntitlementKey);
9146
+ return normalizedKeys.some((key) => normalizedAllowedEntitlements.includes(key));
9147
+ }
9148
+ /**
8854
9149
  * Telemetry Error Sanitizer
8855
9150
  *
8856
9151
  * Removes PII (Personally Identifiable Information) from error messages
@@ -8976,6 +9271,67 @@ z.array(triggerWordSchema).max(20, "Up to 20 trigger words allowed.").transform(
8976
9271
  }
8977
9272
  return out;
8978
9273
  });
9274
+ /**
9275
+ * Skill body argument substitution. Shared by the server-side SkillsFeature,
9276
+ * the LLM `skill` tool, and the CLI's skill executor so the same patterns
9277
+ * resolve identically across surfaces.
9278
+ *
9279
+ * Supports Claude Code's substitution syntax:
9280
+ * - `$ARGUMENTS` — all positional args joined by spaces
9281
+ * - `$1`, `$2`, ... — individual positional args
9282
+ *
9283
+ * Why a shared helper: the CLI already implemented this in
9284
+ * `apps/cli/src/utils/argumentSubstitution.ts`. Centralizing here avoids a
9285
+ * second drift surface — a skill body authored on the web and run in the CLI
9286
+ * (or vice versa) must expand identically.
9287
+ */
9288
+ function substituteArguments(template, args) {
9289
+ let result = template;
9290
+ for (let i = args.length; i >= 1; i--) {
9291
+ const pattern = new RegExp(`\\$${i}`, "g");
9292
+ result = result.replace(pattern, args[i - 1] ?? "");
9293
+ }
9294
+ return result.replace(/\$ARGUMENTS/g, args.join(" "));
9295
+ }
9296
+ /**
9297
+ * Shell-style argument splitter — supports double / single quotes for
9298
+ * grouping. Identical semantics to the CLI's `parseArguments`.
9299
+ *
9300
+ * parseSkillArguments('hello world') => ['hello', 'world']
9301
+ * parseSkillArguments('"hello world" test') => ['hello world', 'test']
9302
+ * parseSkillArguments("'one two' three") => ['one two', 'three']
9303
+ * parseSkillArguments('') => []
9304
+ *
9305
+ * Unclosed quotes are forgiven, not flagged: `parseSkillArguments('"unclosed')`
9306
+ * returns `['unclosed']`. This matches the CLI's pre-existing behavior and is
9307
+ * deliberate — skills are user-authored prose, not a shell, and surfacing a
9308
+ * parse error for a stray `"` would punish a typo more than the cost of a
9309
+ * slightly-wrong split warrants.
9310
+ */
9311
+ function parseSkillArguments(input) {
9312
+ if (!input) return [];
9313
+ const args = [];
9314
+ let current = "";
9315
+ let inQuotes = false;
9316
+ let quoteChar = "";
9317
+ for (let i = 0; i < input.length; i++) {
9318
+ const char = input[i];
9319
+ if (!inQuotes && (char === "\"" || char === "'")) {
9320
+ inQuotes = true;
9321
+ quoteChar = char;
9322
+ } else if (inQuotes && char === quoteChar) {
9323
+ inQuotes = false;
9324
+ quoteChar = "";
9325
+ } else if (!inQuotes && /\s/.test(char)) {
9326
+ if (current.length > 0) {
9327
+ args.push(current);
9328
+ current = "";
9329
+ }
9330
+ } else current += char;
9331
+ }
9332
+ if (current.length > 0) args.push(current);
9333
+ return args;
9334
+ }
8979
9335
  const VIEW_REGISTRY = [
8980
9336
  {
8981
9337
  id: "opti.root",
@@ -10238,11 +10594,32 @@ const ApiConfigSchema = z.object({ customUrl: z.url().optional() });
10238
10594
  */
10239
10595
  const McpServerSchema = z.object({
10240
10596
  name: z.string(),
10597
+ type: z.enum(["stdio", "http"]).optional(),
10241
10598
  command: z.string().optional(),
10242
10599
  args: z.array(z.string()).optional(),
10600
+ url: z.string().optional(),
10601
+ headers: z.record(z.string(), z.string()).optional(),
10243
10602
  env: z.record(z.string(), z.string()).prefault({}),
10244
10603
  enabled: z.boolean().prefault(true)
10245
- });
10604
+ }).superRefine(mcpServerTransportRefine);
10605
+ /**
10606
+ * A valid MCP server is exactly one of:
10607
+ * - stdio: has `command` (spawned child process)
10608
+ * - http: has `url` (streamable-HTTP endpoint)
10609
+ * Rejecting malformed configs at parse time (not connect time) surfaces errors early.
10610
+ */
10611
+ function mcpServerTransportRefine(server, ctx) {
10612
+ const hasCommand = typeof server.command === "string" && server.command.trim() !== "";
10613
+ const hasUrl = typeof server.url === "string" && server.url.trim() !== "";
10614
+ if (hasCommand && hasUrl) ctx.addIssue({
10615
+ code: "custom",
10616
+ message: "MCP server cannot set both \"command\" (stdio) and \"url\" (http)"
10617
+ });
10618
+ else if (!hasCommand && !hasUrl) ctx.addIssue({
10619
+ code: "custom",
10620
+ message: "MCP server must set either \"command\" (stdio) or \"url\" (http)"
10621
+ });
10622
+ }
10246
10623
  /**
10247
10624
  * MCP Servers can be specified in two formats:
10248
10625
  * 1. Array format (B4M native): [{ "name": "...", "command": "...", ... }]
@@ -10251,11 +10628,14 @@ const McpServerSchema = z.object({
10251
10628
  * Both formats are supported for maximum flexibility and compatibility.
10252
10629
  */
10253
10630
  const McpServersSchema = z.union([z.array(McpServerSchema), z.record(z.string(), z.object({
10631
+ type: z.enum(["stdio", "http"]).optional(),
10254
10632
  command: z.string().optional(),
10255
10633
  args: z.array(z.string()).optional(),
10634
+ url: z.string().optional(),
10635
+ headers: z.record(z.string(), z.string()).optional(),
10256
10636
  env: z.record(z.string(), z.string()).optional(),
10257
10637
  enabled: z.boolean().optional()
10258
- }))]);
10638
+ }).superRefine(mcpServerTransportRefine))]);
10259
10639
  /**
10260
10640
  * Normalize MCP servers to internal array format
10261
10641
  * Accepts both array and object formats
@@ -10263,15 +10643,21 @@ const McpServersSchema = z.union([z.array(McpServerSchema), z.record(z.string(),
10263
10643
  function normalizeMcpServers(servers) {
10264
10644
  if (Array.isArray(servers)) return servers.map((server) => ({
10265
10645
  name: server.name,
10646
+ type: server.type,
10266
10647
  command: server.command,
10267
10648
  args: server.args,
10649
+ url: server.url,
10650
+ headers: server.headers,
10268
10651
  env: server.env || {},
10269
10652
  enabled: server.enabled ?? true
10270
10653
  }));
10271
10654
  else return Object.entries(servers).map(([name, config]) => ({
10272
10655
  name,
10656
+ type: config.type,
10273
10657
  command: config.command,
10274
10658
  args: config.args,
10659
+ url: config.url,
10660
+ headers: config.headers,
10275
10661
  env: config.env || {},
10276
10662
  enabled: config.enabled ?? true
10277
10663
  }));
@@ -10302,6 +10688,14 @@ const CliConfigSchema = z.object({
10302
10688
  exportFormat: z.enum(["markdown", "json"]),
10303
10689
  maxIterations: z.number().nullable().prefault(10),
10304
10690
  enableSkillTool: z.boolean().optional().prefault(true),
10691
+ /**
10692
+ * When false (or set via the `--no-remote-skills` CLI flag / the
10693
+ * `B4M_NO_REMOTE_SKILLS=1` env var), the CLI skips fetching skills from
10694
+ * the B4M backend's `/api/skills` endpoint and runs with local files only.
10695
+ * Defaults to true so authenticated users get cross-machine skill sync
10696
+ * out of the box.
10697
+ */
10698
+ enableRemoteSkills: z.boolean().optional().prefault(true),
10305
10699
  enableDynamicAgentCreation: z.boolean().optional().prefault(false),
10306
10700
  enableCoordinatorMode: z.boolean().optional().prefault(false),
10307
10701
  /**
@@ -10393,6 +10787,7 @@ const DEFAULT_CONFIG = {
10393
10787
  exportFormat: "markdown",
10394
10788
  maxIterations: 10,
10395
10789
  enableSkillTool: true,
10790
+ enableRemoteSkills: true,
10396
10791
  enableDynamicAgentCreation: false,
10397
10792
  enableCoordinatorMode: false,
10398
10793
  promptVariant: "current"
@@ -10503,6 +10898,29 @@ async function loadMcpJsonConfig(projectDir) {
10503
10898
  }
10504
10899
  }
10505
10900
  /**
10901
+ * Load an explicit `--mcp-config <file>` (claude shape: `{ "mcpServers": {...} }`).
10902
+ * Unlike `.mcp.json` this is an absolute path passed at launch, not project-relative.
10903
+ * Returns null on missing/malformed file (a bad config must not brick the launch).
10904
+ */
10905
+ async function loadMcpConfigFile(filePath) {
10906
+ try {
10907
+ const data = await promises.readFile(filePath, "utf-8");
10908
+ const rawConfig = JSON.parse(data);
10909
+ return normalizeMcpServers(McpJsonConfigSchema.parse(rawConfig).mcpServers);
10910
+ } catch (error) {
10911
+ if (error.code === "ENOENT") {
10912
+ console.error(`--mcp-config file not found: ${filePath}`);
10913
+ return null;
10914
+ }
10915
+ if (error instanceof z.ZodError) {
10916
+ console.error("--mcp-config validation error:", error.issues);
10917
+ return null;
10918
+ }
10919
+ console.error("Failed to load --mcp-config file:", error);
10920
+ return null;
10921
+ }
10922
+ }
10923
+ /**
10506
10924
  * Merge MCP servers from multiple configs
10507
10925
  * Later configs can override earlier ones by name
10508
10926
  */
@@ -10684,6 +11102,12 @@ var ConfigStore = class {
10684
11102
  } else this.projectConfigDir = null;
10685
11103
  const mergedConfig = mergeConfigs(globalConfig, projectConfig, projectLocalConfig);
10686
11104
  if (mcpJsonServers && mcpJsonServers.length > 0) mergedConfig.mcpServers = mergeMcpServers(mcpJsonServers, mergedConfig.mcpServers);
11105
+ const mcpConfigFile = process.env.B4M_MCP_CONFIG_FILE;
11106
+ if (mcpConfigFile) {
11107
+ const injected = await loadMcpConfigFile(mcpConfigFile);
11108
+ if (process.env.B4M_STRICT_MCP_CONFIG === "1") mergedConfig.mcpServers = injected ?? [];
11109
+ else if (injected) mergedConfig.mcpServers = mergeMcpServers(mergedConfig.mcpServers, injected);
11110
+ }
10687
11111
  this.config = mergedConfig;
10688
11112
  return this.config;
10689
11113
  } catch (error) {
@@ -11030,4 +11454,4 @@ var ConfigStore = class {
11030
11454
  }
11031
11455
  };
11032
11456
  //#endregion
11033
- export { ProjectEvents as $, GenerateImageToolCallSchema as A, dayjsConfig_default as At, InviteEvents as B, resolveNavigationIntents as Bt, ElabsEvents as C, UnauthorizedError as Ct, ForbiddenError as D, VideoModels as Dt, FileEvents as E, VideoGenerationUsageTransaction as Et, ImageEditUsageTransaction as F, isGPTImage2Model as Ft, ModalEvents as G, validateNotebookPath as Gt, KnowledgeType as H, secureParameters as Ht, ImageGenerationUsageTransaction as I, isGPTImageModel as It, OpenAIEmbeddingModel as J, isNearLimit as Jt, ModelBackend as K, buildRateLimitLogEntry as Kt, ImageModels as L, isSupportedEmbeddingModel as Lt, GenericCreditDeductTransaction as M, getDataLakeTags as Mt, HTTPError as N, getMcpProviderMetadata as Nt, FriendshipEvents as O, XAI_IMAGE_MODELS as Ot, HttpStatus as P, getViewById as Pt, ProfileEvents as Q, InboxEvents as R, isZodError as Rt, DashboardParamsSchema as S, UiNavigationEvents as St, FeedbackEvents as T, VIDEO_SIZE_CONSTRAINTS as Tt, LLMEvents as U, settingsMap as Ut, InviteType as V, sanitizeTelemetryError as Vt, MiscEvents as W, validateJupyterKernelName as Wt, Permission as X, CollectionType as Xt, OpenAIImageGenerationInput as Y, parseRateLimitHeaders as Yt, PermissionDeniedError as Z, ChatCompletionCreateInputSchema as _, TaskScheduleHandler as _t, ALERT_THRESHOLDS as a, ReceivedCreditTransaction as at, CompletionApiUsageTransaction as b, ToolUsageTransaction as bt, ApiKeyScope as c, ResearchModeParamsSchema as ct, ArtifactTypeSchema as d, ResearchTaskType as dt, PromptIntentSchema as et, AuthEvents as f, SessionEvents as ft, CREDIT_DEDUCT_TRANSACTION_TYPES as g, TagType as gt, BadRequestError as h, SupportedFabFileMimeTypes as ht, getEnvironmentName as i, RealtimeVoiceUsageTransaction as it, GenericCreditAddTransaction as j, getAccessibleDataLakes as jt, GEMINI_IMAGE_MODELS as k, b4mLLMTools as kt, ApiKeyType as l, ResearchTaskExecutionType as lt, BFL_SAFETY_TOLERANCE as m, SubscriptionCreditTransaction as mt, logger as n, PurchaseTransaction as nt, AiEvents as o, RechartsChartTypeList as ot, BFL_IMAGE_MODELS as p, SpeechToTextUsageTransaction as pt, NotFoundError as q, extractSnippetMeta as qt, getApiUrl as r, QuestMasterParamsSchema as rt, ApiKeyEvents as s, RegInviteEvents as st, ConfigStore as t, PromptMetaZodSchema as tt, AppFileEvents as u, ResearchTaskPeriodicFrequencyType as ut, ChatModels as v, TextGenerationUsageTransaction as vt, FavoriteDocumentType as w, UnprocessableEntityError as wt, CorruptedFileError as x, TransferCreditTransaction as xt, ClaudeArtifactMimeTypes as y, TooManyRequestsError as yt, InternalServerError as z, obfuscateApiKey as zt };
11457
+ export { ProjectEvents as $, parseRateLimitHeaders as $t, GenerateImageToolCallSchema as A, dayjsConfig_default as At, InviteEvents as B, obfuscateApiKey as Bt, ElabsEvents as C, UnauthorizedError as Ct, ForbiddenError as D, VideoModels as Dt, FileEvents as E, VideoGenerationUsageTransaction as Et, ImageEditUsageTransaction as F, isGPTImageModel as Ft, ModalEvents as G, settingsMap as Gt, KnowledgeType as H, resolveNavigationIntents as Ht, ImageGenerationUsageTransaction as I, isModelAccessible as It, OpenAIEmbeddingModel as J, validateJupyterKernelName as Jt, ModelBackend as K, substituteArguments as Kt, ImageModels as L, isSupportedEmbeddingModel as Lt, GenericCreditDeductTransaction as M, getMcpProviderMetadata as Mt, HTTPError as N, getViewById as Nt, FriendshipEvents as O, XAI_IMAGE_MODELS as Ot, HttpStatus as P, isGPTImage2Model as Pt, ProfileEvents as Q, isNearLimit as Qt, InboxEvents as R, isZodError as Rt, DashboardParamsSchema as S, UiNavigationEvents as St, FeedbackEvents as T, VIDEO_SIZE_CONSTRAINTS as Tt, LLMEvents as U, sanitizeTelemetryError as Ut, InviteType as V, parseSkillArguments as Vt, MiscEvents as W, secureParameters as Wt, Permission as X, buildRateLimitLogEntry as Xt, OpenAIImageGenerationInput as Y, validateNotebookPath as Yt, PermissionDeniedError as Z, extractSnippetMeta as Zt, ChatCompletionCreateInputSchema as _, TaskScheduleHandler as _t, ALERT_THRESHOLDS as a, ReceivedCreditTransaction as at, CompletionApiUsageTransaction as b, ToolUsageTransaction as bt, ApiKeyScope as c, ResearchModeParamsSchema as ct, ArtifactTypeSchema as d, ResearchTaskType as dt, CollectionType as en, PromptIntentSchema as et, AuthEvents as f, SessionEvents as ft, CREDIT_DEDUCT_TRANSACTION_TYPES as g, TagType as gt, BadRequestError as h, SupportedFabFileMimeTypes as ht, getEnvironmentName as i, RealtimeVoiceUsageTransaction as it, GenericCreditAddTransaction as j, getAccessibleDataLakes as jt, GEMINI_IMAGE_MODELS as k, b4mLLMTools as kt, ApiKeyType as l, ResearchTaskExecutionType as lt, BFL_SAFETY_TOLERANCE as m, SubscriptionCreditTransaction as mt, logger as n, PurchaseTransaction as nt, AiEvents as o, RechartsChartTypeList as ot, BFL_IMAGE_MODELS as p, SpeechToTextUsageTransaction as pt, NotFoundError as q, toDataLakeConfig as qt, getApiUrl as r, QuestMasterParamsSchema as rt, ApiKeyEvents as s, RegInviteEvents as st, ConfigStore as t, PromptMetaZodSchema as tt, AppFileEvents as u, ResearchTaskPeriodicFrequencyType as ut, ChatModels as v, TextGenerationUsageTransaction as vt, FavoriteDocumentType as w, UnprocessableEntityError as wt, CorruptedFileError as x, TransferCreditTransaction as xt, ClaudeArtifactMimeTypes as y, TooManyRequestsError as yt, InternalServerError as z, mapMimeTypeToArtifactType as zt };