create-workframe 0.1.18 → 0.1.21
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/README.md +1 -1
- package/bin/create-workframe.js +73 -9
- package/package.json +1 -1
- package/scripts/bundle-workframe-ui.mjs +11 -1
- package/workframe-api/action_proxy.py +8 -0
- package/workframe-api/chat_sessions.py +37 -6
- package/workframe-api/chat_stream.py +16 -0
- package/workframe-api/db_schema.py +29 -0
- package/workframe-api/handler_modules/handler_admin.py +32 -0
- package/workframe-api/handler_modules/handler_auth.py +6 -4
- package/workframe-api/handler_modules/handler_chat.py +17 -26
- package/workframe-api/handler_modules/handler_provider.py +13 -14
- package/workframe-api/handler_modules/handler_workspace.py +60 -2
- package/workframe-api/hermes_profiles.py +31 -9
- package/workframe-api/llm_proxy.py +18 -0
- package/workframe-api/mention_invoke.py +29 -24
- package/workframe-api/message_reactions.py +139 -0
- package/workframe-api/model_surface.py +329 -113
- package/workframe-api/package.json +4 -4
- package/workframe-api/profile_gateway.py +14 -6
- package/workframe-api/provider_bindings.py +118 -17
- package/workframe-api/provider_bootstrap.py +74 -38
- package/workframe-api/provider_model_catalog.py +261 -0
- package/workframe-api/rooms.py +4 -3
- package/workframe-api/route_registry.py +4 -0
- package/workframe-api/runtime_cohort.py +118 -23
- package/workframe-api/scripts/run-python.mjs +28 -0
- package/workframe-api/server.py +44 -12
- package/workframe-api/test_agent_identity_authority.py +96 -0
- package/workframe-api/test_chat_attribution_history.py +59 -0
- package/workframe-api/test_enrich_room_messages.py +43 -2
- package/workframe-api/test_llm_proxy_encoding.py +33 -0
- package/workframe-api/test_message_reactions.py +151 -0
- package/workframe-api/test_model_routing_reconcile.py +122 -0
- package/workframe-api/test_model_surface_consistency.py +45 -5
- package/workframe-api/test_oauth_llm.py +1 -1
- package/workframe-api/test_profile_gateway_health.py +37 -0
- package/workframe-api/test_provider_bindings.py +115 -1
- package/workframe-api/test_provider_model_catalog.py +27 -0
- package/workframe-api/test_provider_model_surface.py +2 -2
- package/workframe-api/test_runtime_lease_lifecycle.py +93 -0
- package/workframe-api/test_server_reexports.py +2 -0
- package/workframe-api/test_user_llm_prefs.py +27 -0
- package/workframe-api/test_user_theme_prefs.py +65 -0
- package/workframe-api/test_workspace_file_actions.py +119 -0
- package/workframe-api/turn_credentials.py +41 -0
- package/workframe-api/turn_overlay.py +95 -34
- package/workframe-api/user_prefs.py +22 -1
- package/workframe-api/workspace_bootstrap.py +1 -0
- package/workframe-api/workspace_files.py +124 -0
- package/workframe-api/zk_auth.py +7 -1
- package/workframe-ui/public/assets/{arc-CSddOtMa.js → arc-D_gXvZHy.js} +1 -1
- package/workframe-ui/public/assets/architecture-7EHR7CIX-B8ZI_Iqb.js +1 -0
- package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-BSiUei3D.js → architectureDiagram-3BPJPVTR-2F65XJkk.js} +1 -1
- package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-BhKTULPo.js → blockDiagram-GPEHLZMM-BoURuzr5.js} +1 -1
- package/workframe-ui/public/assets/{brave-B885JYnH.svg → brave-BoYMbZi1.svg} +0 -1
- package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-Bv_xZi-a.js → c4Diagram-AAUBKEIU-BBtveYSm.js} +1 -1
- package/workframe-ui/public/assets/channel-Ca1FvZ7w.js +1 -0
- package/workframe-ui/public/assets/{chunk-2J33WTMH-Bh0WiqFE.js → chunk-2J33WTMH-BCuhFg2u.js} +1 -1
- package/workframe-ui/public/assets/{chunk-3OPIFGDE-qN09pRuK.js → chunk-3OPIFGDE-gM6Q-rjJ.js} +1 -1
- package/workframe-ui/public/assets/{chunk-4BX2VUAB-Dz6FVuMa.js → chunk-4BX2VUAB-D11CJMfF.js} +1 -1
- package/workframe-ui/public/assets/{chunk-55IACEB6-DWtPHVfR.js → chunk-55IACEB6-tOvPBzSf.js} +1 -1
- package/workframe-ui/public/assets/{chunk-5ZQYHXKU-CyOUeDAo.js → chunk-5ZQYHXKU-Bb1uF7QP.js} +1 -1
- package/workframe-ui/public/assets/{chunk-727SXJPM-BrICvWwx.js → chunk-727SXJPM-BvzzLkGB.js} +1 -1
- package/workframe-ui/public/assets/{chunk-AQP2D5EJ-CQu2SmLM.js → chunk-AQP2D5EJ-tr5A2PUD.js} +1 -1
- package/workframe-ui/public/assets/{chunk-BSJP7CBP-uJbYXNNG.js → chunk-BSJP7CBP-BSFEsidk.js} +1 -1
- package/workframe-ui/public/assets/{chunk-CSCIHK7Q-CCS_FtVZ.js → chunk-CSCIHK7Q-Bgf-IHHq.js} +1 -1
- package/workframe-ui/public/assets/{chunk-FMBD7UC4-CoLUtPiK.js → chunk-FMBD7UC4-rD27Gf3U.js} +1 -1
- package/workframe-ui/public/assets/{chunk-KSCS5N6A-DLW4gvEW.js → chunk-KSCS5N6A-C1MobXen.js} +1 -1
- package/workframe-ui/public/assets/{chunk-L5ZTLDWV-CMt0LJAd.js → chunk-L5ZTLDWV-CD2wS3bn.js} +1 -1
- package/workframe-ui/public/assets/{chunk-LZXEDZCA-Bn-maIlt.js → chunk-LZXEDZCA-DQxuSIdE.js} +2 -2
- package/workframe-ui/public/assets/{chunk-ND2GUHAM-CG7yoApR.js → chunk-ND2GUHAM-DHlBi932.js} +1 -1
- package/workframe-ui/public/assets/{chunk-NZK2D7GU-B4dIvbii.js → chunk-NZK2D7GU-MB8bO5o8.js} +1 -1
- package/workframe-ui/public/assets/{chunk-O5CBEL6O-B5spDyXy.js → chunk-O5CBEL6O-BmPwoKAr.js} +1 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-BsuC0swk.js +1 -0
- package/workframe-ui/public/assets/chunk-WU5MYG2G-CyfRhD17.js +1 -0
- package/workframe-ui/public/assets/{chunk-XPW4576I-D5c0TMX6.js → chunk-XPW4576I-BUCJoags.js} +1 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-DZ_KubnM.js +1 -0
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-DZ_KubnM.js +1 -0
- package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-DFRM3DZv.js → cose-bilkent-S5V4N54A-w9hpFlQU.js} +1 -1
- package/workframe-ui/public/assets/{dagre-BM42HDAG-CnUdKeJa.js → dagre-BM42HDAG-DEnBMVrR.js} +1 -1
- package/workframe-ui/public/assets/{diagram-2AECGRRQ-Ds3-e-hW.js → diagram-2AECGRRQ-CMoYqkVC.js} +1 -1
- package/workframe-ui/public/assets/{diagram-5GNKFQAL-B41zx8FF.js → diagram-5GNKFQAL-rS0QQe2R.js} +1 -1
- package/workframe-ui/public/assets/{diagram-KO2AKTUF-D83tI9I8.js → diagram-KO2AKTUF-Do63Vbzi.js} +1 -1
- package/workframe-ui/public/assets/{diagram-LMA3HP47-h_EuFyLI.js → diagram-LMA3HP47-Bq3e2FDx.js} +1 -1
- package/workframe-ui/public/assets/{diagram-OG6HWLK6-CpU-d3RZ.js → diagram-OG6HWLK6-DTESfGaG.js} +1 -1
- package/workframe-ui/public/assets/{dist-_igeSJ9r.js → dist-BiIfSARy.js} +1 -1
- package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-jLFoncRU.js → erDiagram-TEJ5UH35-C7RA0P76.js} +1 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-DChy3wGd.js +1 -0
- package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-C9IaNgCy.js → flowDiagram-I6XJVG4X-e47AwJAm.js} +1 -1
- package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-A9FhfFc5.js → ganttDiagram-6RSMTGT7-BVytj000.js} +1 -1
- package/workframe-ui/public/assets/{gemini-Cq-0-rmr.svg → gemini-DqnZlvMo.svg} +0 -1
- package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-BqK_IjZb.js → gitGraph-WXDBUCRP-QdT2gCGh.js} +1 -1
- package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-CDVqxhyW.js → gitGraphDiagram-PVQCEYII-COEavQVV.js} +1 -1
- package/workframe-ui/public/assets/index-03WsLxfn.js +133 -0
- package/workframe-ui/public/assets/index-BIKn2D6J.css +1 -0
- package/workframe-ui/public/assets/{info-J43DQDTF-C2EKRglL.js → info-J43DQDTF-CEyVZ-9w.js} +1 -1
- package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-azQtFzpU.js → infoDiagram-5YYISTIA-CV3ftI3x.js} +1 -1
- package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-BuDOLv1t.js → ishikawaDiagram-YF4QCWOH-M8CoaInm.js} +1 -1
- package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-ypTHFoT0.js → journeyDiagram-JHISSGLW-BtoaC0oj.js} +1 -1
- package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-Cgr78J_x.js → kanban-definition-UN3LZRKU-Dke-ozKB.js} +1 -1
- package/workframe-ui/public/assets/{line-CcwgaMdv.js → line-CDg_Ms_2.js} +1 -1
- package/workframe-ui/public/assets/{linear-CswIlWaW.js → linear-D3OWfPXS.js} +1 -1
- package/workframe-ui/public/assets/{mermaid-parser.core-CQYhP37X.js → mermaid-parser.core-CGDFs4AZ.js} +2 -2
- package/workframe-ui/public/assets/{mermaid.core-Cz1XNBaK.js → mermaid.core-C9-q_XHF.js} +3 -3
- package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-BJLu0829.js → mindmap-definition-RKZ34NQL-BvEiBAWz.js} +1 -1
- package/workframe-ui/public/assets/{packet-YPE3B663-AB1qh96V.js → packet-YPE3B663-FF5nKMbE.js} +1 -1
- package/workframe-ui/public/assets/{pie-LRSECV5Y-CokIXLdT.js → pie-LRSECV5Y-CZ9ijSae.js} +1 -1
- package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-DyrwQn5F.js → pieDiagram-4H26LBE5-kD9o509O.js} +1 -1
- package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-C_Fdg3F6.js → quadrantDiagram-W4KKPZXB-d_eA7_-T.js} +1 -1
- package/workframe-ui/public/assets/{radar-GUYGQ44K-C21Xfrrg.js → radar-GUYGQ44K-ViyzC1rg.js} +1 -1
- package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-ChhaWgaF.js → requirementDiagram-4Y6WPE33-Sq3D_b4Z.js} +1 -1
- package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-C5T4sDTt.js → sankeyDiagram-5OEKKPKP-ZZnl3ZwZ.js} +1 -1
- package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-DRks8i3A.js → sequenceDiagram-3UESZ5HK-Yaz2MgMq.js} +1 -1
- package/workframe-ui/public/assets/{src-BQu5waHd.js → src-DBh8rT2Z.js} +1 -1
- package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BI-AKYta.js → stateDiagram-AJRCARHV-DVwi_P3W.js} +1 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-DRSxEx1_.js +1 -0
- package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-DnAIWzEc.js → timeline-definition-PNZ67QCA-C88RoMRK.js} +1 -1
- package/workframe-ui/public/assets/{treeView-BLDUP644-BEF_sjx7.js → treeView-BLDUP644-2xabSpow.js} +1 -1
- package/workframe-ui/public/assets/{treemap-LRROVOQU-BcbL7g5c.js → treemap-LRROVOQU-BA_oRW4e.js} +1 -1
- package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-DAoIbCkz.js → vennDiagram-CIIHVFJN-DvyVA588.js} +1 -1
- package/workframe-ui/public/assets/{wardley-L42UT6IY-3SOmFBFw.js → wardley-L42UT6IY-BR0suGqb.js} +1 -1
- package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DlQV_-LH.js → wardleyDiagram-YWT4CUSO-CXMvBEO9.js} +1 -1
- package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-CVoTTRHF.js → xychartDiagram-2RQKCTM6-wM7UrTHx.js} +1 -1
- package/workframe-ui/public/index.html +23 -12
- package/workframe-ui/public/workframe-build.json +4 -3
- package/workframe-ui/public/assets/architecture-7EHR7CIX-CDDNM1ni.js +0 -1
- package/workframe-ui/public/assets/channel-R52LrLO3.js +0 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-C_9MbT10.js +0 -1
- package/workframe-ui/public/assets/chunk-WU5MYG2G-VJadzJmf.js +0 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-CpBjaI3g.js +0 -1
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-CpBjaI3g.js +0 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-kzOxPvtj.js +0 -1
- package/workframe-ui/public/assets/index-Bco9IDMA.css +0 -1
- package/workframe-ui/public/assets/index-Do34PZS2.js +0 -132
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-vJyhFZbx.js +0 -1
|
@@ -131,9 +131,7 @@ def _set_profile_model_base_url(profile: str, base_url: str) -> tuple[bool, str]
|
|
|
131
131
|
|
|
132
132
|
|
|
133
133
|
def _proxy_fallback_chain(profile: str, billing_provider: str) -> None:
|
|
134
|
-
"""
|
|
135
|
-
if not _srv()._is_runtime_profile_slug(profile):
|
|
136
|
-
return
|
|
134
|
+
"""Vault-backed fallbacks use provider=custom so Hermes keeps proxy auth."""
|
|
137
135
|
block = _srv()._read_model_block(profile)
|
|
138
136
|
chain = block.get("fallback_chain") or []
|
|
139
137
|
billing = str(billing_provider or "openrouter").strip().lower()
|
|
@@ -214,16 +212,17 @@ def _profile_llm_proxy_ready(profile: str, provider: str) -> bool:
|
|
|
214
212
|
llm_provider = str(provider or "openrouter").strip().lower()
|
|
215
213
|
if str(block.get("base_url") or "").strip() != _llm_proxy_base_url(llm_provider):
|
|
216
214
|
return False
|
|
217
|
-
if not _srv()._is_runtime_profile_slug(prof):
|
|
218
|
-
return True
|
|
219
215
|
if str(block.get("provider") or "").strip().lower() != "custom":
|
|
220
216
|
return False
|
|
217
|
+
valid_fallbacks = 0
|
|
221
218
|
for entry in block.get("fallback_chain") or []:
|
|
222
219
|
prov = str(entry.get("provider", "")).strip().lower()
|
|
223
220
|
model = str(entry.get("model", "")).strip()
|
|
221
|
+
if model and prov:
|
|
222
|
+
valid_fallbacks += 1
|
|
224
223
|
if model and prov == llm_provider and prov != "custom":
|
|
225
224
|
return False
|
|
226
|
-
return
|
|
225
|
+
return valid_fallbacks > 0
|
|
227
226
|
|
|
228
227
|
|
|
229
228
|
def _ensure_profile_llm_proxy(profile: str, provider: str) -> None:
|
|
@@ -232,20 +231,22 @@ def _ensure_profile_llm_proxy(profile: str, provider: str) -> None:
|
|
|
232
231
|
prof = _srv().safe_profile_slug(profile)
|
|
233
232
|
if _srv()._oauth_llm_provider_spec(llm_provider):
|
|
234
233
|
_srv()._normalize_profile_config_yaml(prof)
|
|
235
|
-
_srv().
|
|
234
|
+
current_model = str(_srv()._read_model_block(prof).get("default") or "").strip()
|
|
235
|
+
if current_model:
|
|
236
|
+
_srv()._apply_model_for_billing_provider(prof, llm_provider, current_model)
|
|
237
|
+
else:
|
|
238
|
+
_srv()._apply_mvp_model_for_provider(prof, llm_provider)
|
|
236
239
|
return
|
|
237
240
|
_srv()._normalize_profile_config_yaml(prof)
|
|
238
241
|
if _profile_llm_proxy_ready(prof, llm_provider):
|
|
239
242
|
_ensure_profile_proxy_headers(prof)
|
|
240
|
-
|
|
241
|
-
_proxy_fallback_chain(prof, llm_provider)
|
|
243
|
+
_proxy_fallback_chain(prof, llm_provider)
|
|
242
244
|
return
|
|
243
245
|
_coalesce_profile_model_yaml(prof)
|
|
244
246
|
_set_profile_model_base_url(prof, _llm_proxy_base_url(llm_provider))
|
|
245
247
|
_ensure_profile_proxy_headers(prof)
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
_proxy_fallback_chain(prof, llm_provider)
|
|
248
|
+
_srv()._set_profile_model_provider(prof, "custom")
|
|
249
|
+
_proxy_fallback_chain(prof, llm_provider)
|
|
249
250
|
|
|
250
251
|
|
|
251
252
|
def _set_profile_model_api_key(profile: str, api_key: str) -> tuple[bool, str]:
|
|
@@ -345,6 +346,24 @@ def _read_config_model_api_key(profile: str) -> str:
|
|
|
345
346
|
return str(_srv()._read_model_block(profile).get("api_key") or "").strip()
|
|
346
347
|
|
|
347
348
|
|
|
349
|
+
def _runtime_llm_lease_id(
|
|
350
|
+
profile: str,
|
|
351
|
+
user_id: str,
|
|
352
|
+
workspace_id: str,
|
|
353
|
+
provider: str,
|
|
354
|
+
) -> str:
|
|
355
|
+
"""Stable DB key for the opaque credential bound to one user runtime."""
|
|
356
|
+
seed = "\x1f".join(
|
|
357
|
+
(
|
|
358
|
+
str(profile or "").strip(),
|
|
359
|
+
str(user_id or "").strip(),
|
|
360
|
+
str(workspace_id or "").strip(),
|
|
361
|
+
str(provider or "openrouter").strip().lower(),
|
|
362
|
+
),
|
|
363
|
+
)
|
|
364
|
+
return f"runtime-llm:{uuid.uuid5(uuid.NAMESPACE_URL, seed).hex}"
|
|
365
|
+
|
|
366
|
+
|
|
348
367
|
|
|
349
368
|
|
|
350
369
|
def _sync_profile_model_api_key(profile: str, api_key: str, *, wait_healthy: bool = False) -> None:
|
|
@@ -405,8 +424,16 @@ def _apply_turn_credential_lease(
|
|
|
405
424
|
workspace_id: str,
|
|
406
425
|
provider: str,
|
|
407
426
|
run_id: str,
|
|
427
|
+
*,
|
|
428
|
+
wait_healthy: bool = True,
|
|
408
429
|
) -> str:
|
|
409
|
-
"""
|
|
430
|
+
"""Bind an opaque LLM broker token to the user runtime.
|
|
431
|
+
|
|
432
|
+
``run_id`` remains part of the public call shape because action credentials
|
|
433
|
+
really are turn-scoped. LLM credentials are runtime-scoped: Hermes caches
|
|
434
|
+
``model.api_key``, so per-turn rotation/revocation would restart the gateway
|
|
435
|
+
during normal chat and sever active streams.
|
|
436
|
+
"""
|
|
410
437
|
provider = str(provider or "openrouter").strip().lower()
|
|
411
438
|
prof = _srv().resolve_hermes_profile(profile)
|
|
412
439
|
env_var = _profile_lease_env_var(prof, provider)
|
|
@@ -428,9 +455,9 @@ def _apply_turn_credential_lease(
|
|
|
428
455
|
binding_id=str(lease_meta.get("credential_binding_id") or ""),
|
|
429
456
|
):
|
|
430
457
|
# ponytail: hot path — skip credential resolve + env rewrite when lease still valid
|
|
431
|
-
if
|
|
458
|
+
if existing_token:
|
|
432
459
|
if _read_config_model_api_key(prof) != existing_token:
|
|
433
|
-
_sync_profile_model_api_key(prof, existing_token)
|
|
460
|
+
_sync_profile_model_api_key(prof, existing_token, wait_healthy=wait_healthy)
|
|
434
461
|
return existing_token
|
|
435
462
|
|
|
436
463
|
if _srv()._is_runtime_profile_slug(prof):
|
|
@@ -442,7 +469,7 @@ def _apply_turn_credential_lease(
|
|
|
442
469
|
_srv()._sync_oauth_llm_to_profile(prof, user_id, provider)
|
|
443
470
|
if not _srv()._profile_routing_matches_billing(prof, provider):
|
|
444
471
|
current_model = str(_srv()._read_model_from_config(prof)[1] or "").strip()
|
|
445
|
-
if not
|
|
472
|
+
if not current_model:
|
|
446
473
|
current_model = str(
|
|
447
474
|
(_srv().PROVIDER_MVP_MODELS.get(provider) or {}).get("primary") or current_model
|
|
448
475
|
).strip()
|
|
@@ -472,24 +499,27 @@ def _apply_turn_credential_lease(
|
|
|
472
499
|
provider=provider,
|
|
473
500
|
binding_id=binding_id,
|
|
474
501
|
):
|
|
475
|
-
if
|
|
502
|
+
if existing_token:
|
|
476
503
|
if _read_config_model_api_key(prof) != existing_token:
|
|
477
|
-
_sync_profile_model_api_key(prof, existing_token)
|
|
504
|
+
_sync_profile_model_api_key(prof, existing_token, wait_healthy=wait_healthy)
|
|
478
505
|
return existing_token
|
|
479
506
|
|
|
507
|
+
lease_id = _runtime_llm_lease_id(prof, user_id, workspace_id, provider)
|
|
480
508
|
token = turn_credentials.issue_lease(
|
|
481
|
-
|
|
509
|
+
lease_id,
|
|
482
510
|
user_id,
|
|
483
511
|
workspace_id,
|
|
484
512
|
provider,
|
|
485
513
|
prof,
|
|
486
514
|
binding_id or None,
|
|
515
|
+
ttl_seconds=turn_credentials.RUNTIME_TTL,
|
|
487
516
|
)
|
|
488
517
|
if token != existing_token:
|
|
489
518
|
_srv()._upsert_env_secret(_srv()._profile_dir(prof) / ".env", env_var, token)
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
519
|
+
_ensure_profile_llm_proxy(prof, provider)
|
|
520
|
+
# config.yaml is cached by Hermes. Wait for the reload here; scheduling it
|
|
521
|
+
# in the background races the request and causes intermittent disconnects.
|
|
522
|
+
_sync_profile_model_api_key(prof, token, wait_healthy=wait_healthy)
|
|
493
523
|
return token
|
|
494
524
|
|
|
495
525
|
|
|
@@ -608,20 +638,33 @@ def _apply_action_credential_lease(
|
|
|
608
638
|
|
|
609
639
|
|
|
610
640
|
def _revoke_turn_credential_lease(run_id: str, profile: str) -> None:
|
|
611
|
-
"""Revoke
|
|
641
|
+
"""Revoke genuinely turn-scoped action leases after a run.
|
|
642
|
+
|
|
643
|
+
The profile's LLM broker token is deliberately retained. It contains no
|
|
644
|
+
provider secret, is profile-bound by the internal proxy, and is resolved
|
|
645
|
+
against the owner's current vault binding on every request.
|
|
646
|
+
"""
|
|
612
647
|
run_id = str(run_id or "").strip()
|
|
613
648
|
if not run_id:
|
|
614
649
|
return
|
|
615
|
-
turn_credentials.revoke_lease(run_id)
|
|
616
650
|
for provider_id, _env_var in _user_action_env_specs():
|
|
617
651
|
turn_credentials.revoke_lease(f"{run_id}::{provider_id}")
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def _revoke_runtime_llm_leases(
|
|
655
|
+
*,
|
|
656
|
+
payer_user_id: str = "",
|
|
657
|
+
workspace_id: str = "",
|
|
658
|
+
provider: str = "",
|
|
659
|
+
credential_binding_id: str = "",
|
|
660
|
+
) -> int:
|
|
661
|
+
"""Invalidate opaque runtime credentials when vault authority changes."""
|
|
662
|
+
return turn_credentials.revoke_matching_leases(
|
|
663
|
+
payer_user_id=payer_user_id,
|
|
664
|
+
workspace_id=workspace_id,
|
|
665
|
+
provider=provider,
|
|
666
|
+
credential_binding_id=credential_binding_id,
|
|
667
|
+
)
|
|
625
668
|
|
|
626
669
|
|
|
627
670
|
def _require_user_provider(user_id: str, workspace_id: str, provider: str) -> dict[str, Any]:
|
|
@@ -736,11 +779,20 @@ def _overlay_turn_provider_env(
|
|
|
736
779
|
workspace_id: str,
|
|
737
780
|
provider: str,
|
|
738
781
|
run_id: str = "",
|
|
782
|
+
*,
|
|
783
|
+
wait_healthy: bool = True,
|
|
739
784
|
) -> str:
|
|
740
|
-
"""
|
|
785
|
+
"""Runtime broker credential into profile env; raw key stays API-only."""
|
|
741
786
|
run_id = str(run_id or "").strip() or str(uuid.uuid4())
|
|
742
787
|
# ponytail: yaml sync + gateway reload are async on the send path — never block TTFT (~40s).
|
|
743
|
-
return _apply_turn_credential_lease(
|
|
788
|
+
return _apply_turn_credential_lease(
|
|
789
|
+
profile,
|
|
790
|
+
user_id,
|
|
791
|
+
workspace_id,
|
|
792
|
+
provider,
|
|
793
|
+
run_id,
|
|
794
|
+
wait_healthy=wait_healthy,
|
|
795
|
+
)
|
|
744
796
|
|
|
745
797
|
|
|
746
798
|
def _try_overlay_turn_provider_env(
|
|
@@ -749,10 +801,19 @@ def _try_overlay_turn_provider_env(
|
|
|
749
801
|
workspace_id: str,
|
|
750
802
|
provider: str,
|
|
751
803
|
run_id: str = "",
|
|
804
|
+
*,
|
|
805
|
+
wait_healthy: bool = True,
|
|
752
806
|
) -> bool:
|
|
753
807
|
"""Best-effort LLM lease; returns False when the user has no key yet."""
|
|
754
808
|
try:
|
|
755
|
-
_overlay_turn_provider_env(
|
|
809
|
+
_overlay_turn_provider_env(
|
|
810
|
+
profile,
|
|
811
|
+
user_id,
|
|
812
|
+
workspace_id,
|
|
813
|
+
provider,
|
|
814
|
+
run_id,
|
|
815
|
+
wait_healthy=wait_healthy,
|
|
816
|
+
)
|
|
756
817
|
return True
|
|
757
818
|
except ValueError as exc:
|
|
758
819
|
if "no_llm_provider_for_user" in str(exc):
|
|
@@ -182,8 +182,13 @@ def validate_me_profile_updates(updates: dict[str, Any]) -> None:
|
|
|
182
182
|
|
|
183
183
|
def apply_me_profile_updates(user_id: str, body: dict[str, Any]) -> dict[str, Any]:
|
|
184
184
|
user_id = str(user_id or "").strip()
|
|
185
|
-
allowed = {"display_name", "avatar_url", "tagline", "bio"}
|
|
185
|
+
allowed = {"display_name", "avatar_url", "tagline", "bio", "theme"}
|
|
186
186
|
updates = {k: v for k, v in body.items() if k in allowed}
|
|
187
|
+
if "theme" in updates:
|
|
188
|
+
theme = str(updates.get("theme") or "").strip()
|
|
189
|
+
if not theme or len(theme) > 64 or any(char not in "abcdefghijklmnopqrstuvwxyz0123456789-" for char in theme):
|
|
190
|
+
raise ValueError("invalid_theme")
|
|
191
|
+
updates["theme"] = theme
|
|
187
192
|
validate_me_profile_updates(updates)
|
|
188
193
|
if "avatar_url" in updates:
|
|
189
194
|
updates["avatar_url"] = _srv()._normalize_user_avatar_url(str(updates.get("avatar_url") or ""))
|
|
@@ -192,6 +197,21 @@ def apply_me_profile_updates(user_id: str, body: dict[str, Any]) -> dict[str, An
|
|
|
192
197
|
platform_ids = parse_user_platform_ids(body.get("platform_ids"))
|
|
193
198
|
conn = _srv()._workframe_db()
|
|
194
199
|
try:
|
|
200
|
+
current_row = conn.execute(
|
|
201
|
+
"SELECT platform_ids FROM users WHERE id = ?", (user_id,),
|
|
202
|
+
).fetchone()
|
|
203
|
+
if current_row:
|
|
204
|
+
try:
|
|
205
|
+
current_ids = json.loads(current_row["platform_ids"] or "{}")
|
|
206
|
+
except (TypeError, json.JSONDecodeError):
|
|
207
|
+
current_ids = {}
|
|
208
|
+
if isinstance(current_ids, dict):
|
|
209
|
+
# LLM preferences share this legacy JSON column but are
|
|
210
|
+
# not public messaging identities. Preserve them when
|
|
211
|
+
# Linked Accounts saves discord/telegram/slack values.
|
|
212
|
+
for key in ("llm_primary", "llm_fallback_chain"):
|
|
213
|
+
if key in current_ids:
|
|
214
|
+
platform_ids[key] = current_ids[key]
|
|
195
215
|
now_ts = str(int(time.time()))
|
|
196
216
|
conn.execute(
|
|
197
217
|
"UPDATE users SET platform_ids = ?, updated_at = ? WHERE id = ?",
|
|
@@ -399,6 +419,7 @@ def session_profile_payload(user_id: str) -> dict[str, Any] | None:
|
|
|
399
419
|
"avatar_url": avatar_url,
|
|
400
420
|
"tagline": (profile or {}).get("tagline"),
|
|
401
421
|
"bio": (profile or {}).get("bio"),
|
|
422
|
+
"theme": (profile or {}).get("theme"),
|
|
402
423
|
"platform_ids": platform_ids,
|
|
403
424
|
},
|
|
404
425
|
"workspaces": workspaces,
|
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import base64
|
|
6
|
+
import io
|
|
6
7
|
import mimetypes
|
|
8
|
+
import os
|
|
7
9
|
import threading
|
|
8
10
|
import time
|
|
11
|
+
import zipfile
|
|
9
12
|
from pathlib import Path
|
|
10
13
|
from typing import Any
|
|
11
14
|
|
|
@@ -26,6 +29,8 @@ TREE_SKIP_NAMES = {
|
|
|
26
29
|
|
|
27
30
|
PROTECTED_WORKSPACE_FILE_NAMES = {"AGENTS.md", ".hermes.md"}
|
|
28
31
|
PROTECTED_PROFILE_CONFIG_FILE_NAMES = {"config.yaml", "profile.yaml"}
|
|
32
|
+
MAX_FILE_ACTION_COUNT = 500
|
|
33
|
+
MAX_ARCHIVE_BYTES = 100 * 1024 * 1024
|
|
29
34
|
|
|
30
35
|
_workspace_state_lock = threading.Lock()
|
|
31
36
|
_workspace_state_cache: dict[str, Any] = {
|
|
@@ -342,3 +347,122 @@ def file_raw(rel: str) -> tuple[bytes, str]:
|
|
|
342
347
|
raise ValueError("not found")
|
|
343
348
|
mime, _ = mimetypes.guess_type(str(fp))
|
|
344
349
|
return fp.read_bytes(), mime or "application/octet-stream"
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _selected_workspace_paths(paths: list[Any]) -> list[tuple[str, Path]]:
|
|
353
|
+
if not isinstance(paths, list) or not paths:
|
|
354
|
+
raise ValueError("select at least one file")
|
|
355
|
+
if len(paths) > MAX_FILE_ACTION_COUNT:
|
|
356
|
+
raise ValueError(f"select at most {MAX_FILE_ACTION_COUNT} files")
|
|
357
|
+
|
|
358
|
+
selected: list[tuple[str, Path]] = []
|
|
359
|
+
seen: set[str] = set()
|
|
360
|
+
for raw in paths:
|
|
361
|
+
rel = _normalized_workspace_rel(str(raw or "").strip())
|
|
362
|
+
if not rel:
|
|
363
|
+
continue
|
|
364
|
+
reason = workspace_protected_reason(rel)
|
|
365
|
+
if reason:
|
|
366
|
+
raise PermissionError(f"protected file: {reason}")
|
|
367
|
+
path = safe_workspace_path(rel)
|
|
368
|
+
if path is None or not path.exists():
|
|
369
|
+
raise ValueError(f"file not found: {rel}")
|
|
370
|
+
canonical_rel = _workspace_rel(path)
|
|
371
|
+
if canonical_rel in seen:
|
|
372
|
+
continue
|
|
373
|
+
selected.append((canonical_rel, path))
|
|
374
|
+
seen.add(canonical_rel)
|
|
375
|
+
|
|
376
|
+
if not selected:
|
|
377
|
+
raise ValueError("select at least one file")
|
|
378
|
+
return selected
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _selected_workspace_files(paths: list[Any]) -> list[tuple[str, Path]]:
|
|
382
|
+
selected = _selected_workspace_paths(paths)
|
|
383
|
+
for rel, path in selected:
|
|
384
|
+
if not path.is_file():
|
|
385
|
+
raise ValueError(f"not a file: {rel}")
|
|
386
|
+
return selected
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _archive_workspace_files(paths: list[Any]) -> list[tuple[str, Path]]:
|
|
390
|
+
selected = _selected_workspace_paths(paths)
|
|
391
|
+
files: dict[str, Path] = {}
|
|
392
|
+
|
|
393
|
+
def add_file(path: Path) -> None:
|
|
394
|
+
try:
|
|
395
|
+
rel = _workspace_rel(path)
|
|
396
|
+
except ValueError as exc:
|
|
397
|
+
raise ValueError("selected file is outside the workspace") from exc
|
|
398
|
+
if workspace_protected_reason(rel):
|
|
399
|
+
return
|
|
400
|
+
if any(part in TREE_SKIP_NAMES for part in Path(rel).parts):
|
|
401
|
+
return
|
|
402
|
+
files.setdefault(rel, path)
|
|
403
|
+
if len(files) > MAX_FILE_ACTION_COUNT:
|
|
404
|
+
raise ValueError(f"select at most {MAX_FILE_ACTION_COUNT} files")
|
|
405
|
+
|
|
406
|
+
for rel, path in selected:
|
|
407
|
+
if path.is_file():
|
|
408
|
+
add_file(path)
|
|
409
|
+
continue
|
|
410
|
+
if not path.is_dir():
|
|
411
|
+
raise ValueError(f"not a file or folder: {rel}")
|
|
412
|
+
def on_walk_error(exc: OSError) -> None:
|
|
413
|
+
raise ValueError(f"could not read folder: {rel}") from exc
|
|
414
|
+
|
|
415
|
+
for folder, dirnames, filenames in os.walk(path, followlinks=False, onerror=on_walk_error):
|
|
416
|
+
folder_path = Path(folder)
|
|
417
|
+
dirnames[:] = sorted(
|
|
418
|
+
(
|
|
419
|
+
name
|
|
420
|
+
for name in dirnames
|
|
421
|
+
if name not in TREE_SKIP_NAMES
|
|
422
|
+
and not workspace_protected_reason(_workspace_rel(folder_path / name))
|
|
423
|
+
),
|
|
424
|
+
key=str.lower,
|
|
425
|
+
)
|
|
426
|
+
for filename in sorted(filenames, key=str.lower):
|
|
427
|
+
add_file(folder_path / filename)
|
|
428
|
+
|
|
429
|
+
if not files:
|
|
430
|
+
raise ValueError("selected folders do not contain downloadable files")
|
|
431
|
+
return sorted(files.items(), key=lambda item: item[0].lower())
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def files_archive(paths: list[Any]) -> bytes:
|
|
435
|
+
selected = _archive_workspace_files(paths)
|
|
436
|
+
total_bytes = 0
|
|
437
|
+
for _, path in selected:
|
|
438
|
+
try:
|
|
439
|
+
total_bytes += path.stat().st_size
|
|
440
|
+
except OSError as exc:
|
|
441
|
+
raise ValueError("could not read selected file") from exc
|
|
442
|
+
if total_bytes > MAX_ARCHIVE_BYTES:
|
|
443
|
+
raise ValueError("selected files exceed the 100 MB download limit")
|
|
444
|
+
|
|
445
|
+
output = io.BytesIO()
|
|
446
|
+
try:
|
|
447
|
+
with zipfile.ZipFile(output, mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=6) as archive:
|
|
448
|
+
for rel, path in selected:
|
|
449
|
+
archive.write(path, arcname=rel)
|
|
450
|
+
except OSError as exc:
|
|
451
|
+
raise ValueError("could not archive selected files") from exc
|
|
452
|
+
return output.getvalue()
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def files_delete(paths: list[Any]) -> dict[str, Any]:
|
|
456
|
+
# Validate the complete batch before mutating anything so a stale or protected
|
|
457
|
+
# selection cannot produce a partial deletion.
|
|
458
|
+
selected = _selected_workspace_files(paths)
|
|
459
|
+
deleted: list[str] = []
|
|
460
|
+
for rel, path in selected:
|
|
461
|
+
try:
|
|
462
|
+
path.unlink()
|
|
463
|
+
except OSError as exc:
|
|
464
|
+
raise ValueError(f"could not delete file: {rel}") from exc
|
|
465
|
+
deleted.append(rel)
|
|
466
|
+
|
|
467
|
+
bump_workspace_state(selected[0][1])
|
|
468
|
+
return {"ok": True, "deleted": deleted, "count": len(deleted)}
|
package/workframe-api/zk_auth.py
CHANGED
|
@@ -226,6 +226,7 @@ def _zk_init_db(conn: sqlite3.Connection) -> None:
|
|
|
226
226
|
avatar_url TEXT,
|
|
227
227
|
tagline TEXT,
|
|
228
228
|
bio TEXT,
|
|
229
|
+
theme TEXT,
|
|
229
230
|
created_at TEXT NOT NULL,
|
|
230
231
|
updated_at TEXT NOT NULL
|
|
231
232
|
);
|
|
@@ -237,6 +238,10 @@ def _zk_init_db(conn: sqlite3.Connection) -> None:
|
|
|
237
238
|
CREATE INDEX IF NOT EXISTS idx_sessions_family ON sessions(token_family_id);
|
|
238
239
|
CREATE INDEX IF NOT EXISTS idx_sessions_refresh ON sessions(refresh_token_hash);
|
|
239
240
|
""")
|
|
241
|
+
profile_columns = {str(row[1]) for row in conn.execute("PRAGMA table_info(profiles)").fetchall()}
|
|
242
|
+
if "theme" not in profile_columns:
|
|
243
|
+
conn.execute("ALTER TABLE profiles ADD COLUMN theme TEXT")
|
|
244
|
+
conn.commit()
|
|
240
245
|
|
|
241
246
|
|
|
242
247
|
# ---------------------------------------------------------------------------
|
|
@@ -566,13 +571,14 @@ def get_profile(user_id: str) -> dict | None:
|
|
|
566
571
|
"avatar_url": row["avatar_url"],
|
|
567
572
|
"tagline": row["tagline"],
|
|
568
573
|
"bio": row["bio"],
|
|
574
|
+
"theme": row["theme"],
|
|
569
575
|
}
|
|
570
576
|
finally:
|
|
571
577
|
conn.close()
|
|
572
578
|
|
|
573
579
|
|
|
574
580
|
def update_profile(user_id: str, updates: dict) -> dict:
|
|
575
|
-
allowed = {"display_name", "avatar_url", "tagline", "bio"}
|
|
581
|
+
allowed = {"display_name", "avatar_url", "tagline", "bio", "theme"}
|
|
576
582
|
fields = {k: v for k, v in updates.items() if k in allowed and v is not None}
|
|
577
583
|
if not fields:
|
|
578
584
|
return get_profile(user_id)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{n as e,t}from"./path-BWPyau1x.js";import{a as n,c as r,d as i,f as a,i as o,l as s,m as c,n as l,o as u,p as d,r as f,u as p}from"./dist-
|
|
1
|
+
import{n as e,t}from"./path-BWPyau1x.js";import{a as n,c as r,d as i,f as a,i as o,l as s,m as c,n as l,o as u,p as d,r as f,u as p}from"./dist-BiIfSARy.js";function m(e){return e.innerRadius}function h(e){return e.outerRadius}function g(e){return e.startAngle}function _(e){return e.endAngle}function v(e){return e&&e.padAngle}function y(e,t,n,r,i,a,o,s){var c=n-e,l=r-t,u=o-i,d=s-a,f=d*c-u*l;if(!(f*f<1e-12))return f=(u*(t-a)-d*(e-i))/f,[e+f*c,t+f*l]}function b(e,t,n,r,i,a,o){var c=e-n,l=t-r,u=(o?a:-a)/d(c*c+l*l),f=u*l,p=-u*c,m=e+f,h=t+p,g=n+f,_=r+p,v=(m+g)/2,y=(h+_)/2,b=g-m,x=_-h,S=b*b+x*x,C=i-a,w=m*_-g*h,T=(x<0?-1:1)*d(s(0,C*C*S-w*w)),E=(w*x-b*T)/S,D=(-w*b-x*T)/S,O=(w*x+b*T)/S,k=(-w*b+x*T)/S,A=E-v,j=D-y,M=O-v,N=k-y;return A*A+j*j>M*M+N*N&&(E=O,D=k),{cx:E,cy:D,x01:-f,y01:-p,x11:E*(i/C-1),y11:D*(i/C-1)}}function x(){var s=m,x=h,S=e(0),C=null,w=g,T=_,E=v,D=null,O=t(k);function k(){var e,t,m=+s.apply(this,arguments),h=+x.apply(this,arguments),g=w.apply(this,arguments)-r,_=T.apply(this,arguments)-r,v=l(_-g),k=_>g;if(D||=e=O(),h<m&&(t=h,h=m,m=t),!(h>1e-12))D.moveTo(0,0);else if(v>c-1e-12)D.moveTo(h*u(g),h*a(g)),D.arc(0,0,h,g,_,!k),m>1e-12&&(D.moveTo(m*u(_),m*a(_)),D.arc(0,0,m,_,g,k));else{var A=g,j=_,M=g,N=_,P=v,F=v,I=E.apply(this,arguments)/2,L=I>1e-12&&(C?+C.apply(this,arguments):d(m*m+h*h)),R=p(l(h-m)/2,+S.apply(this,arguments)),z=R,B=R,V,H;if(L>1e-12){var U=o(L/m*a(I)),W=o(L/h*a(I));(P-=U*2)>1e-12?(U*=k?1:-1,M+=U,N-=U):(P=0,M=N=(g+_)/2),(F-=W*2)>1e-12?(W*=k?1:-1,A+=W,j-=W):(F=0,A=j=(g+_)/2)}var G=h*u(A),K=h*a(A),q=m*u(N),J=m*a(N);if(R>1e-12){var Y=h*u(j),X=h*a(j),Z=m*u(M),Q=m*a(M),$;if(v<i)if($=y(G,K,Z,Q,Y,X,q,J)){var ee=G-$[0],te=K-$[1],ne=Y-$[0],re=X-$[1],ie=1/a(f((ee*ne+te*re)/(d(ee*ee+te*te)*d(ne*ne+re*re)))/2),ae=d($[0]*$[0]+$[1]*$[1]);z=p(R,(m-ae)/(ie-1)),B=p(R,(h-ae)/(ie+1))}else z=B=0}F>1e-12?B>1e-12?(V=b(Z,Q,G,K,h,B,k),H=b(Y,X,q,J,h,B,k),D.moveTo(V.cx+V.x01,V.cy+V.y01),B<R?D.arc(V.cx,V.cy,B,n(V.y01,V.x01),n(H.y01,H.x01),!k):(D.arc(V.cx,V.cy,B,n(V.y01,V.x01),n(V.y11,V.x11),!k),D.arc(0,0,h,n(V.cy+V.y11,V.cx+V.x11),n(H.cy+H.y11,H.cx+H.x11),!k),D.arc(H.cx,H.cy,B,n(H.y11,H.x11),n(H.y01,H.x01),!k))):(D.moveTo(G,K),D.arc(0,0,h,A,j,!k)):D.moveTo(G,K),!(m>1e-12)||!(P>1e-12)?D.lineTo(q,J):z>1e-12?(V=b(q,J,Y,X,m,-z,k),H=b(G,K,Z,Q,m,-z,k),D.lineTo(V.cx+V.x01,V.cy+V.y01),z<R?D.arc(V.cx,V.cy,z,n(V.y01,V.x01),n(H.y01,H.x01),!k):(D.arc(V.cx,V.cy,z,n(V.y01,V.x01),n(V.y11,V.x11),!k),D.arc(0,0,m,n(V.cy+V.y11,V.cx+V.x11),n(H.cy+H.y11,H.cx+H.x11),k),D.arc(H.cx,H.cy,z,n(H.y11,H.x11),n(H.y01,H.x01),!k))):D.arc(0,0,m,N,M,k)}if(D.closePath(),e)return D=null,e+``||null}return k.centroid=function(){var e=(+s.apply(this,arguments)+ +x.apply(this,arguments))/2,t=(+w.apply(this,arguments)+ +T.apply(this,arguments))/2-i/2;return[u(t)*e,a(t)*e]},k.innerRadius=function(t){return arguments.length?(s=typeof t==`function`?t:e(+t),k):s},k.outerRadius=function(t){return arguments.length?(x=typeof t==`function`?t:e(+t),k):x},k.cornerRadius=function(t){return arguments.length?(S=typeof t==`function`?t:e(+t),k):S},k.padRadius=function(t){return arguments.length?(C=t==null?null:typeof t==`function`?t:e(+t),k):C},k.startAngle=function(t){return arguments.length?(w=typeof t==`function`?t:e(+t),k):w},k.endAngle=function(t){return arguments.length?(T=typeof t==`function`?t:e(+t),k):T},k.padAngle=function(t){return arguments.length?(E=typeof t==`function`?t:e(+t),k):E},k.context=function(e){return arguments.length?(D=e??null,k):D},k}export{x as t};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./chunk-NNHCCRGN-DlpIbxXb.js";import{x as e}from"./mermaid-parser.core-CGDFs4AZ.js";export{e as createArchitectureServices};
|