create-workframe 0.1.12 → 0.1.13
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 +2 -2
- package/bin/workframe.js +130 -2
- package/docs/security.md +2 -2
- package/package.json +1 -1
- package/scripts/bundle-workframe-ui.mjs +9 -0
- package/scripts/sync-canonical-to-package.mjs +1 -0
- package/scripts/verify-public-deploy.sh +65 -3
- package/workframe-api/action_proxy.py +23 -17
- package/workframe-api/activity_feed.py +798 -0
- package/workframe-api/api_meta.py +157 -0
- package/workframe-api/auth_gate.py +566 -0
- package/workframe-api/avatar_registry.py +343 -0
- package/workframe-api/broker_audit.py +164 -0
- package/workframe-api/cell_authority.py +231 -0
- package/workframe-api/chat_bind.py +319 -0
- package/workframe-api/chat_sessions.py +509 -0
- package/workframe-api/chat_stream.py +694 -0
- package/workframe-api/cleanup_dogfood_smoke.py +252 -0
- package/workframe-api/credential_broker.py +162 -0
- package/workframe-api/credential_resolve.py +202 -0
- package/workframe-api/credential_store.py +245 -0
- package/workframe-api/crew_registry.py +250 -0
- package/workframe-api/db_schema.py +755 -0
- package/workframe-api/docker_gateway.py +166 -0
- package/workframe-api/doctor_runtime.py +109 -0
- package/workframe-api/domain/__init__.py +47 -0
- package/workframe-api/domain/entities.py +252 -0
- package/workframe-api/domain/schema/workframe-domain.schema.json +278 -0
- package/workframe-api/egress_policy.py +42 -0
- package/workframe-api/handler_modules/__init__.py +17 -0
- package/workframe-api/handler_modules/handler_admin.py +542 -0
- package/workframe-api/handler_modules/handler_auth.py +512 -0
- package/workframe-api/handler_modules/handler_chat.py +641 -0
- package/workframe-api/handler_modules/handler_install.py +178 -0
- package/workframe-api/handler_modules/handler_provider.py +325 -0
- package/workframe-api/handler_modules/handler_workspace.py +1420 -0
- package/workframe-api/health_monitor.py +80 -0
- package/workframe-api/hermes_admin.py +756 -0
- package/workframe-api/hermes_profiles.py +1328 -0
- package/workframe-api/install_api.py +123 -0
- package/workframe-api/kanban_cron.py +473 -0
- package/workframe-api/lane_bindings.py +423 -0
- package/workframe-api/llm_proxy.py +17 -43
- package/workframe-api/mention_helpers.py +180 -0
- package/workframe-api/mention_invoke.py +431 -0
- package/workframe-api/model_surface.py +1139 -0
- package/workframe-api/oauth_pending.py +85 -0
- package/workframe-api/oauth_redirect.py +381 -0
- package/workframe-api/package.json +2 -2
- package/workframe-api/profile_api_lifecycle.py +66 -0
- package/workframe-api/profile_gateway.py +436 -0
- package/workframe-api/provider_bindings.py +673 -0
- package/workframe-api/provider_bootstrap.py +347 -0
- package/workframe-api/provider_catalog.py +180 -0
- package/workframe-api/rooms.py +1613 -0
- package/workframe-api/route_registry.py +331 -191
- package/workframe-api/run-typecheck.mjs +2 -1
- package/workframe-api/run_authority.py +287 -0
- package/workframe-api/run_ledger.py +470 -0
- package/workframe-api/run_surface_wiring.py +344 -0
- package/workframe-api/runtime_cohort.py +752 -0
- package/workframe-api/runtime_tokens.py +127 -0
- package/workframe-api/server.py +1453 -19513
- package/workframe-api/snapshot_feed.py +49 -0
- package/workframe-api/stack_config.py +33 -1
- package/workframe-api/supervisor_client.py +154 -0
- package/workframe-api/test_api_meta_build_stamp.py +34 -0
- package/workframe-api/test_cell_authority.py +79 -0
- package/workframe-api/test_chat_bind.py +48 -0
- package/workframe-api/test_credential_lease_matrix.py +171 -0
- package/workframe-api/test_credential_resolve.py +51 -0
- package/workframe-api/test_credential_store.py +27 -0
- package/workframe-api/test_dogfood_flows.py +346 -0
- package/workframe-api/test_domain_entities.py +152 -0
- package/workframe-api/test_exception_hygiene.py +12 -0
- package/workframe-api/test_hermes_admin.py +72 -0
- package/workframe-api/test_install_wizard_resume.py +78 -0
- package/workframe-api/test_local_bootstrap.py +67 -0
- package/workframe-api/test_mention_helpers.py +35 -0
- package/workframe-api/test_mention_invoke.py +26 -0
- package/workframe-api/test_model_surface_consistency.py +1 -0
- package/workframe-api/test_oauth_pending.py +41 -0
- package/workframe-api/test_provider_bindings.py +52 -0
- package/workframe-api/test_provider_catalog.py +28 -0
- package/workframe-api/test_route_registry.py +171 -35
- package/workframe-api/test_run_authority.py +112 -0
- package/workframe-api/test_run_ledger.py +157 -0
- package/workframe-api/test_run_surface_wiring.py +130 -0
- package/workframe-api/test_runtime_cohort.py +85 -0
- package/workframe-api/test_secure_mode_docker_boundary.py +39 -0
- package/workframe-api/test_server_reexports.py +99 -0
- package/workframe-api/test_stack_config_install_smtp.py +7 -0
- package/workframe-api/turn_credentials.py +28 -13
- package/workframe-api/turn_overlay.py +715 -0
- package/workframe-api/updates.py +16 -1
- package/workframe-api/user_prefs.py +387 -0
- package/workframe-api/wf032_extract_remaining_handlers.py +417 -0
- package/workframe-api/workspace_bootstrap.py +536 -0
- package/workframe-api/workspace_files.py +344 -0
- package/workframe-api/workspace_messaging.py +206 -0
- package/workframe-api/zk_auth.py +3 -2
- package/workframe-supervisor/server.py +10 -1
- package/workframe-supervisor/test_supervisor_negative.py +75 -0
- package/workframe-ui/public/assets/{arc-B0OFRGmJ.js → arc-aUUffclm.js} +1 -1
- package/workframe-ui/public/assets/architecture-7EHR7CIX-BSUP4S8J.js +1 -0
- package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-DeBNltHS.js → architectureDiagram-3BPJPVTR-DU6oaqIb.js} +1 -1
- package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-BDhCHu7I.js → blockDiagram-GPEHLZMM-D2p8BU6-.js} +1 -1
- package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-olcDYvtI.js → c4Diagram-AAUBKEIU-ouwyPmTJ.js} +1 -1
- package/workframe-ui/public/assets/channel-DOunZZ0X.js +1 -0
- package/workframe-ui/public/assets/{chunk-2J33WTMH-D0obCBn_.js → chunk-2J33WTMH-DsE7U5dj.js} +1 -1
- package/workframe-ui/public/assets/{chunk-3OPIFGDE-DX93f-ZZ.js → chunk-3OPIFGDE-DM0kkZ9h.js} +1 -1
- package/workframe-ui/public/assets/{chunk-4BX2VUAB-BX9B5wUs.js → chunk-4BX2VUAB-CQ8mAyXp.js} +1 -1
- package/workframe-ui/public/assets/{chunk-55IACEB6-C4DGc6RO.js → chunk-55IACEB6-DN1BDtbH.js} +1 -1
- package/workframe-ui/public/assets/{chunk-5ZQYHXKU-Crlja9Lf.js → chunk-5ZQYHXKU-BzK2KqOt.js} +1 -1
- package/workframe-ui/public/assets/{chunk-727SXJPM-JYSm3szI.js → chunk-727SXJPM-C3AxtOi9.js} +1 -1
- package/workframe-ui/public/assets/{chunk-AQP2D5EJ-_KslxVEl.js → chunk-AQP2D5EJ-4mVwEFuD.js} +1 -1
- package/workframe-ui/public/assets/{chunk-BSJP7CBP-CpTO-jU8.js → chunk-BSJP7CBP-23kN2q0A.js} +1 -1
- package/workframe-ui/public/assets/{chunk-CSCIHK7Q-JGUkCmbI.js → chunk-CSCIHK7Q-CPBKVW-L.js} +2 -2
- package/workframe-ui/public/assets/{chunk-FMBD7UC4-BscBwGaC.js → chunk-FMBD7UC4-CKqcYq7K.js} +1 -1
- package/workframe-ui/public/assets/{chunk-KSCS5N6A-5ZTZ0bpX.js → chunk-KSCS5N6A-JRQnhxQE.js} +1 -1
- package/workframe-ui/public/assets/{chunk-L5ZTLDWV-DxvitYbW.js → chunk-L5ZTLDWV-BLZrdIwa.js} +1 -1
- package/workframe-ui/public/assets/chunk-LZXEDZCA-DisG0XJT.js +2 -0
- package/workframe-ui/public/assets/{chunk-ND2GUHAM-Cayl0iV9.js → chunk-ND2GUHAM-ClAOxArS.js} +1 -1
- package/workframe-ui/public/assets/{chunk-NZK2D7GU-_7dyBR5G.js → chunk-NZK2D7GU-XUE1aNkm.js} +1 -1
- package/workframe-ui/public/assets/{chunk-O5CBEL6O--xTpD0yi.js → chunk-O5CBEL6O-DDN-Ifon.js} +1 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-EVQ0VMd5.js +1 -0
- package/workframe-ui/public/assets/chunk-WU5MYG2G-k_-ZsLOS.js +1 -0
- package/workframe-ui/public/assets/{chunk-XPW4576I-CwxJQaeK.js → chunk-XPW4576I-B4_iYZ6r.js} +1 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-W9WVQsby.js +1 -0
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-W9WVQsby.js +1 -0
- package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-CCspENYv.js → cose-bilkent-S5V4N54A-Cy1HhS7j.js} +1 -1
- package/workframe-ui/public/assets/{dagre-BM42HDAG-Dv8V6R60.js → dagre-BM42HDAG-Bjal81Ie.js} +1 -1
- package/workframe-ui/public/assets/{diagram-2AECGRRQ-Da48hFPT.js → diagram-2AECGRRQ-v5Gdvzp_.js} +1 -1
- package/workframe-ui/public/assets/{diagram-5GNKFQAL-dTihc7-3.js → diagram-5GNKFQAL-DuyRdfmY.js} +1 -1
- package/workframe-ui/public/assets/{diagram-KO2AKTUF-D_Jqu699.js → diagram-KO2AKTUF-WwWCGbIW.js} +1 -1
- package/workframe-ui/public/assets/{diagram-LMA3HP47-Bt8PtEaZ.js → diagram-LMA3HP47-DTYwZALT.js} +1 -1
- package/workframe-ui/public/assets/{diagram-OG6HWLK6-BtmPjlh0.js → diagram-OG6HWLK6-B46kBIqE.js} +1 -1
- package/workframe-ui/public/assets/{dist-Cz2turIT.js → dist-mQWmB3z6.js} +1 -1
- package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-BxMGCflX.js → erDiagram-TEJ5UH35-__40xO-y.js} +1 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-Cm1uRTxU.js +1 -0
- package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-ivyroIZt.js → flowDiagram-I6XJVG4X-Cwj6dVUz.js} +1 -1
- package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-B9llwShx.js → ganttDiagram-6RSMTGT7-D_IOSwAS.js} +1 -1
- package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-BFQHk4bw.js → gitGraph-WXDBUCRP-C1vGecee.js} +1 -1
- package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-ursegLbc.js → gitGraphDiagram-PVQCEYII-DMTxFJxn.js} +1 -1
- package/workframe-ui/public/assets/index-BdiM4-lI.js +130 -0
- package/workframe-ui/public/assets/index-Fnad47vV.css +1 -0
- package/workframe-ui/public/assets/{info-J43DQDTF-DQpifAsB.js → info-J43DQDTF-CwjmyPmD.js} +1 -1
- package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-C16XP2Q7.js → infoDiagram-5YYISTIA-BGzh7ZL2.js} +1 -1
- package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-CiJoz7rP.js → ishikawaDiagram-YF4QCWOH-BLXdnR_5.js} +1 -1
- package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-CVwEvvUL.js → journeyDiagram-JHISSGLW-BK3behMe.js} +1 -1
- package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-CWQ6hX5i.js → kanban-definition-UN3LZRKU-Bx172cCl.js} +1 -1
- package/workframe-ui/public/assets/{line-CgQsmU35.js → line-BTCEHb7l.js} +1 -1
- package/workframe-ui/public/assets/{linear-Bxbc77dL.js → linear-CkbydpnK.js} +1 -1
- package/workframe-ui/public/assets/{mermaid-parser.core-C_jMeF0a.js → mermaid-parser.core-DvrtArpk.js} +2 -2
- package/workframe-ui/public/assets/{mermaid.core-4RKV9MZP.js → mermaid.core-yj_1x_qj.js} +3 -3
- package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-DZ7y7Sz0.js → mindmap-definition-RKZ34NQL-BXbMltQ0.js} +1 -1
- package/workframe-ui/public/assets/{packet-YPE3B663-B9h2I7Vq.js → packet-YPE3B663-9pVCBQ7S.js} +1 -1
- package/workframe-ui/public/assets/{pie-LRSECV5Y-B2mP5uHm.js → pie-LRSECV5Y-CdqoWesP.js} +1 -1
- package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-DyYKyKiP.js → pieDiagram-4H26LBE5-kGs-VfXd.js} +1 -1
- package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-CL_6nej-.js → quadrantDiagram-W4KKPZXB-D9D8gQw5.js} +1 -1
- package/workframe-ui/public/assets/{radar-GUYGQ44K-CNKNDQ7S.js → radar-GUYGQ44K-d4zMZpQS.js} +1 -1
- package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-eWU_mhFa.js → requirementDiagram-4Y6WPE33-BI7EQ9zc.js} +1 -1
- package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-DrjcXvEQ.js → sankeyDiagram-5OEKKPKP-Bcz19jQB.js} +1 -1
- package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-BHWR1xTJ.js → sequenceDiagram-3UESZ5HK-iQiCEfYp.js} +1 -1
- package/workframe-ui/public/assets/{src-DfJB8kV1.js → src-D6mvlP96.js} +1 -1
- package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BGJbSd3I.js → stateDiagram-AJRCARHV-CxaTXs02.js} +1 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CXikj9xi.js +1 -0
- package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-DkwoCjiZ.js → timeline-definition-PNZ67QCA-Bej-EAnf.js} +1 -1
- package/workframe-ui/public/assets/{treeView-BLDUP644-GxfhiqoW.js → treeView-BLDUP644-C7PnnzwX.js} +1 -1
- package/workframe-ui/public/assets/{treemap-LRROVOQU-DxrOsars.js → treemap-LRROVOQU-CqISLmkO.js} +1 -1
- package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-BsWhpUfr.js → vennDiagram-CIIHVFJN-eJMTTEW-.js} +1 -1
- package/workframe-ui/public/assets/{wardley-L42UT6IY-CrFZWMHS.js → wardley-L42UT6IY-D4sMroBF.js} +1 -1
- package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DV8Xpf5I.js → wardleyDiagram-YWT4CUSO-DC3DCUs7.js} +1 -1
- package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-DqMqnwdZ.js → xychartDiagram-2RQKCTM6-BeMaM6Aq.js} +1 -1
- package/workframe-ui/public/index.html +7 -5
- package/workframe-ui/public/workframe-build.json +5 -0
- package/workframe-ui/public/assets/architecture-7EHR7CIX-Dwwi9yA0.js +0 -1
- package/workframe-ui/public/assets/channel-fNHbDpV4.js +0 -1
- package/workframe-ui/public/assets/chunk-LZXEDZCA-DMfdMUwz.js +0 -2
- package/workframe-ui/public/assets/chunk-QZHKN3VN-ewTgEQK8.js +0 -1
- package/workframe-ui/public/assets/chunk-WU5MYG2G-D4Tg-A0l.js +0 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-CFAgBpEl.js +0 -1
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-CFAgBpEl.js +0 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-BIJTP5k-.js +0 -1
- package/workframe-ui/public/assets/index-8CuZDEIG.css +0 -1
- package/workframe-ui/public/assets/index-xS9lFekI.js +0 -129
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CMLuNyeC.js +0 -1
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
"""WF-032 extract: Hermes admin/profile API and slash command catalog."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sqlite3
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import run_surface_wiring
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _srv():
|
|
12
|
+
import server as srv
|
|
13
|
+
|
|
14
|
+
return srv
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Cache installed skill names for /api/hermes/commands/exec slash routing.
|
|
18
|
+
_skill_name_cache: dict[str, Any] = {"names": None, "expires_at": 0.0}
|
|
19
|
+
_SKILL_CACHE_TTL_SECONDS = 60
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _known_skill_names() -> set[str]:
|
|
23
|
+
now = time.time()
|
|
24
|
+
cached = _skill_name_cache.get("names")
|
|
25
|
+
if cached is not None and _skill_name_cache.get("expires_at", 0) > now:
|
|
26
|
+
return cached
|
|
27
|
+
try:
|
|
28
|
+
names = {str(s.get("name", "")).strip() for s in _srv().hermes_skills() if s.get("name")}
|
|
29
|
+
except Exception:
|
|
30
|
+
names = cached or set()
|
|
31
|
+
_skill_name_cache["names"] = names
|
|
32
|
+
_skill_name_cache["expires_at"] = now + _SKILL_CACHE_TTL_SECONDS
|
|
33
|
+
return names
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _is_known_skill(name: str) -> bool:
|
|
37
|
+
if not name:
|
|
38
|
+
return False
|
|
39
|
+
return name in _known_skill_names()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Static mirror of Hermes' COMMAND_REGISTRY (hermes_cli/commands.py).
|
|
43
|
+
# The BFF does not import hermes_agent; it serves a curated snapshot so the
|
|
44
|
+
# Workframe UI has a fast, local source of truth for the slash palette.
|
|
45
|
+
# Drift is acceptable: any new command lands here in the same release that
|
|
46
|
+
# updates upstream Hermes. See workframe-api dead-code note for context.
|
|
47
|
+
HERMES_COMMANDS: list[dict[str, Any]] = [
|
|
48
|
+
# Session
|
|
49
|
+
{"name": "/new", "aliases": ["/reset"], "category": "Session",
|
|
50
|
+
"description": "Fresh session", "args_hint": "",
|
|
51
|
+
"dispatch": "client:startNewSession"},
|
|
52
|
+
{"name": "/clear", "aliases": [], "category": "Session",
|
|
53
|
+
"description": "Clear screen + new session", "args_hint": "",
|
|
54
|
+
"dispatch": "client:clearMessages"},
|
|
55
|
+
{"name": "/retry", "aliases": [], "category": "Session",
|
|
56
|
+
"description": "Resend last message", "args_hint": "",
|
|
57
|
+
"dispatch": "gateway"},
|
|
58
|
+
{"name": "/undo", "aliases": [], "category": "Session",
|
|
59
|
+
"description": "Remove last exchange", "args_hint": "",
|
|
60
|
+
"dispatch": "gateway"},
|
|
61
|
+
{"name": "/compress", "aliases": [], "category": "Session",
|
|
62
|
+
"description": "Manually compress context", "args_hint": "",
|
|
63
|
+
"dispatch": "gateway"},
|
|
64
|
+
{"name": "/stop", "aliases": [], "category": "Session",
|
|
65
|
+
"description": "Kill background processes", "args_hint": "",
|
|
66
|
+
"dispatch": "gateway"},
|
|
67
|
+
{"name": "/rollback", "aliases": [], "category": "Session",
|
|
68
|
+
"description": "Restore filesystem checkpoint", "args_hint": "[N]",
|
|
69
|
+
"dispatch": "gateway"},
|
|
70
|
+
{"name": "/snapshot", "aliases": [], "category": "Session",
|
|
71
|
+
"description": "Create or restore state snapshots", "args_hint": "[sub]",
|
|
72
|
+
"dispatch": "gateway"},
|
|
73
|
+
{"name": "/background", "aliases": [], "category": "Session",
|
|
74
|
+
"description": "Run prompt in background", "args_hint": "<prompt>",
|
|
75
|
+
"dispatch": "gateway"},
|
|
76
|
+
{"name": "/queue", "aliases": [], "category": "Session",
|
|
77
|
+
"description": "Queue for next turn", "args_hint": "<prompt>",
|
|
78
|
+
"dispatch": "gateway"},
|
|
79
|
+
{"name": "/steer", "aliases": [], "category": "Session",
|
|
80
|
+
"description": "Inject after the next tool call", "args_hint": "<prompt>",
|
|
81
|
+
"dispatch": "gateway"},
|
|
82
|
+
{"name": "/agents", "aliases": ["/tasks"], "category": "Session",
|
|
83
|
+
"description": "Show active agents and running tasks", "args_hint": "",
|
|
84
|
+
"dispatch": "gateway"},
|
|
85
|
+
{"name": "/resume", "aliases": [], "category": "Session",
|
|
86
|
+
"description": "Resume a named session", "args_hint": "[name]",
|
|
87
|
+
"dispatch": "gateway"},
|
|
88
|
+
{"name": "/goal", "aliases": [], "category": "Session",
|
|
89
|
+
"description": "Set a standing goal", "args_hint": "[text|sub]",
|
|
90
|
+
"dispatch": "gateway"},
|
|
91
|
+
{"name": "/redraw", "aliases": [], "category": "Session",
|
|
92
|
+
"description": "Force a full UI repaint", "args_hint": "",
|
|
93
|
+
"dispatch": "client:noOp"},
|
|
94
|
+
|
|
95
|
+
# Configuration
|
|
96
|
+
{"name": "/model", "aliases": [], "category": "Configuration",
|
|
97
|
+
"description": "Show or change model", "args_hint": "[name]",
|
|
98
|
+
"dispatch": "client:openModelSwitcher"},
|
|
99
|
+
{"name": "/personality", "aliases": [], "category": "Configuration",
|
|
100
|
+
"description": "Set personality", "args_hint": "[name]",
|
|
101
|
+
"dispatch": "client:openPersonality"},
|
|
102
|
+
{"name": "/reasoning", "aliases": [], "category": "Configuration",
|
|
103
|
+
"description": "Set reasoning", "args_hint": "[level]",
|
|
104
|
+
"dispatch": "gateway"},
|
|
105
|
+
{"name": "/verbose", "aliases": [], "category": "Configuration",
|
|
106
|
+
"description": "Cycle verbose level", "args_hint": "",
|
|
107
|
+
"dispatch": "gateway"},
|
|
108
|
+
{"name": "/voice", "aliases": [], "category": "Configuration",
|
|
109
|
+
"description": "Voice mode", "args_hint": "[on|off|tts]",
|
|
110
|
+
"dispatch": "gateway"},
|
|
111
|
+
{"name": "/yolo", "aliases": [], "category": "Configuration",
|
|
112
|
+
"description": "Toggle approval bypass", "args_hint": "",
|
|
113
|
+
"dispatch": "gateway"},
|
|
114
|
+
{"name": "/busy", "aliases": [], "category": "Configuration",
|
|
115
|
+
"description": "What Enter does while working", "args_hint": "[sub]",
|
|
116
|
+
"dispatch": "gateway"},
|
|
117
|
+
{"name": "/indicator", "aliases": [], "category": "Configuration",
|
|
118
|
+
"description": "Busy indicator style", "args_hint": "[style]",
|
|
119
|
+
"dispatch": "gateway"},
|
|
120
|
+
{"name": "/footer", "aliases": [], "category": "Configuration",
|
|
121
|
+
"description": "Gateway runtime footer on replies", "args_hint": "[on|off]",
|
|
122
|
+
"dispatch": "gateway"},
|
|
123
|
+
{"name": "/skin", "aliases": [], "category": "Configuration",
|
|
124
|
+
"description": "Change theme (CLI)", "args_hint": "[name]",
|
|
125
|
+
"dispatch": "gateway"},
|
|
126
|
+
{"name": "/statusbar", "aliases": [], "category": "Configuration",
|
|
127
|
+
"description": "Toggle status bar (CLI)", "args_hint": "",
|
|
128
|
+
"dispatch": "gateway"},
|
|
129
|
+
|
|
130
|
+
# Tools & Skills
|
|
131
|
+
{"name": "/tools", "aliases": [], "category": "Tools & Skills",
|
|
132
|
+
"description": "Manage tools", "args_hint": "",
|
|
133
|
+
"dispatch": "gateway"},
|
|
134
|
+
{"name": "/toolsets", "aliases": [], "category": "Tools & Skills",
|
|
135
|
+
"description": "List toolsets", "args_hint": "",
|
|
136
|
+
"dispatch": "gateway"},
|
|
137
|
+
{"name": "/skills", "aliases": [], "category": "Tools & Skills",
|
|
138
|
+
"description": "Search/install skills", "args_hint": "",
|
|
139
|
+
"dispatch": "client:openSkills"},
|
|
140
|
+
{"name": "/skill", "aliases": [], "category": "Tools & Skills",
|
|
141
|
+
"description": "Load a skill into session", "args_hint": "<name>",
|
|
142
|
+
"dispatch": "gateway"},
|
|
143
|
+
{"name": "/reload-skills", "aliases": [], "category": "Tools & Skills",
|
|
144
|
+
"description": "Re-scan ~/.hermes/skills/", "args_hint": "",
|
|
145
|
+
"dispatch": "gateway"},
|
|
146
|
+
{"name": "/reload", "aliases": [], "category": "Tools & Skills",
|
|
147
|
+
"description": "Reload .env into running session", "args_hint": "",
|
|
148
|
+
"dispatch": "gateway"},
|
|
149
|
+
{"name": "/reload-mcp", "aliases": [], "category": "Tools & Skills",
|
|
150
|
+
"description": "Reload MCP servers", "args_hint": "",
|
|
151
|
+
"dispatch": "gateway"},
|
|
152
|
+
{"name": "/cron", "aliases": [], "category": "Tools & Skills",
|
|
153
|
+
"description": "Manage cron jobs", "args_hint": "",
|
|
154
|
+
"dispatch": "gateway"},
|
|
155
|
+
{"name": "/curator", "aliases": [], "category": "Tools & Skills",
|
|
156
|
+
"description": "Skill lifecycle", "args_hint": "[sub]",
|
|
157
|
+
"dispatch": "gateway"},
|
|
158
|
+
{"name": "/kanban", "aliases": [], "category": "Tools & Skills",
|
|
159
|
+
"description": "Multi-profile board", "args_hint": "[sub]",
|
|
160
|
+
"dispatch": "gateway"},
|
|
161
|
+
{"name": "/plugins", "aliases": [], "category": "Tools & Skills",
|
|
162
|
+
"description": "List plugins", "args_hint": "",
|
|
163
|
+
"dispatch": "gateway"},
|
|
164
|
+
|
|
165
|
+
# Utility
|
|
166
|
+
{"name": "/branch", "aliases": ["/fork"], "category": "Utility",
|
|
167
|
+
"description": "Branch the current session", "args_hint": "",
|
|
168
|
+
"dispatch": "gateway"},
|
|
169
|
+
{"name": "/fast", "aliases": [], "category": "Utility",
|
|
170
|
+
"description": "Toggle priority/fast processing", "args_hint": "",
|
|
171
|
+
"dispatch": "gateway"},
|
|
172
|
+
{"name": "/browser", "aliases": [], "category": "Utility",
|
|
173
|
+
"description": "Open CDP browser connection", "args_hint": "",
|
|
174
|
+
"dispatch": "gateway"},
|
|
175
|
+
{"name": "/history", "aliases": [], "category": "Utility",
|
|
176
|
+
"description": "Show conversation history", "args_hint": "",
|
|
177
|
+
"dispatch": "gateway"},
|
|
178
|
+
{"name": "/save", "aliases": [], "category": "Utility",
|
|
179
|
+
"description": "Save conversation to file", "args_hint": "",
|
|
180
|
+
"dispatch": "gateway"},
|
|
181
|
+
{"name": "/copy", "aliases": [], "category": "Utility",
|
|
182
|
+
"description": "Copy last assistant reply", "args_hint": "[N]",
|
|
183
|
+
"dispatch": "gateway"},
|
|
184
|
+
{"name": "/paste", "aliases": [], "category": "Utility",
|
|
185
|
+
"description": "Attach clipboard image", "args_hint": "",
|
|
186
|
+
"dispatch": "gateway"},
|
|
187
|
+
{"name": "/image", "aliases": [], "category": "Utility",
|
|
188
|
+
"description": "Attach local image file", "args_hint": "",
|
|
189
|
+
"dispatch": "gateway"},
|
|
190
|
+
|
|
191
|
+
# Info
|
|
192
|
+
{"name": "/help", "aliases": [], "category": "Info",
|
|
193
|
+
"description": "Show commands", "args_hint": "",
|
|
194
|
+
"dispatch": "client:openHelp"},
|
|
195
|
+
{"name": "/status", "aliases": [], "category": "Info",
|
|
196
|
+
"description": "Session info", "args_hint": "",
|
|
197
|
+
"dispatch": "client:openStatus"},
|
|
198
|
+
{"name": "/usage", "aliases": [], "category": "Info",
|
|
199
|
+
"description": "Token usage for the active session", "args_hint": "",
|
|
200
|
+
"dispatch": "client:openUsage"},
|
|
201
|
+
{"name": "/insights", "aliases": [], "category": "Info",
|
|
202
|
+
"description": "Usage analytics", "args_hint": "[days]",
|
|
203
|
+
"dispatch": "client:openInsights"},
|
|
204
|
+
{"name": "/gquota", "aliases": [], "category": "Info",
|
|
205
|
+
"description": "Google Gemini quota", "args_hint": "",
|
|
206
|
+
"dispatch": "client:openGquota"},
|
|
207
|
+
{"name": "/commands", "aliases": [], "category": "Info",
|
|
208
|
+
"description": "Browse all commands", "args_hint": "[page]",
|
|
209
|
+
"dispatch": "client:openHelp"},
|
|
210
|
+
{"name": "/profile", "aliases": [], "category": "Info",
|
|
211
|
+
"description": "Active profile info", "args_hint": "",
|
|
212
|
+
"dispatch": "client:openProfile"},
|
|
213
|
+
{"name": "/debug", "aliases": [], "category": "Info",
|
|
214
|
+
"description": "Upload debug report", "args_hint": "",
|
|
215
|
+
"dispatch": "client:openDebug"},
|
|
216
|
+
|
|
217
|
+
# Exit
|
|
218
|
+
{"name": "/quit", "aliases": ["/exit", "/q"], "category": "Exit",
|
|
219
|
+
"description": "Exit CLI", "args_hint": "",
|
|
220
|
+
"dispatch": "client:noOp"},
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _resolve_command(token: str) -> dict[str, Any] | None:
|
|
225
|
+
"""Resolve a slash token (with or without leading /) to a wired
|
|
226
|
+
catalog entry. Wip commands are internal-only — they live in
|
|
227
|
+
`HERMES_COMMANDS` for the team to track progress, but `_resolve_command`
|
|
228
|
+
treats them as not-found so the user can never accidentally invoke
|
|
229
|
+
something that isn't wired end-to-end.
|
|
230
|
+
"""
|
|
231
|
+
if not token:
|
|
232
|
+
return None
|
|
233
|
+
needle = token if token.startswith("/") else f"/{token}"
|
|
234
|
+
for entry in HERMES_COMMANDS:
|
|
235
|
+
if entry.get("wip", False):
|
|
236
|
+
continue
|
|
237
|
+
if entry["name"] == needle or needle in entry.get("aliases", []):
|
|
238
|
+
return entry
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def hermes_usage() -> dict[str, Any]:
|
|
245
|
+
"""Token usage for the latest session of the active profile."""
|
|
246
|
+
profile = _srv()._primary_profile()
|
|
247
|
+
if not profile:
|
|
248
|
+
return {"ok": False, "error": "no active profile"}
|
|
249
|
+
|
|
250
|
+
session_id = _srv()._latest_session_id(profile)
|
|
251
|
+
if not session_id:
|
|
252
|
+
return {"ok": True, "profile": profile, "session": None}
|
|
253
|
+
|
|
254
|
+
db = _srv()._profile_dir(profile) / "state.db"
|
|
255
|
+
conn = _srv()._ro_sqlite_live(db)
|
|
256
|
+
if not conn:
|
|
257
|
+
return {"ok": False, "error": "state.db not reachable"}
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
row = conn.execute(
|
|
261
|
+
"SELECT title, model, message_count, started_at, ended_at, "
|
|
262
|
+
"tool_call_count, input_tokens, output_tokens "
|
|
263
|
+
"FROM sessions WHERE id = ?",
|
|
264
|
+
(session_id,),
|
|
265
|
+
).fetchone()
|
|
266
|
+
|
|
267
|
+
if not row:
|
|
268
|
+
return {"ok": True, "profile": profile, "session": None}
|
|
269
|
+
|
|
270
|
+
input_tokens = int(row["input_tokens"] or 0)
|
|
271
|
+
output_tokens = int(row["output_tokens"] or 0)
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
"ok": True,
|
|
275
|
+
"profile": profile,
|
|
276
|
+
"session": {
|
|
277
|
+
"id": str(session_id)[:12] + "…",
|
|
278
|
+
"title": row["title"] or "(untitled)",
|
|
279
|
+
"model": row["model"] or "—",
|
|
280
|
+
"message_count": int(row["message_count"] or 0),
|
|
281
|
+
"tool_call_count": int(row["tool_call_count"] or 0),
|
|
282
|
+
"input_tokens": input_tokens,
|
|
283
|
+
"output_tokens": output_tokens,
|
|
284
|
+
"total_tokens": input_tokens + output_tokens,
|
|
285
|
+
"started_at": _srv()._iso_from_unix(row["started_at"]),
|
|
286
|
+
"ended_at": _srv()._iso_from_unix(row["ended_at"]) if row["ended_at"] else None,
|
|
287
|
+
},
|
|
288
|
+
}
|
|
289
|
+
except sqlite3.Error as exc:
|
|
290
|
+
return {"ok": False, "error": str(exc)}
|
|
291
|
+
finally:
|
|
292
|
+
conn.close()
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def hermes_gateway_exec(
|
|
296
|
+
line: str,
|
|
297
|
+
profile: str = "",
|
|
298
|
+
user_id: str = "",
|
|
299
|
+
workspace_id: str = "",
|
|
300
|
+
) -> dict[str, Any]:
|
|
301
|
+
"""Execute a slash command through the Hermes CLI.
|
|
302
|
+
|
|
303
|
+
Runs `hermes -p {profile} {cmd}` via Docker exec in the gateway
|
|
304
|
+
container. This bypasses the streaming chat endpoint — slash commands
|
|
305
|
+
are processed synchronously by the CLI's command handler and the
|
|
306
|
+
output is returned immediately.
|
|
307
|
+
"""
|
|
308
|
+
prof = _srv().resolve_validated_profile(profile) if profile else _srv()._primary_profile()
|
|
309
|
+
if not prof:
|
|
310
|
+
return {"ok": False, "error": "no active profile"}
|
|
311
|
+
|
|
312
|
+
if not _srv()._user_may_access_runtime_profile(user_id, prof, workspace_id):
|
|
313
|
+
return {"ok": False, "error": "profile_access_denied", "profile": prof}
|
|
314
|
+
|
|
315
|
+
text = (line or "").strip()
|
|
316
|
+
if not text:
|
|
317
|
+
return {"ok": False, "error": "empty command"}
|
|
318
|
+
|
|
319
|
+
# Parse the command: first token is the command name (with or without
|
|
320
|
+
# leading /), rest is args.
|
|
321
|
+
parts = text.split(None, 1)
|
|
322
|
+
cmd_token = parts[0].lstrip("/")
|
|
323
|
+
cmd_args = parts[1] if len(parts) > 1 else ""
|
|
324
|
+
|
|
325
|
+
# Build the CLI command.
|
|
326
|
+
cli_args = [cmd_token]
|
|
327
|
+
if cmd_args:
|
|
328
|
+
cli_args.extend(cmd_args.split())
|
|
329
|
+
|
|
330
|
+
if _srv()._exec_targets_runtime_profile_secrets(cli_args, prof) or _srv()._exec_targets_runtime_profile_secrets(
|
|
331
|
+
["hermes", "-p", prof, *cli_args], prof,
|
|
332
|
+
):
|
|
333
|
+
return {"ok": False, "error": "blocked_credential_path", "profile": prof}
|
|
334
|
+
|
|
335
|
+
run_id: str | None = None
|
|
336
|
+
try:
|
|
337
|
+
run_id, decision = run_surface_wiring.begin_slash_run(
|
|
338
|
+
cmd_token=cmd_token,
|
|
339
|
+
line=text,
|
|
340
|
+
profile_slug=prof,
|
|
341
|
+
user_id=user_id,
|
|
342
|
+
workspace_id=workspace_id,
|
|
343
|
+
)
|
|
344
|
+
if decision is not None and not decision.allowed:
|
|
345
|
+
return {
|
|
346
|
+
"ok": False,
|
|
347
|
+
"error": str(decision.deny_reason or "run_denied"),
|
|
348
|
+
"run_id": run_id,
|
|
349
|
+
"profile": prof,
|
|
350
|
+
"command": cmd_token,
|
|
351
|
+
}
|
|
352
|
+
except Exception as exc: # noqa: BLE001
|
|
353
|
+
_srv()._log_handler_error("hermes_gateway_exec.run_ledger", exc)
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
rc, output = _srv()._gateway_exec(prof, cli_args)
|
|
357
|
+
out = output.strip() if output else ""
|
|
358
|
+
if run_id:
|
|
359
|
+
try:
|
|
360
|
+
run_surface_wiring.finish_surface_run(run_id, ok=rc == 0, detail=out)
|
|
361
|
+
except Exception as exc: # noqa: BLE001
|
|
362
|
+
_srv()._log_handler_error("hermes_gateway_exec.finish_run", exc)
|
|
363
|
+
return {
|
|
364
|
+
"ok": rc == 0,
|
|
365
|
+
"profile": prof,
|
|
366
|
+
"command": cmd_token,
|
|
367
|
+
"rc": rc,
|
|
368
|
+
"output": out,
|
|
369
|
+
"run_id": run_id,
|
|
370
|
+
}
|
|
371
|
+
except Exception as exc:
|
|
372
|
+
if run_id:
|
|
373
|
+
try:
|
|
374
|
+
run_surface_wiring.finish_surface_run(run_id, ok=False, detail=str(exc))
|
|
375
|
+
except Exception as finish_exc: # noqa: BLE001
|
|
376
|
+
_srv()._log_handler_error("hermes_gateway_exec.finish_run", finish_exc)
|
|
377
|
+
return {"ok": False, "error": str(exc), "run_id": run_id}
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def hermes_profile() -> dict[str, Any]:
|
|
383
|
+
"""Active profile info for the /profile dialog."""
|
|
384
|
+
profile = _srv()._primary_profile()
|
|
385
|
+
if not profile:
|
|
386
|
+
return {"ok": False, "error": "no active profile"}
|
|
387
|
+
|
|
388
|
+
soul_path = _srv()._profile_soul_path(profile)
|
|
389
|
+
description = ""
|
|
390
|
+
if soul_path.is_file():
|
|
391
|
+
try:
|
|
392
|
+
text = soul_path.read_text(encoding="utf-8", errors="replace")
|
|
393
|
+
for raw in text.splitlines():
|
|
394
|
+
line = raw.strip()
|
|
395
|
+
if not line or line.startswith("#"):
|
|
396
|
+
continue
|
|
397
|
+
description = line[:200]
|
|
398
|
+
break
|
|
399
|
+
except Exception:
|
|
400
|
+
description = ""
|
|
401
|
+
|
|
402
|
+
try:
|
|
403
|
+
state = _srv().gateway_data(profile)
|
|
404
|
+
gateway_running = bool(state.get("ok")) and str(state.get("state") or "").lower() == "running"
|
|
405
|
+
except Exception:
|
|
406
|
+
gateway_running = False
|
|
407
|
+
|
|
408
|
+
session_id = _srv()._latest_session_id(profile)
|
|
409
|
+
session_info = None
|
|
410
|
+
if session_id:
|
|
411
|
+
try:
|
|
412
|
+
info = _srv()._session_info(profile, session_id)
|
|
413
|
+
session_info = {
|
|
414
|
+
"id": str(session_id)[:12] + "…",
|
|
415
|
+
"title": info.get("session_title", ""),
|
|
416
|
+
"message_count": info.get("message_count", 0),
|
|
417
|
+
"model": info.get("model", ""),
|
|
418
|
+
}
|
|
419
|
+
except Exception:
|
|
420
|
+
pass
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
"ok": True,
|
|
424
|
+
"profile": profile,
|
|
425
|
+
"description": description,
|
|
426
|
+
"gateway_running": gateway_running,
|
|
427
|
+
"session": session_info,
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _agent_db_row(template_slug: str, workspace_id: str = "") -> dict[str, Any]:
|
|
432
|
+
template_slug = _srv().safe_profile_slug(str(template_slug or "").strip())
|
|
433
|
+
if not template_slug:
|
|
434
|
+
return {}
|
|
435
|
+
conn = _srv()._workframe_db()
|
|
436
|
+
try:
|
|
437
|
+
if workspace_id:
|
|
438
|
+
row = _srv()._lookup_agent_profile(conn, workspace_id, template_slug)
|
|
439
|
+
else:
|
|
440
|
+
row = conn.execute(
|
|
441
|
+
"""
|
|
442
|
+
SELECT display_name, tagline, role, avatar_url
|
|
443
|
+
FROM agent_profiles
|
|
444
|
+
WHERE slug = ? AND deleted_at IS NULL
|
|
445
|
+
ORDER BY is_native DESC, updated_at DESC
|
|
446
|
+
LIMIT 1
|
|
447
|
+
""",
|
|
448
|
+
(template_slug,),
|
|
449
|
+
).fetchone()
|
|
450
|
+
if not row:
|
|
451
|
+
return {}
|
|
452
|
+
return {
|
|
453
|
+
"display_name": str(row["display_name"] or "").strip(),
|
|
454
|
+
"tagline": str(row["tagline"] or "").strip() if "tagline" in row.keys() else "",
|
|
455
|
+
"role": str(row["role"] or "").strip() if "role" in row.keys() else "",
|
|
456
|
+
"avatar_url": str(row["avatar_url"] or "").strip() if "avatar_url" in row.keys() else "",
|
|
457
|
+
}
|
|
458
|
+
finally:
|
|
459
|
+
conn.close()
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def hermes_profile_detail(profile: str, workspace_id: str = "") -> dict[str, Any]:
|
|
463
|
+
"""Profile metadata + SOUL + model surface for a specific Hermes profile."""
|
|
464
|
+
prof = _srv().resolve_validated_profile(profile)
|
|
465
|
+
reg = _srv()._agent_registry_row(prof)
|
|
466
|
+
db_row = _agent_db_row(prof, workspace_id)
|
|
467
|
+
soul_text = _srv()._profile_soul_text(prof)
|
|
468
|
+
block = _srv()._read_model_block(prof)
|
|
469
|
+
try:
|
|
470
|
+
state = _srv().gateway_data(prof)
|
|
471
|
+
gateway_running = bool(state.get("ok")) and str(state.get("state") or "").lower() == "running"
|
|
472
|
+
gateway_state = str(state.get("state") or "unknown")
|
|
473
|
+
except Exception:
|
|
474
|
+
gateway_running = False
|
|
475
|
+
gateway_state = "unknown"
|
|
476
|
+
row: dict[str, Any] = {
|
|
477
|
+
"ok": True,
|
|
478
|
+
"profile": prof,
|
|
479
|
+
"display_name": str(
|
|
480
|
+
db_row.get("display_name")
|
|
481
|
+
or reg.get("display_name")
|
|
482
|
+
or _srv()._profile_display_name(prof, workspace_id)
|
|
483
|
+
),
|
|
484
|
+
"role": str(db_row.get("role") or reg.get("role") or _srv()._profile_role(prof)),
|
|
485
|
+
"tagline": str(db_row.get("tagline") or reg.get("tagline") or ""),
|
|
486
|
+
"description": str(reg.get("description") or ""),
|
|
487
|
+
"soul": soul_text,
|
|
488
|
+
"soul_exists": bool(soul_text),
|
|
489
|
+
"gateway_running": gateway_running,
|
|
490
|
+
"gateway_state": gateway_state,
|
|
491
|
+
"model": block.get("default", ""),
|
|
492
|
+
"provider": block.get("provider", ""),
|
|
493
|
+
"is_native": _srv()._is_native_profile(prof),
|
|
494
|
+
}
|
|
495
|
+
avatar_url = str(db_row.get("avatar_url") or reg.get("avatar_url") or "").strip()
|
|
496
|
+
if avatar_url:
|
|
497
|
+
row["avatar_url"] = avatar_url
|
|
498
|
+
if reg.get("avatar_id"):
|
|
499
|
+
row["avatar_id"] = str(reg["avatar_id"])
|
|
500
|
+
_srv()._resolve_avatar_fields(row)
|
|
501
|
+
return row
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def hermes_profile_update(profile: str, body: dict[str, Any]) -> dict[str, Any]:
|
|
505
|
+
"""Update Workframe agent registry metadata for a Hermes profile."""
|
|
506
|
+
prof = _srv().resolve_validated_profile(profile)
|
|
507
|
+
allowed = {"display_name", "role", "tagline", "description", "avatar_url", "avatar_id"}
|
|
508
|
+
patch: dict[str, Any] = {}
|
|
509
|
+
for key in allowed:
|
|
510
|
+
if key not in body:
|
|
511
|
+
continue
|
|
512
|
+
value = body[key]
|
|
513
|
+
patch[key] = str(value).strip() if value is not None else ""
|
|
514
|
+
if "avatar_url" in patch:
|
|
515
|
+
_srv()._validate_me_profile_updates({"avatar_url": patch["avatar_url"]})
|
|
516
|
+
if not patch:
|
|
517
|
+
return {"ok": False, "error": "no_allowed_fields"}
|
|
518
|
+
_srv()._upsert_agent_registry_row(prof, patch)
|
|
519
|
+
_srv()._sync_agent_profile_db(prof, patch)
|
|
520
|
+
return {"ok": True, "profile": prof, **patch}
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def profile_soul_get(profile: str) -> dict[str, Any]:
|
|
524
|
+
prof = _srv().resolve_validated_profile(profile)
|
|
525
|
+
text = _srv()._profile_soul_text(prof)
|
|
526
|
+
path = _srv()._profile_soul_path(prof)
|
|
527
|
+
return {
|
|
528
|
+
"ok": True,
|
|
529
|
+
"profile": prof,
|
|
530
|
+
"soul": text,
|
|
531
|
+
"path": str(path),
|
|
532
|
+
"exists": path.is_file(),
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def profile_soul_set(profile: str, soul: str) -> dict[str, Any]:
|
|
537
|
+
prof = _srv().resolve_validated_profile(profile)
|
|
538
|
+
if _srv()._is_native_profile(prof):
|
|
539
|
+
path = _srv()._profile_soul_path(prof)
|
|
540
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
541
|
+
normalized = soul if not soul or soul.endswith("\n") else f"{soul}\n"
|
|
542
|
+
path.write_text(normalized, encoding="utf-8")
|
|
543
|
+
return {"ok": True, "profile": prof, "bytes": len(normalized.encode("utf-8")), "target": "soul_file"}
|
|
544
|
+
lookup = _srv()._runtime_template_slug(prof) if _srv()._is_runtime_profile_slug(prof) else prof
|
|
545
|
+
_srv()._apply_profile_identity(lookup, user_soul=soul)
|
|
546
|
+
return {"ok": True, "profile": prof, "bytes": len(soul.encode("utf-8")), "target": "user_soul_overlay"}
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def hermes_debug() -> dict[str, Any]:
|
|
550
|
+
"""Debug info for the /debug dialog."""
|
|
551
|
+
profile = _srv()._primary_profile()
|
|
552
|
+
if not profile:
|
|
553
|
+
return {"ok": False, "error": "no active profile"}
|
|
554
|
+
|
|
555
|
+
import platform as plat
|
|
556
|
+
import sys
|
|
557
|
+
|
|
558
|
+
db = _srv()._profile_dir(profile) / "state.db"
|
|
559
|
+
conn = _srv()._ro_sqlite_live(db)
|
|
560
|
+
session_count = 0
|
|
561
|
+
message_count = 0
|
|
562
|
+
if conn:
|
|
563
|
+
try:
|
|
564
|
+
row = conn.execute("SELECT COUNT(*) as cnt FROM sessions").fetchone()
|
|
565
|
+
session_count = row["cnt"] if row else 0
|
|
566
|
+
row = conn.execute("SELECT COUNT(*) as cnt FROM messages").fetchone()
|
|
567
|
+
message_count = row["cnt"] if row else 0
|
|
568
|
+
except Exception:
|
|
569
|
+
pass
|
|
570
|
+
finally:
|
|
571
|
+
conn.close()
|
|
572
|
+
|
|
573
|
+
return {
|
|
574
|
+
"ok": True,
|
|
575
|
+
"profile": profile,
|
|
576
|
+
"python_version": sys.version,
|
|
577
|
+
"platform": plat.platform(),
|
|
578
|
+
"session_count": session_count,
|
|
579
|
+
"message_count": message_count,
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def hermes_insights() -> dict[str, Any]:
|
|
584
|
+
"""Usage analytics for the /insights dialog."""
|
|
585
|
+
profile = _srv()._primary_profile()
|
|
586
|
+
if not profile:
|
|
587
|
+
return {"ok": False, "error": "no active profile"}
|
|
588
|
+
|
|
589
|
+
db = _srv()._profile_dir(profile) / "state.db"
|
|
590
|
+
conn = _srv()._ro_sqlite_live(db)
|
|
591
|
+
if not conn:
|
|
592
|
+
return {"ok": False, "error": "state.db not reachable"}
|
|
593
|
+
|
|
594
|
+
try:
|
|
595
|
+
# Total tokens per day (last 7 days)
|
|
596
|
+
rows = conn.execute(
|
|
597
|
+
"SELECT DATE(started_at, 'unixepoch') as day, "
|
|
598
|
+
"SUM(input_tokens) as input_tok, SUM(output_tokens) as output_tok, "
|
|
599
|
+
"COUNT(*) as sessions "
|
|
600
|
+
"FROM sessions "
|
|
601
|
+
"WHERE started_at > strftime('%s', 'now', '-7 days') "
|
|
602
|
+
"GROUP BY day ORDER BY day DESC"
|
|
603
|
+
).fetchall()
|
|
604
|
+
|
|
605
|
+
daily = []
|
|
606
|
+
for r in rows:
|
|
607
|
+
daily.append({
|
|
608
|
+
"day": r["day"] or "unknown",
|
|
609
|
+
"input_tokens": int(r["input_tok"] or 0),
|
|
610
|
+
"output_tokens": int(r["output_tok"] or 0),
|
|
611
|
+
"sessions": int(r["sessions"] or 0),
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
# Model usage breakdown
|
|
615
|
+
model_rows = conn.execute(
|
|
616
|
+
"SELECT model, COUNT(*) as cnt, SUM(input_tokens) as input_tok "
|
|
617
|
+
"FROM sessions GROUP BY model ORDER BY cnt DESC LIMIT 10"
|
|
618
|
+
).fetchall()
|
|
619
|
+
|
|
620
|
+
models = []
|
|
621
|
+
for r in model_rows:
|
|
622
|
+
models.append({
|
|
623
|
+
"model": r["model"] or "unknown",
|
|
624
|
+
"sessions": int(r["cnt"] or 0),
|
|
625
|
+
"input_tokens": int(r["input_tok"] or 0),
|
|
626
|
+
})
|
|
627
|
+
|
|
628
|
+
return {"ok": True, "profile": profile, "daily": daily, "models": models}
|
|
629
|
+
except sqlite3.Error as exc:
|
|
630
|
+
return {"ok": False, "error": str(exc)}
|
|
631
|
+
finally:
|
|
632
|
+
conn.close()
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def hermes_gquota() -> dict[str, Any]:
|
|
636
|
+
"""Google Gemini quota info — stub until Google API is configured."""
|
|
637
|
+
profile = _srv()._primary_profile()
|
|
638
|
+
if not profile:
|
|
639
|
+
return {"ok": False, "error": "no active profile"}
|
|
640
|
+
return {
|
|
641
|
+
"ok": True,
|
|
642
|
+
"profile": profile,
|
|
643
|
+
"configured": False,
|
|
644
|
+
"message": "Google Gemini quota: not configured. Set GOOGLE_API_KEY in .env to enable.",
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def hermes_commands_catalog() -> dict[str, Any]:
|
|
649
|
+
"""Categorized slash command catalog for the Workframe composer palette.
|
|
650
|
+
|
|
651
|
+
`wip` commands are an internal tracker — they exist in
|
|
652
|
+
`HERMES_COMMANDS` so the team can see what's still to wire, but
|
|
653
|
+
they aren't returned here. The UI never sees a half-implemented
|
|
654
|
+
command; either it's wired end-to-end or it's invisible.
|
|
655
|
+
"""
|
|
656
|
+
visible = [e for e in HERMES_COMMANDS if not e.get("wip", False)]
|
|
657
|
+
grouped: dict[str, list[dict[str, Any]]] = {}
|
|
658
|
+
for entry in visible:
|
|
659
|
+
grouped.setdefault(entry["category"], []).append(
|
|
660
|
+
{
|
|
661
|
+
"name": entry["name"],
|
|
662
|
+
"aliases": entry.get("aliases", []),
|
|
663
|
+
"description": entry["description"],
|
|
664
|
+
"args_hint": entry.get("args_hint", ""),
|
|
665
|
+
"dispatch": entry["dispatch"],
|
|
666
|
+
}
|
|
667
|
+
)
|
|
668
|
+
categories = [
|
|
669
|
+
{"name": name, "commands": [
|
|
670
|
+
(cmd["name"], cmd["description"]) for cmd in items
|
|
671
|
+
]}
|
|
672
|
+
for name, items in grouped.items()
|
|
673
|
+
]
|
|
674
|
+
return {
|
|
675
|
+
"ok": True,
|
|
676
|
+
"categories": categories,
|
|
677
|
+
"entries": [
|
|
678
|
+
{**entry, "wired": True}
|
|
679
|
+
for entry in visible
|
|
680
|
+
],
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def hermes_commands_exec(line: str) -> dict[str, Any]:
|
|
685
|
+
"""Resolve a slash command line and return a dispatch decision for the UI.
|
|
686
|
+
|
|
687
|
+
The BFF does not run client-side UI effects; it classifies the command
|
|
688
|
+
and returns a hint. The UI executes the hint. For commands that need to
|
|
689
|
+
round-trip the gateway (`/auth`, `/config`, etc.) the BFF returns a
|
|
690
|
+
`gateway` dispatch and the UI calls a separate gateway proxy endpoint.
|
|
691
|
+
|
|
692
|
+
Skills (`/skillname` where the skill is installed in Hermes) aren't in
|
|
693
|
+
the static catalog. The BFF falls back to a skill-name lookup so the
|
|
694
|
+
UI gets a clean "this is dispatchable" decision rather than "unknown
|
|
695
|
+
command" for every installed skill.
|
|
696
|
+
"""
|
|
697
|
+
raw = (line or "").strip()
|
|
698
|
+
if not raw.startswith("/"):
|
|
699
|
+
return {"ok": False, "error": "not a slash command", "line": raw}
|
|
700
|
+
parts = raw.split(maxsplit=1)
|
|
701
|
+
token = parts[0]
|
|
702
|
+
rest = parts[1] if len(parts) > 1 else ""
|
|
703
|
+
entry = _resolve_command(token)
|
|
704
|
+
if entry is None:
|
|
705
|
+
# Fallback: is this an installed skill? Skills route through
|
|
706
|
+
# the gateway once the gateway hookup lands (Slice 2). For now
|
|
707
|
+
# the UI gets a `gateway` dispatch so the user sees the same
|
|
708
|
+
# "real command, gateway side not wired yet" hint as the other
|
|
709
|
+
# gateway-class commands.
|
|
710
|
+
skill_name = token.lstrip("/")
|
|
711
|
+
if _is_known_skill(skill_name):
|
|
712
|
+
return {
|
|
713
|
+
"ok": True,
|
|
714
|
+
"dispatched": "gateway",
|
|
715
|
+
"command": token,
|
|
716
|
+
"args": rest,
|
|
717
|
+
"message": f"Skill: {skill_name} — gateway dispatch coming in Slice 2",
|
|
718
|
+
}
|
|
719
|
+
return {
|
|
720
|
+
"ok": False,
|
|
721
|
+
"error": f"unknown command: {token}",
|
|
722
|
+
"suggestion": "/help",
|
|
723
|
+
}
|
|
724
|
+
dispatch = entry["dispatch"]
|
|
725
|
+
if dispatch == "noop":
|
|
726
|
+
return {
|
|
727
|
+
"ok": True,
|
|
728
|
+
"dispatched": "noop",
|
|
729
|
+
"command": entry["name"],
|
|
730
|
+
"args": rest,
|
|
731
|
+
"message": "Not yet wired in Workframe — see M2 in the Workframe roadmap.",
|
|
732
|
+
}
|
|
733
|
+
if dispatch.startswith("client:"):
|
|
734
|
+
return {
|
|
735
|
+
"ok": True,
|
|
736
|
+
"dispatched": "client",
|
|
737
|
+
"command": entry["name"],
|
|
738
|
+
"args": rest,
|
|
739
|
+
"handler": dispatch.split(":", 1)[1],
|
|
740
|
+
}
|
|
741
|
+
if dispatch.startswith("bff:"):
|
|
742
|
+
return {
|
|
743
|
+
"ok": True,
|
|
744
|
+
"dispatched": "bff",
|
|
745
|
+
"command": entry["name"],
|
|
746
|
+
"args": rest,
|
|
747
|
+
"handler": dispatch.split(":", 1)[1],
|
|
748
|
+
}
|
|
749
|
+
return {
|
|
750
|
+
"ok": True,
|
|
751
|
+
"dispatched": "gateway",
|
|
752
|
+
"command": entry["name"],
|
|
753
|
+
"args": rest,
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
|