Compare commits

...
Author SHA1 Message Date
MervinPraisonandClaude Opus 4.8 112253013a fix: add AIUI_STARTUP_TIMEOUT to bound startup hooks (fixes #278)
Startup hooks ran with an unbounded await while shutdown hooks were
already protected by AIUI_SHUTDOWN_TIMEOUT. A single hung startup hook
blocked the Starlette lifespan from reaching yield, so /health/live never
responded and the dashboard never loaded.

Mirror the shutdown pattern: wrap startup hooks in asyncio.wait_for with
AIUI_STARTUP_TIMEOUT (default 60s). On timeout, log the error and mark
startup completed so the server still reaches lifespan yield. Shutdown
behaviour is unchanged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-14 09:47:16 +00:00
Mervin Praison 597280a6c6 Merge pull request #265 from Dhivya-Bharathy/feat/meeting-agent-phase-2-recall
feat(meeting-agent): Phase 2 Recall.ai live bots (Part A)
2026-09-08 16:39:01 +01:00
MervinPraisonandClaude Opus 4.8 fe954cbd4a fix(meeting-agent): drop leftover hardcoded Recall workspace UUID from part_b test
The personal contributor workspace UUID (33a035c7-...) was still hardcoded
in the test_meeting_agent_part_b.py env fixture, even though the same value
was cleaned to a neutral placeholder everywhere else (config default,
.env.example, smoke_recall.py, and test_meeting_agent_recall.py). Aligned it
with the sibling test's 'test-workspace' placeholder. No assertion depends on
the value, so behavior is unchanged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-08 09:43:30 +00:00
MervinPraisonandClaude Opus 4.8 ab9ef2544d fix(meeting-agent): point Part B tests at renamed meeting_integrations dir
The Part A branch renamed integrations/recall -> meeting_integrations/recall
to avoid a tests/unit/integrations module-name collision, but the Part B test
loaders (test_meeting_agent_part_b.py) still resolved the old integrations/recall
paths, raising FileNotFoundError on CI (py3.10/3.11/3.12). Repoint the 9 _load()
paths at meeting_integrations/recall to match the source layout and the already-
updated test_meeting_agent_recall.py.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-08 09:39:03 +00:00
MervinPraisonandCursor e15b3c0718 merge: resolve conflicts with main (Part B + voice-agent merged)
Reconcile meeting-agent Phase A with main after #266/#267: keep
meeting_integrations/ package rename, merge Part B live-transcript and
Calendar V2 routes, and preserve Part A fixes (atomic webhook idempotency,
background task refs, workspace ID placeholders, py3.10 compiler compat).
Update part_b tests to meeting_integrations paths.

Co-authored-by: Cursor <[email protected]>
2026-09-08 10:34:47 +01:00
MervinPraison 6ecb80e977 Release v0.3.143 2026-09-08 09:05:53 +00:00
Mervin Praison 7ebd97d359 Merge pull request #266 from Dhivya-Bharathy/feat/meeting-agent-phase-2-part-b
feat(meeting-agent): Phase 2 Part B — live transcript + Calendar V2
2026-09-08 10:04:46 +01:00
Mervin Praison bd6b67f38b Merge pull request #267 from Dhivya-Bharathy/feat/voice-agent-phase-1
feat(voice-agent): Phase 1 telephony webhooks, tools, and call dashboard
2026-09-08 10:03:17 +01:00
MervinPraisonandClaude Opus 4.8 442e2d6dfe fix(meeting-agent): retain strong refs to webhook background tasks
asyncio keeps only weak references to tasks created via create_task, so a
fire-and-forget webhook dispatch could be garbage-collected mid-execution and
silently drop the event (a transcript line persist or the post-transcript
pipeline). Route both Recall and calendar webhook dispatches through a helper
that holds a strong reference in a module-level set until the task completes.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-08 08:17:46 +00:00
MervinPraisonandClaude Opus 4.8 3608d011c6 fix(voice-agent): build stable webhook idempotency keys so status updates aren't dropped
webhook_event_key derived the dedup key from a 200-char JSON prefix for
status-update/conversation-update/end-of-call-report. When a large static
field (e.g. `call`) sorts before the discriminating field, successive
events collapse to the same prefix and later ones are dropped as
duplicates, freezing the dashboard status on the first value (e.g. stuck
on 'ringing'). Derive keys from the fields that actually distinguish each
event, matching the existing transcript-event pattern. No new deps,
params, or exports.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-07 23:45:23 +00:00
MervinPraisonandClaude Opus 4.8 d615e2e0a3 fix(tests): warm up liveness endpoint before timing to fix flaky 3.10 CI
test_health_live_is_fast timed the first request through the Starlette
TestClient, which pays one-time route/client initialization cost and
measured 0.15s on a slow Python 3.10 runner (threshold 0.1s). Add a
warmup request so the assertion measures steady-state latency, matching
the endpoint's own <500ms target. No production code change.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-07 19:26:06 +00:00
MervinPraison 352023db8d fix(meeting-agent): serialize live-transcript metadata read-modify-write
Recall webhooks are dispatched one-thread-each via asyncio.to_thread, so
rapid-fire transcript.data events ran _upsert_live_transcript concurrently.
Each did an unsynchronized read (get_meeting) -> append -> UPDATE of the
persisted meeting transcript, so two concurrent captions could read the same
base and one write would clobber the other, dropping transcript lines.

Add a shared reentrant lock (pipeline._metadata_lock) around every metadata
read-modify-write in merge_meeting_metadata, and hold it across the read in
_upsert_live_transcript so the whole read-append-write is atomic. In-memory
fan-out (live.py) already had a lock; this closes the same gap for the
persisted transcript. Example-only, no package params/exports/deps added.
2026-09-07 19:23:01 +00:00
MervinPraisonandClaude Opus 4.8 bfb1eeee06 fix(voice-agent): preserve live call status on transcript/conversation events
upsert_call overwrote status with excluded.status and defaulted to
'unknown', so transcript and conversation-update events (which pass no
status) reset an in-progress call to 'unknown' in the dashboard. Preserve
the prior status when the caller omits one, matching the existing
COALESCE handling for summary/customer_number. Adds a regression test.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-07 19:21:10 +00:00
MervinPraisonandClaude Opus 4.8 12b42e88be fix(meeting-agent): make Recall webhook idempotency check atomic
Replace the non-atomic SELECT-then-INSERT in mark_event_processed with an
atomic INSERT OR IGNORE + rowcount check. Under concurrent duplicate webhook
deliveries (Svix retries/fan-out) both callers could pass the SELECT and the
second INSERT would raise an unhandled IntegrityError on the primary key,
silently dropping that event's processing after a 200 was returned.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-07 15:29:26 +00:00
MervinPraisonandClaude Opus 4.8 5e80f11535 fix(meeting-agent): drop hardcoded personal Recall workspace ID from example
The RECALL_WORKSPACE_ID default was a real contributor workspace UUID
committed across the example (config default, README, .env.example, smoke
script, test). It is non-functional — the Recall API key already scopes
requests to a workspace and the value is only echoed back by the
config-status endpoint. Default it to an empty string and replace the
committed UUID with neutral placeholders.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-07 15:25:58 +00:00
MervinPraisonandClaude Opus 4.8 84ae1fd0ac fix(meeting-agent): retain webhook background task refs to prevent GC
asyncio only holds weak references to tasks created via create_task, so the
fire-and-forget Recall webhook processing task could be garbage-collected
mid-execution, silently dropping transcript/status updates. Keep strong refs
in a module-level set and discard on completion.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-07 15:21:44 +00:00
MervinPraisonandClaude Opus 4.8 b59af41d88 fix(compiler): use timezone.utc for Python 3.10 compatibility
datetime.UTC was added in Python 3.11, so importing it broke the
python (3.10) CI job with ImportError in _format_lastmod. Switch to
timezone.utc which is equivalent and available on all supported
Python versions (3.10+).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-07 15:17:41 +00:00
MervinPraisonandClaude Opus 4.8 fb999aba3c fix(seo): use timezone.utc for Python 3.10 compatibility in sitemap lastmod
datetime.UTC was added in Python 3.11, but requires-python is >=3.10, so
Compiler._format_lastmod raised ImportError on 3.10, failing the SEO build
tests and blocking CI. timezone.utc is equivalent and available since 3.2.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-07 15:15:55 +00:00
MervinPraisonandClaude Opus 4.8 8e5c4d40e2 fix(meeting-agent): resolve CI failures on Phase 2 Recall PR
- Rename example package integrations/ -> meeting_integrations/ to avoid a
  top-level module-name collision with tests/unit/integrations (imported as
  top-level 'integrations' because tests/unit has no __init__.py). The
  collision made 'meeting_integrations.recall' unresolvable once the test
  session imported the tests integrations package first, causing 8 errors +
  3 failures. Updated all absolute imports and test path references.
- Guard praisonai_tools-dependent tests with pytest.importorskip: the meeting
  tools live in the external PraisonAI-Tools package and are not installed in
  this repo's CI, so those tests skip cleanly instead of erroring.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-07 15:15:01 +00:00
MervinPraisonandClaude Opus 4.8 c7dc8a751a fix(tests): skip meeting-agent tests when praisonai_tools is unavailable
The 6 new meeting-agent tests invoke the external PraisonAI-Tools
'meeting_tools' module, which is not a PraisonAIUI CI dependency, causing
ModuleNotFoundError failures on Python 3.10/3.11/3.12. Guard them with
pytest.importorskip so CI skips them cleanly instead of failing, matching
how the example treats its other optional heavy deps.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-07 15:11:50 +00:00
MervinPraisonandClaude Opus 4.8 b5b640a2b7 fix(compiler): use timezone.utc for lastmod so Python 3.10 CI passes
datetime.UTC was added in Python 3.11, but the project supports >=3.10
(CI matrix + classifiers), so _format_lastmod raised ImportError on 3.10:
  cannot import name 'UTC' from 'datetime'
Switch to timezone.utc (available since 3.2) — identical behaviour, all
supported versions. Fixes the failing python (3.10) job on PR #267.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-07 10:21:44 +00:00
MervinPraisonandClaude Opus 4.8 a188039480 fix(voice-agent): hold strong ref to background webhook task to prevent GC drop
asyncio only keeps a weak reference to tasks created via create_task, so a
fire-and-forget webhook-processing task could be garbage-collected mid-flight
and silently drop a status/transcript event. Track tasks in a set and discard
on completion (standard asyncio pattern).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-07 10:16:51 +00:00
MervinPraisonandClaude Opus 4.8 c71ad7d6da fix(voice-agent): isolate example package import in unit tests to fix CI
tests/unit/integrations ships a top-level 'integrations' package, which
shadowed the voice-agent example's integrations.voice package when the
voice test ran after the integration tests (ModuleNotFoundError: No module
named 'integrations.voice'), failing python 3.10/3.11/3.12 CI.

Import the example's modules inside a context manager that gives the
example dir priority on sys.path and restores the original 'integrations'
package afterwards. Example code is unchanged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-07 10:13:46 +00:00
MervinPraisonandClaude Opus 4.8 ecf2118bb2 fix(voice-agent): remove committed run logs, timing-safe webhook verify, lazy httpx
- Delete accidentally committed .app.err / .tunnel.log.err (leaked local
  paths, Cloudflare tunnel URLs, connector IDs); ignore *.err
- verify.py: use hmac.compare_digest for constant-time secret comparison
- client.py: lazy-import httpx (optional dep) per AGENTS.md lazy-import rule
- Fix import ordering (ruff I001) in app.py / voice_routes.py

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-07 10:06:59 +00:00
Mervin Praison 43722c4901 Merge pull request #271 from MervinPraison/chore/hub-v1.1.6
chore(ci): hub v1.1.6 — fix Claude Assistant hub checkout
2026-09-07 11:02:21 +01:00
MervinPraisonandCursor 798e893153 chore(ci): bump hub to v1.1.6 (claude-code-action checkout fix)
Co-authored-by: Cursor <[email protected]>
2026-09-07 11:02:16 +01:00
Mervin Praison 696455db7b Merge pull request #270 from MervinPraison/chore/hub-v1.1.5
fix(ci): hub v1.1.5 Claude Assistant caller permissions
2026-09-07 11:00:42 +01:00
MervinPraisonandCursor 9735e1c4af fix(ci): hub v1.1.5 — Claude Assistant caller permissions
Co-authored-by: Cursor <[email protected]>
2026-09-07 11:00:33 +01:00
Mervin Praison effc3982d6 Merge pull request #269 from MervinPraison/chore/hub-v1.1.4
fix(ci): hub v1.1.4 caller permissions for Claude review chain
2026-09-07 10:59:06 +01:00
MervinPraisonandCursor e67075b799 fix(ci): hub v1.1.4 — grant reusable-workflow caller permissions
Repos with default_workflow_permissions: read reject Auto PR Comment and merge
gate when the caller only grants pull-requests: read but callee jobs need write.

Co-authored-by: Cursor <[email protected]>
2026-09-07 10:58:56 +01:00
Mervin Praison 993114bb55 Merge pull request #268 from MervinPraison/chore/hub-v1.1.3
chore(ci): bump github-automation hub to v1.1.3
2026-09-07 10:53:23 +01:00
MervinPraisonandCursor 8c59d2db85 chore(ci): bump github-automation hub to v1.1.3
Fixes broken reusable workflow refs (v1.1.1 dynamic hub-setup) and review-chain
callable YAML so Auto PR Comment / merge gate / CI-failure Claude run again.

Co-authored-by: Cursor <[email protected]>
2026-09-07 10:53:05 +01:00
Dhivya-Bharathy dc98a79f6e feat(voice-agent): Phase 1 telephony webhooks, tools, and call dashboard 2026-09-07 13:18:37 +05:30
Dhivya-BharathyandCursor ced5a52871 feat(meeting-agent): Phase 2 Part B — live transcript + Calendar V2
Co-authored-by: Cursor <[email protected]>
2026-09-04 17:55:33 +05:30
Dhivya-BharathyandCursor 5f3b026da8 feat(meeting-agent): Phase 2 Recall.ai live bots (Part A)
Add Recall integration for live Google Meet/Zoom/Teams capture: bot scheduling, verified webhooks, async transcription, and post-call summarize + index pipeline. Includes recovery script, smoke mode, and unit tests. Phase 1 upload flow unchanged.

Co-authored-by: Cursor <[email protected]>
2026-09-04 10:59:01 +05:30
MervinPraisonandCursor 15aa219cb3 chore(automation): upgrade to hub v1.1.1 and fix pre-labelled issue Claude trigger
Switch merge-gate, review chain, CI failure, and pipeline workflows to thin
hub callers. Fix auto-issue-comment and claude-dedupe so bot-routed issues
opened with the claude label still dispatch Claude.

Co-authored-by: Cursor <[email protected]>
2026-09-02 13:08:41 +01:00
Mervin Praison 5c88692009 Merge pull request #261 from MervinPraison/claude/issue-260-20260902-meeting-ui
feat: thin meeting-agent UI example (PAI-MEET-UI)
2026-09-02 10:33:55 +01:00
praisonai-triage-agent[bot]andMervin Praison 83c273eba3 fix(compiler): remove extraneous f-string prefix breaking ruff (CI)
CI job `python (3.12)` failed at `ruff check src/praisonaiui` with F541
(f-string without placeholders) on compiler.py:922, blocking pytest before
it ran. Removed the unnecessary `f` prefix on the static og:type meta tag.

Co-authored-by: Mervin Praison <[email protected]>
2026-09-02 09:31:10 +00:00
MervinPraisonandClaude ee44c38297 feat: add thin meeting-agent UI example composing existing features (fixes #260)
Adds examples/python/meeting-agent/ wiring existing attachments, knowledge,
chat and Slack features via YAML/config only. No new package, no new public
exports, no create_app()/server.py params, no eager heavy deps.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-09-02 09:26:05 +00:00
Mervin Praison 8a79913cc4 Merge pull request #228 from MervinPraison/claude/issue-222-20260715-1318
feat: Training Lab UI for praisonai-train agent sessions (STITCH-012)
2026-09-02 10:09:12 +01:00
Mervin Praison af97f31e68 Merge pull request #229 from MervinPraison/claude/issue-217-20260715-run-recorder
feat: Run Flight Recorder — /runs three-pane run debugger (#217)
2026-09-02 10:09:00 +01:00
MervinPraisonandCursor 812c53abaa fix(docs): prevent stale content and full reloads on fast navigation
Abort in-flight markdown fetches, clear the article immediately on route change, and always intercept sidebar links so rapid clicks stay in the SPA instead of showing the wrong page.

Co-authored-by: Cursor <[email protected]>
2026-08-28 12:54:38 +01:00
MervinPraisonandCursor 4bc7497e99 feat(docs): Mintlify-style full-width layout and header navigation
Adopt a three-column docs shell with a centred 720px content column, integrated header tabs, search, and tab-filtered sidebar so navigation matches Mintlify-style sites.

Co-authored-by: Cursor <[email protected]>
2026-08-28 11:16:35 +01:00
MervinPraisonandCursor 4bf74be9c7 fix(docs): render Mermaid diagrams full width like Mintlify
Use useMaxWidth, measure the container width on render/resize, and scale
SVG to 100% of the content column. Widen the docs main area by removing
max-w-3xl so diagrams use space between sidebar and TOC.

Co-authored-by: Cursor <[email protected]>
2026-08-28 10:23:12 +01:00
MervinPraisonandCursor 800883aaf4 test: assert mermaid renders via code component unwrap
Co-authored-by: Cursor <[email protected]>
2026-08-27 14:58:06 +01:00
MervinPraisonandCursor fc28c77e84 fix(docs): detect mermaid fences in code component
Route ```mermaid blocks through the code markdown handler so diagrams
render reliably across react-markdown versions, with pre unwrapping the
MermaidDiagram output.

Co-authored-by: Cursor <[email protected]>
2026-08-27 14:47:03 +01:00
MervinPraisonandCursor 82980cee1a fix(docs): render Mermaid diagrams in React SPA
The mermaid plugin skips React-managed article content to avoid DOM
reconciler crashes, so fenced mermaid blocks showed as raw code. Add a
native MermaidDiagram component with lazy-loaded mermaid@11, theme sync,
and bundled frontend assets.

Co-authored-by: Cursor <[email protected]>
2026-08-27 14:37:34 +01:00
MervinPraisonandCursor 6bfc19a0ee fix(ci): use force_orphan for reliable GitHub Pages deploys
peaceiris was rm-ing gh-pages files then skipping commit when the tree
matched HEAD, leaving llms.txt and sitemap offline. force_orphan replaces
the branch on every deploy.

Co-authored-by: Cursor <[email protected]>
2026-08-26 22:28:25 +01:00
MervinPraisonandCursor 6f855aee3e feat(seo): complete crawl readiness — llms-full, lastmod, auto descriptions
Add llms-full.txt, sitemap lastmod dates, auto meta descriptions from first
paragraph, per-page noindex support, runtime JSON-LD/OG/Twitter updates, a
dedicated Deploy Docs workflow, and corrected SEO documentation.

Co-authored-by: Cursor <[email protected]>
2026-08-26 22:22:09 +01:00
MervinPraisonandCursor 203c57710a ci: allow manual workflow dispatch for docs deploy
Push webhooks stopped firing CI after 13:32 UTC; workflow_dispatch
lets us redeploy GitHub Pages with sitemap, robots.txt, and llms.txt.

Co-authored-by: Cursor <[email protected]>
2026-08-26 17:21:52 +01:00
MervinPraisonandCursor bd6d08b08e feat(seo): add llms.txt, crawl nav, and homepage sitemap entry
Generate llms.txt for AI crawlers, inject static nav links into prerendered
HTML, convert markdown links in noscript content, and include the site root
in sitemap.xml so Google can discover all doc pages once deployed.

Co-authored-by: Cursor <[email protected]>
2026-08-26 17:20:59 +01:00
MervinPraisonandCursor 90feb197b2 fix(docs): stop plugin DOM mutations that crash React SPA navigation
Guard plugin-loader, syntax-highlight, mermaid, and code-copy from touching
React-managed article content; emit flat doc HTML for canonical URLs; add SEO
sitemap/robots and trailing-slash normalisation so sidebar clicks stay stable.

Co-authored-by: Cursor <[email protected]>
2026-08-26 17:17:18 +01:00
MervinPraisonandCursor a391d55f74 fix(frontend): fetch content only when currentPath changes
Avoid re-running the markdown fetch when selectedItem syncs after
aiui:navigate, which was cancelling requests and leaving loading stuck.

Co-authored-by: Cursor <[email protected]>
2026-08-26 14:32:03 +01:00
MervinPraisonandCursor a5e6316021 fix(frontend): unstick "Loading content..." after SPA navigation
Reset loadingContent in effect cleanup when a fetch is cancelled, and
fetch only on currentPath changes so selectedItem sync does not abort
the in-flight request.

Co-authored-by: Cursor <[email protected]>
2026-08-26 14:31:48 +01:00
MervinPraisonandCursor 3c534c9637 fix(frontend): map /docs/index to /docs/index.md not /docs.md
Stripping a trailing /index segment broke the docs home page fetch,
causing "Content for PraisonAIUI not found" at /docs/index.

Co-authored-by: Cursor <[email protected]>
2026-08-26 14:30:49 +01:00
MervinPraisonandCursor e2486fe666 fix(frontend): stop empty pages and stale content on SPA nav
React now owns sidebar navigation content: sync selectedItem on
aiui:navigate, fetch markdown from currentPath with cancel-on-nav,
use canonical URLs without trailing slashes, and add data-nav-path on
sidebar buttons. Plugins teardown hide-mode instead of replacing React
DOM; mkdocs-compat no longer wipes main.flex-1.

Co-authored-by: Cursor <[email protected]>
2026-08-26 14:08:55 +01:00
MervinPraisonandCursor 60e9bbe2df fix(frontend): wrap theme tokens in hsl() so backgrounds render
Tailwind utilities used var(--background) directly, but shadcn tokens
store bare HSL components — backgrounds were transparent while prose
text used hsl(var(--foreground)), causing light-on-white in dark mode.
Also replace hardcoded dark anti-flicker with localStorage-aware bootstrap.

Co-authored-by: Cursor <[email protected]>
2026-08-26 12:07:06 +01:00
MervinPraisonandCursor 672070cd13 fix(frontend): restore light-mode contrast on plugin SPA path
Align content-loader and homepage plugins with React prose classes,
extend CSS contrast rules to all article.prose, fix server anti-flicker
hardcoded dark colours, wire theme toggle with localStorage, and add
guardrail tests for the plugin rendering path.

Co-authored-by: Cursor <[email protected]>
2026-08-26 11:23:46 +01:00
MervinPraisonandCursor db0672feec fix(frontend): restore light-mode prose and code contrast
Scope mermaid heading overrides to dark mode, fix hsl() theme tokens in
syntax-highlight, and force readable prose colours for body text, tables,
and code blocks in light mode.

Co-authored-by: Cursor <[email protected]>
2026-08-26 07:50:25 +01:00
praisonai-triage-agent[bot]andMervin Praison 4d685af1da fix: correct timeline row selected-highlight CSS selector in /runs
The selected-row highlight targeted .rr-timeline-row.selected but rows
render with class .db-timeline-row, so clicking a timeline row never
visually highlighted it. Align the selector to .db-timeline-row.selected.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Mervin Praison <[email protected]>
2026-07-16 07:55:06 +00:00
MervinPraisonandClaude a1c7a3c279 feat: add Run Flight Recorder /runs three-pane debugger (fixes #217)
Merges chat turns, trace spans, log lines and errors into one per-session
timeline. Reuses existing APIs only. Adds formatDuration/emptyState/
traceStatusBadge/timelineRow helpers, registers the runs page (Control,
order 22), and deep-links from Traces, Command Center and Work Hub.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2026-07-16 07:55:06 +00:00
praisonai-triage-agent[bot]andMervin Praison 10f3bbe1ce fix(training): correct agent_id filter, apply dropdown source, and view nav order
- features/training.py: derive agent_id in _summarize_session so the
  /api/training/sessions?agent_id= filter actually matches (was always None).
- training-lab.js: fetch /api/agents/definitions (returns `id`) instead of
  /api/agents (returns `agent_id`) so the apply dropdown carries valid agent
  IDs matching the registry; fall back to agent_id defensively.
- agents.js: set the agent_id URL param before selectPage('training') so the
  Training Lab picks up the filter on initial render.
- tests: add regression test for agent_id session filtering.

Co-authored-by: Mervin Praison <[email protected]>
2026-07-15 13:47:40 +00:00
praisonai-triage-agent[bot]andMervin Praison c66a33c5d3 feat: add Training Lab UI for praisonai-train sessions (fixes #222)
Co-authored-by: Mervin Praison <[email protected]>
2026-07-15 13:26:32 +00:00
181 changed files with 14644 additions and 3043 deletions

No files matched your search

+9
View File
@@ -0,0 +1,9 @@
{
"templateRepo": "MervinPraison/github-automation-template",
"stack": "claude",
"profile": "sdk",
"mode": "hub",
"hubRef": "v1.1.6",
"templateVersion": "1.1.6",
"installedAt": "2026-09-07T10:02:11Z"
}
+4 -3
View File
@@ -24,7 +24,7 @@ jobs:
trigger-claude:
if: |
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'issues' && !contains(toJSON(github.event.issue.labels), 'claude'))
github.event_name == 'issues'
runs-on: ubuntu-latest
permissions:
issues: write
@@ -52,7 +52,8 @@ jobs:
});
const labels = (issue.labels || []).map(l => typeof l === 'string' ? l : l.name);
if (!labels.includes('claude')) {
const hadClaude = labels.includes('claude');
if (!hadClaude) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
@@ -60,7 +61,7 @@ jobs:
labels: ['claude'],
});
} else {
console.log(`Issue #${issueNumber} already has 'claude' label.`);
console.log(`Issue #${issueNumber} already has 'claude' label — dispatching Claude only.`);
}
const body = [
+11 -787
View File
@@ -1,14 +1,5 @@
name: Auto PR Comment
# Review chain: CodeRabbit/Qodo/Gemini review first → Copilot → Claude (final)
# For human PRs: CodeRabbit/Qodo auto-review → this workflow triggers Copilot → then Claude
# For bot PRs: CodeRabbit/Qodo skip bots, so we trigger them explicitly first
#
# IMPORTANT: @copilot ignores bot comments. All @copilot mentions must use
# PAT_TOKEN so the comment posts as MervinPraison (a real user).
#
# Claude trigger runs IN-BAND (same workflow) to avoid GitHub Actions
# blocking bot-triggered workflows with action_required approval gates.
on:
issue_comment:
types: [created]
@@ -17,793 +8,26 @@ on:
pull_request:
types: [opened, synchronize]
pull_request_target:
types: [synchronize]
types: [opened, synchronize]
schedule:
- cron: '30 3,9,15,21 * * *'
workflow_dispatch:
inputs:
pr_number:
description: 'PR number for manual FINAL (leave empty to sweep all open PRs)'
required: false
type: number
env:
REVIEW_CHAIN_SKIP_COPILOT: '1'
permissions:
contents: read
issues: write
pull-requests: write
env:
MAX_POLL_ATTEMPTS: 20
POLL_DELAY_MS: 30000
CLAUDE_TRIGGER_LOGINS: '["MervinPraison","github-actions[bot]"]'
# GH_TOKEN (user PAT) posts as MervinPraison for @copilot; fallback to github.token for @claude
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
CLAUDE_UI_SCOPE: 'SCOPE: Focus ONLY on PraisonAIUI (src/praisonaiui, src/frontend, tests, docs). Do not expand into praisonaiagents or the monorepo unless the issue explicitly requires it.'
CLAUDE_FINAL_PHASES_COMPACT: "**Phase 1: Review per AGENTS.md** (lazy imports, backward compat, import-time <200ms; SDK/product value gate — genuine UI value, correct UI vs praisonaiagents layering, no scope creep)\n**Phase 2: FIX valid issues** found by prior reviewers — push to THIS branch\n**Phase 3: Final verdict** — approve PR if ready, else request changes"
CLAUDE_FINAL_PHASES_DETAILED: "**Phase 1: Review per AGENTS.md**\n1. Protocol-driven: check heavy implementations vs core SDK\n2. Backward compatible: ensure zero feature regressions\n3. Performance: no hot-path regressions (lazy imports, import-time <200ms)\n4. Product value: review in depth whether the change genuinely adds PraisonAIUI UX value — never add features for the sake of adding them. It must strengthen the UI (simpler, more user-friendly, robust). Reject scope creep; request changes or recommend closing if value is unclear\n5. Layering: UI/frontend/server features here; core agent runtime belongs in praisonaiagents. Agent-callable tools → PraisonAI-Tools\n6. Do not bloat server.py or create_app() with new params — prefer existing YAML config, plugins, and feature modules; reject knobs/exports with no live consumer\n\n**Phase 2: FIX Valid Issues**\n7. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n8. Push all code fixes directly to THIS branch (do NOT create a new PR)\n9. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n10. If all issues are resolved, approve the PR / close the Issue\n11. If blocking issues remain, request changes / leave clear action items"
jobs:
# ================================================================
# HUMAN PRs: Trigger @copilot AFTER CodeRabbit posts its summary
# ================================================================
copilot-after-coderabbit:
if: |
github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
github.event.comment.user.login == 'coderabbitai[bot]' &&
contains(github.event.comment.body, 'summarize by coderabbit')
runs-on: ubuntu-latest
permissions:
pull-requests: write
outputs:
pr_number: ${{ steps.trigger.outputs.pr_number }}
triggered: ${{ steps.trigger.outputs.triggered }}
steps:
- name: Post Copilot review request (as user via PAT)
id: trigger
uses: actions/github-script@v7
with:
github-token: ${{ env.GH_TOKEN }}
script: |
const comments = await github.rest.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo
});
// Check for duplicates — posted by MervinPraison (PAT) or github-actions[bot] (legacy)
const alreadyPosted = comments.data.some(c =>
c.body.includes('@copilot') &&
(c.user.login === 'MervinPraison' || c.user.login === 'github-actions[bot]')
);
if (alreadyPosted) {
console.log('Copilot already triggered, skipping');
core.setOutput('triggered', 'false');
core.setOutput('pr_number', context.issue.number.toString());
return;
}
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '@copilot Do a thorough review of this PR. Read ALL existing reviewer comments above from Qodo, Coderabbit, and Gemini first — incorporate their findings.\n\nReview areas:\n1. **Bloat check**: Are changes minimal and focused? Any unnecessary code or scope creep?\n2. **Security**: Any hardcoded secrets, unsafe eval/exec, missing input validation?\n3. **Performance**: Any module-level heavy imports? Hot-path regressions?\n4. **Tests**: Are tests included? Do they cover the changes adequately?\n5. **Backward compat**: Any public API changes without deprecation?\n6. **Code quality**: DRY violations, naming conventions, error handling?\n7. **Address reviewer feedback**: If Qodo, Coderabbit, or Gemini flagged valid issues, include them in your review\n8. Suggest specific improvements with code examples where possible'
});
core.setOutput('triggered', 'true');
core.setOutput('pr_number', context.issue.number.toString());
# Fallback: Trigger @copilot after Qodo review (if Coderabbit doesn't fire)
copilot-after-qodo:
if: |
github.event_name == 'pull_request_review' &&
github.event.review.user.login == 'qodo-code-review[bot]'
runs-on: ubuntu-latest
permissions:
pull-requests: write
outputs:
pr_number: ${{ steps.trigger.outputs.pr_number }}
triggered: ${{ steps.trigger.outputs.triggered }}
steps:
- name: Post Copilot review request (as user via PAT)
id: trigger
uses: actions/github-script@v7
with:
github-token: ${{ env.GH_TOKEN }}
script: |
const comments = await github.rest.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo
});
const alreadyPosted = comments.data.some(c =>
c.body.includes('@copilot') &&
(c.user.login === 'MervinPraison' || c.user.login === 'github-actions[bot]')
);
if (alreadyPosted) {
console.log('Copilot already triggered, skipping');
core.setOutput('triggered', 'false');
core.setOutput('pr_number', context.issue.number.toString());
return;
}
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '@copilot Do a thorough review of this PR. Read ALL existing reviewer comments above first.\n\nReview areas:\n1. **Bloat check**: Are changes minimal and focused?\n2. **Security**: Any hardcoded secrets, unsafe eval/exec, missing input validation?\n3. **Performance**: Any module-level heavy imports? Hot-path regressions?\n4. **Tests**: Are tests included? Do they cover the changes adequately?\n5. **Backward compat**: Any public API changes without deprecation?\n6. **Code quality**: DRY violations, naming conventions, error handling?\n7. Suggest specific improvements with code examples where possible'
});
core.setOutput('triggered', 'true');
core.setOutput('pr_number', context.issue.number.toString());
# ================================================================
# FALLBACK: Claude trigger for cases where Copilot chain fails/skips
# Ensures Claude always runs after sufficient review time
# ================================================================
claude-fallback-timeout:
if: |
github.event_name == 'pull_request' &&
github.event.action == 'opened'
runs-on: ubuntu-latest
timeout-minutes: 20
review-chain:
# Grant union of callee job permissions (repos with default_workflow_permissions: read
# reject reusable calls when the caller ceiling is lower than any callee job).
permissions:
issues: write
pull-requests: write
steps:
- name: Wait for review window (15 min)
run: sleep 900
- name: Check if Claude already triggered
id: check_claude
uses: actions/github-script@v7
env:
TRIGGER_LOGINS: ${{ env.CLAUDE_TRIGGER_LOGINS }}
PR_NUMBER: ${{ github.event.pull_request.number }}
with:
github-token: ${{ env.GH_TOKEN }}
script: |
const pr = parseInt(process.env.PR_NUMBER, 10);
const triggerLogins = JSON.parse(process.env.TRIGGER_LOGINS);
const comments = await github.rest.issues.listComments({
issue_number: pr,
owner: context.repo.owner,
repo: context.repo.repo
});
const alreadyPosted = comments.data.some(c =>
c.body.includes('@claude') && triggerLogins.includes(c.user.login)
);
core.setOutput('already_triggered', alreadyPosted ? 'true' : 'false');
core.setOutput('comments_count', comments.data.length.toString());
const botReviews = comments.data.filter(c =>
['coderabbitai[bot]', 'qodo-code-review[bot]', 'gemini-code-assist[bot]',
'copilot-swe-agent', 'Copilot', 'greptile-apps[bot]'].some(bot =>
c.user.login.toLowerCase().includes(bot.toLowerCase())
)
);
core.setOutput('review_count', botReviews.length.toString());
- name: Aggregate reviews and trigger Claude
if: steps.check_claude.outputs.already_triggered != 'true'
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
with:
github-token: ${{ env.GH_TOKEN }}
script: |
const pr = parseInt(process.env.PR_NUMBER, 10);
const comments = await github.rest.issues.listComments({issue_number: pr, owner: context.repo.owner, repo: context.repo.repo, per_page: 100});
const reviews = {coderabbit: [], qodo: [], gemini: [], copilot: [], greptile: [], others: []};
comments.data.forEach(c => {
const login = c.user.login.toLowerCase();
const body = c.body.substring(0, 500);
if (login.includes('coderabbit')) reviews.coderabbit.push(body);
else if (login.includes('qodo')) reviews.qodo.push(body);
else if (login.includes('gemini')) reviews.gemini.push(body);
else if (login.includes('copilot')) reviews.copilot.push(body);
else if (login.includes('greptile')) reviews.greptile.push(body);
else if (c.user.type === 'Bot') reviews.others.push(body);
});
const summaryParts = [];
if (reviews.coderabbit.length) summaryParts.push('CodeRabbit: ' + reviews.coderabbit.length + ' comments');
if (reviews.qodo.length) summaryParts.push('Qodo: ' + reviews.qodo.length + ' comments');
if (reviews.gemini.length) summaryParts.push('Gemini: ' + reviews.gemini.length + ' comments');
if (reviews.copilot.length) summaryParts.push('Copilot: ' + reviews.copilot.length + ' comments');
if (reviews.greptile.length) summaryParts.push('Greptile: ' + reviews.greptile.length + ' comments');
const reviewSummary = summaryParts.length ? summaryParts.join(' | ') : 'No bot reviews detected';
const scope = process.env.CLAUDE_UI_SCOPE;
const phases = process.env.CLAUDE_FINAL_PHASES_COMPACT;
const claudeBody = '@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAIUI (not a fork), you are able to make modifications to this branch and push directly. ' + scope + ' Read ALL prior reviewer comments. ' + reviewSummary + '.\n\n' +
'Review Context: ' + comments.data.length + ' total comments, ' +
(reviews.coderabbit.length + reviews.qodo.length + reviews.gemini.length + reviews.copilot.length + reviews.greptile.length) + ' bot reviews.\n\n' +
phases;
await github.rest.issues.createComment({issue_number: pr, owner: context.repo.owner, repo: context.repo.repo, body: claudeBody});
console.log('Claude fallback triggered on PR #' + pr + ' with ' + reviewSummary);
# ================================================================
# Claude FINAL review — polls for Copilot's response, then triggers
# This runs IN-BAND under the PAT context (MervinPraison), avoiding
# the action_required approval gate that blocks bot-triggered workflows.
# ================================================================
claude-after-copilot:
needs: [copilot-after-coderabbit]
if: |
always() &&
needs.copilot-after-coderabbit.result == 'success' &&
needs.copilot-after-coderabbit.outputs.triggered == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Wait for Copilot to respond (poll)
id: poll
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ needs.copilot-after-coderabbit.outputs.pr_number }}
MAX_POLL_ATTEMPTS: ${{ env.MAX_POLL_ATTEMPTS }}
POLL_DELAY_MS: ${{ env.POLL_DELAY_MS }}
with:
github-token: ${{ env.GH_TOKEN }}
script: |
const pr = parseInt(process.env.PR_NUMBER, 10);
const owner = context.repo.owner;
const repo = context.repo.repo;
// Poll for Copilot's response — check every 30s for up to 10 minutes
const maxAttempts = parseInt(process.env.MAX_POLL_ATTEMPTS, 10);
const delayMs = parseInt(process.env.POLL_DELAY_MS, 10);
for (let i = 0; i < maxAttempts; i++) {
console.log(`Poll attempt ${i + 1}/${maxAttempts}...`);
const comments = await github.rest.issues.listComments({
issue_number: pr,
owner,
repo,
per_page: 50,
sort: 'created',
direction: 'desc'
});
// Look for Copilot's response (copilot-swe-agent or Copilot)
const copilotResponse = comments.data.find(c =>
(c.user.login === 'copilot-swe-agent' ||
c.user.login === 'Copilot' ||
c.user.login.toLowerCase().includes('copilot')) &&
c.user.type === 'Bot'
);
if (copilotResponse) {
console.log(`Copilot responded at ${copilotResponse.created_at}`);
core.setOutput('copilot_responded', 'true');
return;
}
// Also check PR reviews for Copilot
const reviews = await github.rest.pulls.listReviews({
pull_number: pr,
owner,
repo
});
const copilotReview = reviews.data.find(r =>
r.user.login.toLowerCase().includes('copilot')
);
if (copilotReview) {
console.log(`Copilot review submitted at ${copilotReview.submitted_at}`);
core.setOutput('copilot_responded', 'true');
return;
}
if (i < maxAttempts - 1) {
console.log(`Copilot hasn't responded yet, waiting ${delayMs/1000}s...`);
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
console.log('Copilot did not respond within timeout, triggering Claude anyway');
core.setOutput('copilot_responded', 'timeout');
- name: Check if Claude already triggered
id: check_claude
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ needs.copilot-after-coderabbit.outputs.pr_number }}
TRIGGER_LOGINS: ${{ env.CLAUDE_TRIGGER_LOGINS }}
with:
github-token: ${{ env.GH_TOKEN }}
script: |
const pr = parseInt(process.env.PR_NUMBER, 10);
const triggerLogins = JSON.parse(process.env.TRIGGER_LOGINS);
const comments = await github.rest.issues.listComments({
issue_number: pr,
owner: context.repo.owner,
repo: context.repo.repo
});
const alreadyPosted = comments.data.some(c =>
c.body.includes('@claude') && triggerLogins.includes(c.user.login)
);
core.setOutput('already_triggered', alreadyPosted ? 'true' : 'false');
- name: Trigger Claude final review
if: steps.check_claude.outputs.already_triggered != 'true'
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ needs.copilot-after-coderabbit.outputs.pr_number }}
with:
github-token: ${{ env.GH_TOKEN }}
script: |
const pr = parseInt(process.env.PR_NUMBER, 10);
const scope = process.env.CLAUDE_UI_SCOPE;
const phases = process.env.CLAUDE_FINAL_PHASES_DETAILED;
await github.rest.issues.createComment({
issue_number: pr,
owner: context.repo.owner,
repo: context.repo.repo,
body: '@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAIUI (not a fork), you are able to make modifications to this branch and push directly. ' + scope + ' Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding.\n\n' + phases
});
console.log(`Claude triggered on PR #${pr}`);
- name: Sync pipeline status labels
if: steps.check_claude.outputs.already_triggered != 'true'
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ needs.copilot-after-coderabbit.outputs.pr_number }}
with:
github-token: ${{ env.GH_TOKEN }}
script: |
const path = require('path');
const ps = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pipeline-status.js'));
const pr = parseInt(process.env.PR_NUMBER, 10);
await ps.ensurePipelineLabels(github, context.repo.owner, context.repo.repo, core);
await ps.syncPipelineLabels(github, context.repo.owner, context.repo.repo, pr, core);
# ================================================================
# Claude after Qodo fallback path (same polling approach)
# ================================================================
claude-after-copilot-qodo:
needs: [copilot-after-qodo]
if: |
always() &&
needs.copilot-after-qodo.result == 'success' &&
needs.copilot-after-qodo.outputs.triggered == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
issues: write
pull-requests: write
steps:
- name: Wait for Copilot to respond (poll)
id: poll
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ needs.copilot-after-qodo.outputs.pr_number }}
MAX_POLL_ATTEMPTS: ${{ env.MAX_POLL_ATTEMPTS }}
POLL_DELAY_MS: ${{ env.POLL_DELAY_MS }}
with:
github-token: ${{ env.GH_TOKEN }}
script: |
const pr = parseInt(process.env.PR_NUMBER, 10);
const owner = context.repo.owner;
const repo = context.repo.repo;
const maxAttempts = parseInt(process.env.MAX_POLL_ATTEMPTS, 10);
const delayMs = parseInt(process.env.POLL_DELAY_MS, 10);
for (let i = 0; i < maxAttempts; i++) {
console.log(`Poll attempt ${i + 1}/${maxAttempts}...`);
const comments = await github.rest.issues.listComments({
issue_number: pr, owner, repo,
per_page: 50, sort: 'created', direction: 'desc'
});
const copilotResponse = comments.data.find(c =>
(c.user.login === 'copilot-swe-agent' ||
c.user.login === 'Copilot' ||
c.user.login.toLowerCase().includes('copilot')) &&
c.user.type === 'Bot'
);
if (copilotResponse) {
console.log(`Copilot responded at ${copilotResponse.created_at}`);
core.setOutput('copilot_responded', 'true');
return;
}
const reviews = await github.rest.pulls.listReviews({
pull_number: pr, owner, repo
});
const copilotReview = reviews.data.find(r =>
r.user.login.toLowerCase().includes('copilot')
);
if (copilotReview) {
console.log(`Copilot review submitted at ${copilotReview.submitted_at}`);
core.setOutput('copilot_responded', 'true');
return;
}
if (i < maxAttempts - 1) {
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
console.log('Copilot did not respond within timeout, triggering Claude anyway');
core.setOutput('copilot_responded', 'timeout');
- name: Check if Claude already triggered
id: check_claude
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ needs.copilot-after-qodo.outputs.pr_number }}
TRIGGER_LOGINS: ${{ env.CLAUDE_TRIGGER_LOGINS }}
with:
github-token: ${{ env.GH_TOKEN }}
script: |
const pr = parseInt(process.env.PR_NUMBER, 10);
const triggerLogins = JSON.parse(process.env.TRIGGER_LOGINS);
const comments = await github.rest.issues.listComments({
issue_number: pr,
owner: context.repo.owner,
repo: context.repo.repo
});
const alreadyPosted = comments.data.some(c =>
c.body.includes('@claude') && triggerLogins.includes(c.user.login)
);
core.setOutput('already_triggered', alreadyPosted ? 'true' : 'false');
- name: Trigger Claude final review
if: steps.check_claude.outputs.already_triggered != 'true'
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ needs.copilot-after-qodo.outputs.pr_number }}
with:
github-token: ${{ env.GH_TOKEN }}
script: |
const pr = parseInt(process.env.PR_NUMBER, 10);
const scope = process.env.CLAUDE_UI_SCOPE;
const phases = process.env.CLAUDE_FINAL_PHASES_DETAILED;
await github.rest.issues.createComment({
issue_number: pr,
owner: context.repo.owner,
repo: context.repo.repo,
body: '@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAIUI (not a fork), you are able to make modifications to this branch and push directly. ' + scope + ' Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding.\n\n' + phases
});
console.log(`Claude triggered on PR #${pr}`);
# ================================================================
# Claude after Gemini — restores the trigger lost from the deleted
# chain-claude-after-copilot.yml workflow.
# Fires when Gemini posts a review comment on a PR or issue.
# ================================================================
claude-after-gemini:
if: |
github.event_name == 'issue_comment' &&
(
github.event.comment.user.login == 'gemini-code-assist[bot]' ||
contains(github.event.comment.body, 'Review completed by Gemini CLI') ||
contains(github.event.comment.body, 'The fix is ready for review. Please test')
)
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- name: Check if Claude already triggered
id: check_claude
uses: actions/github-script@v7
env:
TRIGGER_LOGINS: ${{ env.CLAUDE_TRIGGER_LOGINS }}
with:
github-token: ${{ env.GH_TOKEN }}
script: |
const triggerLogins = JSON.parse(process.env.TRIGGER_LOGINS);
const comments = await github.rest.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo
});
const alreadyPosted = comments.data.some(c =>
c.body.includes('@claude') && triggerLogins.includes(c.user.login)
);
core.setOutput('already_triggered', alreadyPosted ? 'true' : 'false');
- name: Post Claude analysis request (Lead Engineer)
if: steps.check_claude.outputs.already_triggered != 'true'
uses: actions/github-script@v7
with:
github-token: ${{ env.GH_TOKEN }}
script: |
const scope = process.env.CLAUDE_UI_SCOPE;
const phases = process.env.CLAUDE_FINAL_PHASES_DETAILED;
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '@claude You are the Lead Engineer. If the branch is under MervinPraison/PraisonAIUI (not a fork), you are able to make modifications to this branch and push directly. ' + scope + ' Read ALL analysis and reviews above carefully (Gemini, CodeRabbit, Qodo, Copilot, etc).\n\n' + phases
});
# ================================================================
# Recovery: re-post FINAL @claude after push or when chain missed
# ================================================================
claude-review-recovery:
if: |
github.event_name == 'pull_request_target' &&
github.event.action == 'synchronize'
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: read
steps:
- uses: actions/checkout@v4
- name: Re-post FINAL Claude review when stale or missing
uses: actions/github-script@v7
with:
github-token: ${{ env.GH_TOKEN }}
script: |
const path = require('path');
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
const chain = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pr-review-chain.js'));
const pr = context.payload.pull_request.number;
const owner = context.repo.owner;
const repo = context.repo.repo;
const comments = await mergeGate.listAllComments(github, owner, repo, pr);
const headPushedAt = (await mergeGate.getHeadCommitDate(github, owner, repo, pr))
|| context.payload.pull_request.updated_at;
const headPusher = context.payload.sender?.login || null;
const hasFinal = mergeGate.hasFinalClaudeReviewTrigger(comments);
const isStale = mergeGate.isStaleFinalAfterPush(comments, headPushedAt);
if (mergeGate.shouldSkipFinalRecovery(comments, headPushedAt)) {
core.info('Recent @claude within 35min and FINAL not stale — skip recovery');
return;
}
if (await mergeGate.hasInProgressClaudeAssistant(github, owner, repo, pr)) {
core.info('Claude workflow in progress — skip recovery');
return;
}
const finalBody = mergeGate.FINAL_CLAUDE_REVIEW_BODY;
const staleOpts = { allowStaleRepost: true, skipCopilot: true, optionalWaitMs: 0 };
const missingOpts = { skipCopilot: true, optionalWaitMs: 0 };
if (hasFinal && isStale) {
const gate = mergeGate.shouldSkipStaleFinalRecovery(comments, headPushedAt, headPusher);
if (gate.skip) {
core.info(`Skip stale-FINAL recovery: ${gate.reason}`);
return;
}
const result = await chain.maybeTriggerClaudeFinal(
github, owner, repo, pr, finalBody, core, staleOpts
);
if (result.posted) core.info(`Posted stale-FINAL recovery @claude on PR #${pr}`);
return;
}
if (hasFinal) {
core.info('FINAL Claude review current on HEAD — skip recovery');
return;
}
const result = await chain.maybeTriggerClaudeFinal(
github, owner, repo, pr, finalBody, core, missingOpts
);
if (result.posted) core.info(`Posted missing FINAL Claude review on PR #${pr}`);
- name: Sync pipeline status labels
uses: actions/github-script@v7
with:
github-token: ${{ env.GH_TOKEN }}
script: |
const path = require('path');
const ps = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pipeline-status.js'));
const pr = context.payload.pull_request.number;
await ps.ensurePipelineLabels(github, context.repo.owner, context.repo.repo, core);
await ps.syncPipelineLabels(github, context.repo.owner, context.repo.repo, pr, core);
# Scheduled + manual: post missing or stale FINAL @claude on all open PRs
post-missing-claude-final:
if: |
github.event_name == 'schedule' ||
(github.event_name == 'workflow_dispatch' && !inputs.pr_number)
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: read
steps:
- uses: actions/checkout@v4
- name: Post missing/stale FINAL @claude on open PRs
uses: actions/github-script@v7
with:
github-token: ${{ env.GH_TOKEN }}
script: |
const path = require('path');
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
const chain = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pr-review-chain.js'));
const ps = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pipeline-status.js'));
const owner = context.repo.owner;
const repo = context.repo.repo;
const prs = await github.paginate(github.rest.pulls.list, {
owner, repo, state: 'open', per_page: 100,
});
let posted = 0;
for (const pr of prs) {
if (pr.draft) continue;
const comments = await mergeGate.listAllComments(github, owner, repo, pr.number);
const headPushedAt = (await mergeGate.getHeadCommitDate(github, owner, repo, pr.number)) || pr.updated_at;
const hasFinal = mergeGate.hasFinalClaudeReviewTrigger(comments);
const isStale = mergeGate.isStaleFinalAfterPush(comments, headPushedAt);
if (hasFinal && !isStale) continue;
if (mergeGate.hasRecentClaudeTrigger(comments, 10)) {
core.info(`PR #${pr.number}: @claude within 10min — skip`);
continue;
}
if (await mergeGate.hasInProgressClaudeAssistant(github, owner, repo, pr.number)) {
core.info(`PR #${pr.number}: Claude in progress — skip`);
continue;
}
if (hasFinal && isStale) {
const gate = mergeGate.shouldSkipStaleFinalRecovery(comments, headPushedAt);
if (gate.skip) {
core.info(`PR #${pr.number}: skip stale — ${gate.reason}`);
continue;
}
}
const opts = hasFinal && isStale
? { allowStaleRepost: true, skipCopilot: true, optionalWaitMs: 0 }
: { skipCopilot: true, optionalWaitMs: 0 };
const result = await chain.maybeTriggerClaudeFinal(
github, owner, repo, pr.number,
mergeGate.FINAL_CLAUDE_REVIEW_BODY, core, opts
);
if (result.posted) {
posted++;
core.info(`Posted Claude FINAL on PR #${pr.number}`);
}
await ps.syncPipelineLabels(github, owner, repo, pr.number, core);
if (posted >= 10) break;
}
core.info(`Missing/stale Claude FINAL sweep complete (${posted} posted)`);
# ================================================================
# BOT PRs: CodeRabbit → Claude directly (skip Copilot when no user PAT)
# Copilot ignores github-actions[bot] comments; bot PRs use this path.
# ================================================================
claude-after-coderabbit-bot-pr:
if: |
github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
github.event.comment.user.login == 'coderabbitai[bot]' &&
contains(github.event.comment.body, 'summarize by coderabbit')
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: read
steps:
- name: Trigger Claude final review on bot PR
uses: actions/github-script@v7
env:
TRIGGER_LOGINS: ${{ env.CLAUDE_TRIGGER_LOGINS }}
with:
github-token: ${{ env.GH_TOKEN }}
script: |
const pr = context.issue.number;
const { data: pull } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr,
});
const author = pull.user.login;
const isBot = author === 'github-actions[bot]' ||
author === 'praisonai-triage-agent[bot]' ||
pull.user.type === 'Bot';
if (!isBot) {
console.log('Human PR — Copilot chain handles Claude trigger');
return;
}
const triggerLogins = JSON.parse(process.env.TRIGGER_LOGINS);
const comments = await github.rest.issues.listComments({
issue_number: pr,
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 100,
});
const alreadyPosted = comments.data.some(c =>
c.body.includes('@claude') && triggerLogins.includes(c.user.login)
);
if (alreadyPosted) {
console.log('Claude already triggered on PR #' + pr);
return;
}
await github.rest.issues.createComment({
issue_number: pr,
owner: context.repo.owner,
repo: context.repo.repo,
body: '@claude You are the FINAL architecture reviewer on this bot-authored PR. Push fixes directly to THIS branch (do NOT open a new PR). ' + process.env.CLAUDE_UI_SCOPE + ' Read ALL CodeRabbit/Qodo comments above.\n\n' + process.env.CLAUDE_FINAL_PHASES_COMPACT
});
console.log('Claude final review triggered on bot PR #' + pr);
# ================================================================
# Manual: trigger Claude final review on any PR
# ================================================================
claude-manual-dispatch:
if: github.event_name == 'workflow_dispatch' && inputs.pr_number
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Trigger Claude final review
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ inputs.pr_number }}
TRIGGER_LOGINS: ${{ env.CLAUDE_TRIGGER_LOGINS }}
with:
github-token: ${{ env.GH_TOKEN }}
script: |
const pr = parseInt(process.env.PR_NUMBER, 10);
const triggerLogins = JSON.parse(process.env.TRIGGER_LOGINS);
const comments = await github.rest.issues.listComments({
issue_number: pr,
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 100,
});
const alreadyPosted = comments.data.some(c =>
c.body.includes('@claude') && triggerLogins.includes(c.user.login)
);
if (alreadyPosted) {
console.log('Claude already triggered on PR #' + pr);
return;
}
await github.rest.issues.createComment({
issue_number: pr,
owner: context.repo.owner,
repo: context.repo.repo,
body: '@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAIUI (not a fork), you are able to make modifications to this branch and push directly. ' + process.env.CLAUDE_UI_SCOPE + ' Read all prior reviewer comments.\n\n' + process.env.CLAUDE_FINAL_PHASES_COMPACT
});
console.log('Claude manually triggered on PR #' + pr);
# ================================================================
# BOT PRs: kick CodeRabbit + Qodo
# ================================================================
bot-pr-trigger-reviews:
if: |
github.event_name == 'pull_request' &&
github.event.action == 'opened' &&
(
github.event.pull_request.user.login == 'github-actions[bot]' ||
github.event.pull_request.user.login == 'praisonai-triage-agent[bot]' ||
github.event.pull_request.user.type == 'Bot'
)
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Trigger CodeRabbit and Qodo reviews
uses: actions/github-script@v7
with:
github-token: ${{ env.GH_TOKEN }}
script: |
const path = require('path');
const chain = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/bot-pr-review-chain.js'));
const pr = context.payload.pull_request.number;
await chain.kickReviewChain(github, context.repo.owner, context.repo.repo, pr, core);
- name: Sync pipeline status labels
uses: actions/github-script@v7
with:
github-token: ${{ env.GH_TOKEN }}
script: |
const path = require('path');
const ps = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pipeline-status.js'));
const pr = context.payload.pull_request.number;
await ps.ensurePipelineLabels(github, context.repo.owner, context.repo.repo, core);
await ps.syncPipelineLabels(github, context.repo.owner, context.repo.repo, pr, core);
uses: MervinPraison/github-automation-template/.github/workflows/[email protected]
secrets: inherit
with:
hub_ref: v1.1.6
+5 -25
View File
@@ -1,7 +1,5 @@
name: Bot PR Recovery
# Belt-and-braces: kick CodeRabbit/Qodo when pull_request:opened never fired for bot PRs.
on:
schedule:
- cron: '*/15 * * * *'
@@ -18,26 +16,8 @@ permissions:
jobs:
recover:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Find and kick stalled bot PRs
uses: actions/github-script@v7
env:
PR_NUMBER_INPUT: ${{ github.event.inputs.pr_number }}
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const path = require('path');
const chain = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/bot-pr-review-chain.js'));
const owner = context.repo.owner;
const repo = context.repo.repo;
const manual = (process.env.PR_NUMBER_INPUT || '').trim();
const prNumber = manual ? parseInt(manual, 10) : null;
if (manual && Number.isNaN(prNumber)) {
throw new Error(`Invalid pr_number: ${manual}`);
}
await chain.recoverStalledBotPrs(github, owner, repo, { prNumber }, core);
uses: MervinPraison/github-automation-template/.github/workflows/[email protected]
secrets: inherit
with:
hub_ref: v1.1.6
pr_number: ${{ github.event.inputs.pr_number || '' }}
+6 -76
View File
@@ -1,17 +1,14 @@
name: Auto CI Failure → Claude
# Detects internal PRs with failing CI and posts @claude with extracted failure details.
on:
workflow_run:
workflows: [CI]
workflows: [Core Tests, Optimized Test Suite]
types: [completed]
pull_request:
types: [opened, synchronize, reopened]
workflow_dispatch:
inputs:
pr_number:
description: 'Optional PR number to process (default: scan blocked PRs)'
required: false
type: string
schedule:
@@ -21,10 +18,6 @@ concurrency:
group: ci-failure-scan-${{ github.repository }}-${{ github.event.pull_request.number || github.event.workflow_run.id || github.run_id }}
cancel-in-progress: true
env:
# PAT posts as MervinPraison so issue_comment triggers claude.yml (GITHUB_TOKEN cannot chain workflows)
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
permissions:
pull-requests: read
issues: write
@@ -32,71 +25,8 @@ permissions:
jobs:
detect-and-trigger:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Detect CI failures and trigger Claude
uses: actions/github-script@v7
with:
github-token: ${{ env.GH_TOKEN }}
script: |
const path = require('path');
const ciFix = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/ci-failure-claude.js'));
const owner = context.repo.owner;
const repo = context.repo.repo;
const baseRepo = `${owner}/${repo}`;
async function processPR(prNumber) {
return ciFix.maybeTriggerCiFixClaude(github, owner, repo, prNumber, core);
}
let results = [];
if (context.eventName === 'workflow_run') {
const wr = context.payload.workflow_run;
const result = await ciFix.processWorkflowRunFailure(
github, owner, repo, wr, core
);
results.push({ event: 'workflow_run', workflow: wr.name, ...result });
} else if (context.eventName === 'pull_request') {
results.push({
pr: context.payload.pull_request.number,
...(await processPR(context.payload.pull_request.number)),
});
} else {
let prNumbers = [];
if (context.eventName === 'workflow_dispatch' && context.payload.inputs?.pr_number) {
prNumbers = [Number(context.payload.inputs.pr_number)];
} else {
const prs = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: 'open',
per_page: 100,
});
for (const pr of prs) {
if (pr.draft) continue;
const headRepo = pr.head?.repo?.full_name;
if (headRepo && headRepo !== baseRepo) continue;
const { data: issue } = await github.rest.issues.get({
owner,
repo,
issue_number: pr.number,
});
const labels = issue.labels.map((l) => l.name);
if (labels.includes('pipeline/blocked:ci') || labels.includes(ciFix.CI_FIX_LABEL)) {
prNumbers.push(pr.number);
}
}
}
for (const num of prNumbers) {
results.push({ pr: num, ...(await processPR(num)) });
}
}
core.summary.addRaw('```json\n' + JSON.stringify(results, null, 2) + '\n```');
await core.summary.write();
uses: MervinPraison/github-automation-template/.github/workflows/[email protected]
secrets: inherit
with:
hub_ref: v1.1.6
pr_number: ${{ github.event.inputs.pr_number || '' }}
+2
View File
@@ -5,6 +5,7 @@ on:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
python:
@@ -65,3 +66,4 @@ jobs:
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./site
force_orphan: true
+25 -469
View File
@@ -1,46 +1,30 @@
name: Claude PR Merge Gate
# Read-only Claude assesses merge readiness; GH_TOKEN merges (no @claude, no gh pr review).
# Does not overlap with merge-conflict-claude.yml or auto-pr-comment @claude chains.
on:
schedule:
# Scan for merge-ready PRs every 10 minutes (assess runs only when candidates exist).
- cron: '*/10 * * * *'
workflow_dispatch:
inputs:
pr_number:
description: 'Optional PR number (scan all open internal PRs if empty)'
required: false
type: string
merge_method:
description: 'Merge method'
required: false
type: choice
default: merge
options:
- merge
- squash
- rebase
options: [merge, squash, rebase]
workflow_run:
workflows:
- Claude Assistant
- CI
types:
- completed
- Core Tests
types: [completed]
repository_dispatch:
types:
- claude-merge-gate
types: [claude-merge-gate]
concurrency:
group: claude-merge-gate-${{ github.repository }}-${{ github.event.inputs.pr_number || github.event.client_payload.pr_number || github.event.workflow_run.id || 'scan' }}
cancel-in-progress: false
env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
MAX_CANDIDATES: 3
DISPATCH_PR_NUMBER: ${{ github.event.inputs.pr_number || github.event.client_payload.pr_number || '' }}
permissions:
contents: read
pull-requests: read
@@ -59,7 +43,9 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@v4
- uses: MervinPraison/github-automation-template/.github/actions/[email protected]
with:
hub_ref: v1.1.6
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
@@ -67,473 +53,43 @@ jobs:
app-id: ${{ secrets.CLAUDE_APP_ID }}
private-key: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- name: Dispatch merge gate when PR pipeline is ready
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
const path = require('path');
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
const mergeGate = require(path.join(process.env.HUB_SCRIPTS, 'merge-gate.js'));
const wr = context.payload.workflow_run;
const owner = context.repo.owner;
const repo = context.repo.repo;
if (wr.conclusion !== 'success') {
core.info(`Skip dispatch: ${wr.name} conclusion=${wr.conclusion}`);
return;
}
const allowed = ['Claude Assistant', 'CI'];
if (!allowed.includes(wr.name)) {
core.info(`Skip dispatch: workflow ${wr.name}`);
return;
}
if (wr.conclusion !== 'success') return;
const allowed = ['Claude Assistant', 'Core Tests'];
if (!allowed.includes(wr.name)) return;
const prNumber = await mergeGate.resolvePrNumberFromWorkflowRun(github, owner, repo, wr);
if (!prNumber) {
core.info('Skip dispatch: could not resolve PR from workflow run');
return;
}
if (!prNumber) return;
const fromClaude = wr.name === 'Claude Assistant';
const check = await mergeGate.evaluatePipelineQuiescent(
github, owner, repo, prNumber, core, {
skipGlobalClaudeRunCheck: fromClaude,
skipRecentClaudeCooldown: fromClaude,
}
);
if (!check.ready) {
core.info(`PR #${prNumber} not ready: ${check.reasons.join(', ')}`);
return;
}
const check = await mergeGate.evaluatePipelineQuiescent(github, owner, repo, prNumber, core, {
skipGlobalClaudeRunCheck: fromClaude,
skipRecentClaudeCooldown: fromClaude,
});
if (!check.ready) return;
await github.rest.repos.createDispatchEvent({
owner,
repo,
event_type: 'claude-merge-gate',
owner, repo, event_type: 'claude-merge-gate',
client_payload: { pr_number: prNumber },
});
core.info(`Dispatched merge gate for PR #${prNumber} after ${wr.name}`);
scan-candidates:
merge-gate:
if: github.event_name != 'workflow_run'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: write
actions: read
id-token: write
outputs:
matrix: ${{ steps.scan.outputs.matrix }}
has_candidates: ${{ steps.scan.outputs.has_candidates }}
steps:
- uses: actions/checkout@v4
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.CLAUDE_APP_ID }}
private-key: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- name: Scan open PRs for merge gate eligibility
id: scan
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
const path = require('path');
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
const ps = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pipeline-status.js'));
const owner = context.repo.owner;
const repo = context.repo.repo;
const maxCandidates = parseInt(process.env.MAX_CANDIDATES || '5', 10);
const inputPr = String(process.env.DISPATCH_PR_NUMBER || '').trim();
const candidates = [];
const skipped = [];
if (inputPr) {
const n = parseInt(inputPr, 10);
if (Number.isNaN(n)) {
throw new Error(`Invalid pr_number input: ${inputPr}`);
}
const result = await mergeGate.evaluatePipelineQuiescent(github, owner, repo, n, core);
if (result.ready) {
candidates.push({ pr_number: n, head_sha: result.headSha });
} else {
skipped.push({ pr: n, reasons: result.reasons });
}
} else {
const prNumbers = await mergeGate.listPrNumbersForMergeGateScan(
github, owner, repo, core
);
const selected = await mergeGate.selectMergeGateCandidates(
github, owner, repo, prNumbers, maxCandidates, core
);
candidates.push(...selected.candidates);
skipped.push(...selected.skipped);
}
core.info('Skipped: ' + JSON.stringify(skipped));
core.setOutput('matrix', JSON.stringify({ include: candidates }));
core.setOutput('has_candidates', candidates.length > 0 ? 'true' : 'false');
core.summary.addRaw('## Merge gate scan\n');
core.summary.addRaw(`Candidates: ${candidates.length}\n`);
core.summary.addRaw('```json\n' + JSON.stringify({ candidates, skipped }, null, 2) + '\n```');
await core.summary.write();
if (inputPr && skipped.length === 1 && skipped[0].pr === parseInt(inputPr, 10)) {
const reasons = skipped[0].reasons;
const body = [
'**Merge gate scan** — not eligible for auto-merge.',
'',
...reasons.map((r) => `- \`${r}\``),
'',
'**Actions:** wait for CI and the Claude review chain, or add label `needs-manual-review` and merge manually.',
'**Opt out:** label `no-auto-merge`.',
].join('\n');
await github.rest.issues.createComment({
owner, repo, issue_number: skipped[0].pr, body,
});
} else if (inputPr && candidates.length === 1) {
await github.rest.issues.createComment({
owner, repo, issue_number: candidates[0].pr_number,
body: '**Merge gate scan** — eligible for assessment. Claude merge gate will assess and may auto-merge if `MERGE_GATE_VERDICT: APPROVE`.',
});
}
await ps.syncOpenPullRequests(github, owner, repo, { maxPrs: 10 }, core);
claude-assess:
needs: scan-candidates
if: needs.scan-candidates.outputs.has_candidates == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.scan-candidates.outputs.matrix) }}
permissions:
contents: read
pull-requests: read
issues: write
actions: read
id-token: write
steps:
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.CLAUDE_APP_ID }}
private-key: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
- name: Mark merge gate active
id: mark_active
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
core.setOutput('started_at', new Date().toISOString());
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: ${{ matrix.pr_number }},
labels: ['claude-merge-gate-active'],
});
- name: Stash merge gate helpers
run: |
# Keep scripts together so require('./gate-config.js') works from /tmp
mkdir -p /tmp/merge-gate-scripts /tmp/gh-actions
cp .github/scripts/merge-gate.js .github/scripts/gate-config.js /tmp/merge-gate-scripts/
cp -R .github/actions/claude-code-action /tmp/gh-actions/
- name: Fetch PR branch
env:
PR_NUMBER: ${{ matrix.pr_number }}
run: |
git fetch origin "pull/${PR_NUMBER}/head:pr-${PR_NUMBER}"
git checkout "pr-${PR_NUMBER}"
mkdir -p .github/actions
rm -rf .github/actions/claude-code-action
cp -R /tmp/gh-actions/claude-code-action .github/actions/
- name: Claude merge gate assessment (read-only)
id: assess
uses: ./.github/actions/claude-code-action
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
CLAUDE_CODE_SUBAGENT_MODEL: inherit
ANTHROPIC_DEFAULT_SONNET_MODEL: claude-sonnet-4-6
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github_token: ${{ steps.app-token.outputs.token }}
model: claude-opus-4-8
trigger_phrase: '@claude-merge-gate-internal'
direct_prompt: |
You are the MERGE GATE reviewer for PraisonAIUI PR #${{ matrix.pr_number }}.
READ-ONLY: do not modify code, commit, push, rebase, or create branches.
CRITICAL OUTPUT RULE:
Post exactly one PR comment via GitHub MCP containing either:
MERGE_GATE_VERDICT: APPROVE
or
MERGE_GATE_VERDICT: BLOCK
followed by a short rationale on the next lines.
NEVER include @claude in any comment. NEVER mention merge conflict resolution.
Review checklist:
1. Read AGENTS.md and the PR diff on the current branch.
2. Read all PR comments and reviews (FINAL architecture reviewer / Lead Engineer must have run).
3. Confirm product value gate: change strengthens PraisonAIUI UX (no scope creep). The aim is a LIGHTWEIGHT AND POWERFUL package — BLOCK changes that add params/modules/exports duplicating existing UI capabilities (YAML config, plugins, feature modules, existing server hooks), or add knobs/fields with no live consumer, i.e. scope creep for the sake of it.
4. Layering routing: BLOCK if core agent logic was added here instead of praisonaiagents; BLOCK if agent-callable tools were added here instead of PraisonAI-Tools.
5. Do not bloat server.py or create_app() with additional params — only if absolutely required; prefer existing config/plugin patterns.
6. If server.py or create_app() adds >100 lines or new top-level params without a clear live consumer, respond MERGE_GATE_VERDICT: BLOCK (manual review required).
7. BLOCK if labels security/breaking-change/needs-manual-review/release, sensitive paths (.github/workflows, auth, pyproject.toml), secrets in diff, PR >800 lines or >30 files, or product code changes without tests. Label `merge-gate-ci-only` exempts CI-only changes under `.github/workflows/`, `.github/actions/`, or merge-gate scripts.
8. Confirm no CHANGES_REQUESTED reviews and CI green on HEAD ${{ matrix.head_sha }}.
9. If ready to merge: MERGE_GATE_VERDICT: APPROVE. Otherwise: MERGE_GATE_VERDICT: BLOCK with reasons.
Do not approve or merge via GitHub UI/API — only post the verdict comment.
disallowed_tools: |
Bash
Edit
Write
Replace
allowed_tools: |
View
GlobTool
GrepTool
Read
timeout_minutes: 15
- name: Post fallback BLOCK if Opus did not comment
id: fallback_verdict
if: steps.assess.outcome != 'success'
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
const mergeGate = require('/tmp/merge-gate-scripts/merge-gate.js');
const owner = context.repo.owner;
const repo = context.repo.repo;
const prNumber = ${{ matrix.pr_number }};
const ctx = await mergeGate.loadPrContext(github, owner, repo, prNumber);
const minCreatedAt = new Date(Date.now() - 25 * 60 * 1000).toISOString();
const existing = mergeGate.findMergeGateVerdict(
ctx.comments, minCreatedAt, ctx.headPushedAt, { excludeAutomatedFallback: true }
);
if (existing) {
core.info(`Opus verdict already posted: ${existing}`);
return;
}
const check = await mergeGate.evaluatePipelineQuiescent(
github, owner, repo, prNumber, core, { forMergeStep: true }
);
const lines = [
'MERGE_GATE_VERDICT: BLOCK',
'',
'Automated fallback — Opus merge gate assessment did not complete.',
];
if (check.ready) {
lines.push('Deterministic gates passed, but Opus MERGE_GATE_VERDICT is required before merge.');
} else {
lines.push('Blockers:', ...check.reasons.map((r) => `- ${r}`));
}
await github.rest.issues.createComment({
owner, repo, issue_number: prNumber,
body: lines.join('\n'),
});
core.info('Posted fallback BLOCK — Opus verdict required');
- name: Require Opus MERGE_GATE_VERDICT
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
const mergeGate = require('/tmp/merge-gate-scripts/merge-gate.js');
const comments = await mergeGate.listAllComments(
github, context.repo.owner, context.repo.repo, ${{ matrix.pr_number }}
);
const ctx = await mergeGate.loadPrContext(
github, context.repo.owner, context.repo.repo, ${{ matrix.pr_number }}
);
const minCreatedAt = new Date(Date.now() - 25 * 60 * 1000).toISOString();
const verdict = mergeGate.findMergeGateVerdict(
comments, minCreatedAt, ctx.headPushedAt, { excludeAutomatedFallback: true }
);
if (!verdict) {
core.setFailed(
'Opus merge gate assessment must post MERGE_GATE_VERDICT on this PR (automated fallback APPROVE is not accepted).'
);
return;
}
core.info(`Opus merge gate verdict: ${verdict}`);
core.setOutput('verdict', verdict);
- name: Remove merge gate active label
if: always()
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
const prNumber = ${{ matrix.pr_number }};
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
name: 'claude-merge-gate-active',
});
core.info(`Removed claude-merge-gate-active from PR #${prNumber}`);
} catch (e) {
core.info(`Label claude-merge-gate-active already removed or missing on PR #${prNumber}`);
}
- name: Fail job if assessment failed
if: steps.assess.outcome == 'failure'
run: exit 1
merge-only:
needs: [scan-candidates, claude-assess]
if: |
always() &&
needs.scan-candidates.outputs.has_candidates == 'true' &&
needs.claude-assess.result == 'success'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.scan-candidates.outputs.matrix) }}
permissions:
contents: write
pull-requests: write
issues: write
actions: read
id-token: write
steps:
- uses: actions/checkout@v4
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.CLAUDE_APP_ID }}
private-key: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- name: Re-check gates and merge
uses: actions/github-script@v7
env:
MERGE_METHOD: ${{ github.event.inputs.merge_method || 'merge' }}
SCAN_HEAD_SHA: ${{ matrix.head_sha }}
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
const path = require('path');
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
const owner = context.repo.owner;
const repo = context.repo.repo;
const prNumber = ${{ matrix.pr_number }};
const scanHeadSha = process.env.SCAN_HEAD_SHA;
const removeLabels = async () => {
try {
await github.rest.issues.removeLabel({
owner, repo, issue_number: prNumber, name: 'claude-merge-gate-active',
});
} catch (e) {
core.info('Label claude-merge-gate-active already removed or missing');
}
};
try {
const ctx = await mergeGate.loadPrContext(github, owner, repo, prNumber);
const comments = ctx.comments;
const verdictMin = new Date(Date.now() - 25 * 60 * 1000).toISOString();
const verdict = mergeGate.findMergeGateVerdict(
comments, verdictMin, ctx.headPushedAt, { excludeAutomatedFallback: true }
);
if (verdict !== 'APPROVE') {
core.info(`Skip merge: verdict=${verdict || 'NONE'}`);
await removeLabels();
return;
}
const check = await mergeGate.evaluatePipelineQuiescent(
github, owner, repo, prNumber, core, { forMergeStep: true }
);
if (!check.ready) {
core.info(`Skip merge: ${check.reasons.join(', ')}`);
await removeLabels();
return;
}
if (check.headSha !== scanHeadSha) {
core.info(`Skip merge: HEAD moved ${scanHeadSha.slice(0, 7)} → ${check.headSha.slice(0, 7)}`);
await removeLabels();
return;
}
const method = process.env.MERGE_METHOD || 'merge';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
let merged = false;
for (let attempt = 1; attempt <= 3; attempt++) {
try {
await github.rest.pulls.merge({
owner, repo, pull_number: prNumber, merge_method: method,
});
merged = true;
break;
} catch (e) {
const msg = (e.message || '').toLowerCase();
if (attempt < 3 && msg.includes('base branch was modified')) {
core.info(`Merge attempt ${attempt} failed (base moved), retrying in 8s...`);
await sleep(8000);
const recheck = await mergeGate.evaluatePipelineQuiescent(
github, owner, repo, prNumber, core, { forMergeStep: true }
);
if (!recheck.ready) {
core.info(`Skip merge retry: ${recheck.reasons.join(', ')}`);
await removeLabels();
return;
}
continue;
}
throw e;
}
}
if (!merged) {
core.info('Skip merge: exhausted retries');
await removeLabels();
return;
}
await github.rest.issues.addLabels({
owner, repo, issue_number: prNumber, labels: ['auto-merged-by-gate'],
});
await github.rest.issues.createComment({
owner, repo, issue_number: prNumber,
body: [
'Merged by **Claude PR merge gate** (`claude-merge-gate.yml`).',
`Verdict: MERGE_GATE_VERDICT: APPROVE`,
`SHA: \`${check.headSha.slice(0, 7)}\``,
`Method: ${method}`,
].join('\n'),
});
core.info(`Merged PR #${prNumber}`);
} finally {
await removeLabels();
}
uses: MervinPraison/github-automation-template/.github/workflows/[email protected]
secrets: inherit
with:
hub_ref: v1.1.6
pr_number: ${{ github.event.inputs.pr_number || github.event.client_payload.pr_number || '' }}
merge_method: ${{ github.event.inputs.merge_method || 'merge' }}
+6 -539
View File
@@ -1,557 +1,24 @@
name: Claude Assistant
# Triggers:
# - issues:opened → lightweight triage (labels + auto-claude for owner)
# - issues:labeled → full implementation when `claude` label applied
# - issues:assigned → full implementation when issue assigned to Claude
# - issue_comment → full implementation when @claude mentioned (issues + PRs)
# - pull_request_review → full implementation when @claude mentioned in review
# - pull_request_review_comment → full implementation when @claude mentioned inline
#
# Escalate an issue with the `claude` label OR an @claude comment — not both.
# If both fire within 3 minutes, the comment trigger is skipped (dedupe job).
#
# Focus: CLAUDE only. Copilot and CodeRabbit are GitHub Apps that react to PRs
# automatically — no workflow file needed. auto-pr-comment.yml orchestrates the
# CodeRabbit → Copilot → Claude review chain for PRs.
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, labeled, assigned]
types: [assigned, labeled, opened]
pull_request_review:
types: [submitted]
workflow_dispatch:
inputs:
issue_number:
description: 'Issue number for Claude implementation'
required: true
type: string
jobs:
# ================================================================
# Job 1 — Issue triage (runs on ANY new issue, including external users)
# Posts welcome comment + triage labels. Auto-adds `claude` for owner issues.
# ================================================================
issue-triage:
if: |
github.event_name == 'issues' &&
github.event.action == 'opened' &&
!contains(toJSON(github.event.issue.labels), 'claude')
runs-on: ubuntu-latest
permissions:
issues: write
contents: read
steps:
- name: Generate GitHub App Token
id: app-token
continue-on-error: true
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.CLAUDE_APP_ID }}
private-key: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- name: Resolve GitHub token
id: github-token
run: |
if [ -n "${{ steps.app-token.outputs.token }}" ]; then
echo "token=${{ steps.app-token.outputs.token }}" >> "$GITHUB_OUTPUT"
else
echo "token=${{ secrets.GITHUB_TOKEN }}" >> "$GITHUB_OUTPUT"
fi
- name: Triage and acknowledge issue
uses: actions/github-script@v7
with:
github-token: ${{ steps.github-token.outputs.token }}
script: |
const issue = context.payload.issue;
const title = (issue.title || '').toLowerCase();
const body = (issue.body || '').toLowerCase();
const content = title + ' ' + body;
const isOwner = ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(
issue.author_association
);
const labels = [];
if (content.includes('feature request') || content.includes('[feature')) {
labels.push('enhancement');
} else if (content.includes('bug') || content.includes('error') || content.includes('crash') || content.includes('traceback')) {
labels.push('bug');
} else if (content.includes('question') || content.includes('how do i') || content.includes('how to')) {
labels.push('question');
}
if (content.includes('security') || content.includes('cve') || content.includes('vulnerability')) {
labels.push('security');
}
if (content.includes('performance') || content.includes('slow') || content.includes('memory leak')) {
labels.push('performance');
}
if (content.includes('documentation') || content.includes('docs')) {
labels.push('documentation');
}
if (content.includes('typescript') || content.includes('javascript') || content.includes('npm')) {
labels.push('javascript');
}
if (content.includes('frontend') || content.includes('react') || content.includes('vite')) {
labels.push('frontend');
}
if (content.includes('example') || content.includes('yaml')) {
labels.push('examples');
}
if (labels.length > 0) {
await github.rest.issues.addLabels({
issue_number: issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels
});
}
if (!isOwner) {
await github.rest.issues.createComment({
issue_number: issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: [
`👋 Thanks for opening this issue, @${issue.user.login}!`,
'',
'A maintainer will review this shortly. In the meantime:',
'- Make sure you\'ve included steps to reproduce (for bugs)',
'- Check [existing issues](https://github.com/MervinPraison/PraisonAIUI/issues) for duplicates',
'- Review [AGENTS.md](https://github.com/MervinPraison/PraisonAIUI/blob/main/AGENTS.md) for project conventions',
'',
'_A maintainer can trigger deeper analysis by commenting `@claude` on this issue._'
].join('\n')
});
}
if (isOwner) {
await github.rest.issues.addLabels({
issue_number: issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['claude']
});
}
# ================================================================
# Job 2 — Full Claude Code implementation
# Triggers on: labeled `claude`, @claude comments, assignments
#
# Dedup: label + @claude comment on the same issue fires two events.
# Skip the comment trigger when `claude` was labelled within 3 minutes.
# ================================================================
claude-dedupe:
if: |
github.event_name == 'workflow_dispatch' ||
(
github.event.action != 'opened' &&
(github.event.action != 'labeled' || github.event.label.name == 'claude') &&
(github.event_name != 'issue_comment' || contains(github.event.comment.body, '@claude')) &&
(
!contains(github.actor, '[bot]') ||
github.actor == 'github-actions[bot]' ||
github.actor == 'praisonai-triage-agent[bot]'
) &&
github.actor != 'dependabot[bot]' &&
github.actor != 'cursor[bot]' &&
github.actor != 'renovate[bot]'
)
runs-on: ubuntu-latest
outputs:
proceed: ${{ steps.check.outputs.proceed }}
permissions:
issues: read
pull-requests: read
steps:
- name: Skip duplicate comment after fresh claude label
id: check
uses: actions/github-script@v7
with:
script: |
if (context.eventName === 'workflow_dispatch') {
core.setOutput('proceed', 'true');
return;
}
if (context.eventName !== 'issue_comment' || context.payload.issue?.pull_request) {
core.setOutput('proceed', 'true');
return;
}
const issue = context.payload.issue;
const hasClaude = (issue.labels || []).some(l => l.name === 'claude');
if (!hasClaude) {
core.setOutput('proceed', 'true');
return;
}
const events = await github.rest.issues.listEvents({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
per_page: 30,
});
const windowMs = 3 * 60 * 1000;
const recentLabel = events.data.find(e =>
e.event === 'labeled' &&
e.label?.name === 'claude' &&
(Date.now() - new Date(e.created_at).getTime()) < windowMs
);
if (recentLabel) {
core.info('Skipping: claude label was added within 3 minutes — label trigger already handles this issue.');
core.setOutput('proceed', 'false');
return;
}
core.setOutput('proceed', 'true');
claude-response:
needs: claude-dedupe
env:
ISSUE_NUMBER_RESOLVED: ${{ github.event.issue.number || github.event.pull_request.number || inputs.issue_number }}
if: needs.claude-dedupe.outputs.proceed == 'true'
concurrency:
group: claude-${{ github.event.issue.number || github.event.pull_request.number || inputs.issue_number || github.run_id }}
cancel-in-progress: true
runs-on: ubuntu-latest
claude:
permissions:
contents: write
pull-requests: write
issues: write
actions: read
id-token: write
steps:
- name: Generate GitHub App Token
id: app-token
continue-on-error: true
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.CLAUDE_APP_ID }}
private-key: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- name: Resolve GitHub token
id: github-token
run: |
if [ -n "${{ steps.app-token.outputs.token }}" ]; then
echo "token=${{ steps.app-token.outputs.token }}" >> "$GITHUB_OUTPUT"
else
echo "token=${{ secrets.GITHUB_TOKEN }}" >> "$GITHUB_OUTPUT"
fi
- name: Check for Fork PR
id: check_fork
uses: actions/github-script@v7
with:
github-token: ${{ steps.github-token.outputs.token }}
script: |
const isPR = context.eventName === 'pull_request' ||
context.eventName === 'pull_request_review' ||
context.eventName === 'pull_request_review_comment' ||
(context.payload.issue && context.payload.issue.pull_request);
if (isPR) {
core.setOutput('is_pr_review', 'true');
const prNumber = context.payload.pull_request
? context.payload.pull_request.number
: context.payload.issue.number;
core.setOutput('pr_number', String(prNumber));
const pr = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});
core.setOutput('pr_branch', pr.data.head.ref);
const headRepo = pr.data.head.repo?.full_name || context.repo.full_name;
core.setOutput('head_repo', headRepo);
let mergeState = '';
try {
const gql = await github.graphql(
`query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) { mergeStateStatus }
}
}`,
{ owner: context.repo.owner, repo: context.repo.repo, number: prNumber }
);
mergeState = (gql.repository.pullRequest.mergeStateStatus || '').toUpperCase();
} catch (e) {
core.warning(`GraphQL mergeStateStatus failed: ${e.message}`);
}
if (!mergeState) {
mergeState = (pr.data.mergeable_state || pr.data.merge_state_status || '').toUpperCase();
}
core.setOutput('merge_state', mergeState);
if (headRepo !== `${context.repo.owner}/${context.repo.repo}`) {
core.setOutput('is_fork', 'true');
core.setOutput('clone_url', pr.data.head.repo.clone_url);
core.setOutput('branch', pr.data.head.ref);
return;
}
core.setOutput('is_fork', 'false');
return;
}
core.setOutput('is_pr_review', 'false');
core.setOutput('is_fork', 'false');
core.setOutput('merge_state', '');
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false
fetch-depth: 0
token: ${{ steps.github-token.outputs.token }}
ref: ${{ steps.check_fork.outputs.is_pr_review == 'true' && format('refs/pull/{0}/head', steps.check_fork.outputs.pr_number) || github.ref }}
- name: Checkout PR branch for edits
if: steps.check_fork.outputs.is_pr_review == 'true' && steps.check_fork.outputs.is_fork != 'true'
env:
PR_BRANCH: ${{ steps.check_fork.outputs.pr_branch }}
run: |
git checkout "${PR_BRANCH}"
git config --local user.name "MervinPraison"
git config --local user.email "[email protected]"
echo "On branch: $(git branch --show-current)"
- name: Fetch PR branch and Setup Remote
if: steps.check_fork.outputs.is_pr_review == 'true' && steps.check_fork.outputs.is_fork == 'true'
env:
PR_BRANCH: ${{ steps.check_fork.outputs.pr_branch }}
run: |
git fetch origin pull/${{ steps.check_fork.outputs.pr_number }}/head:"${PR_BRANCH}"
git checkout "${PR_BRANCH}"
git remote set-url origin file://$(pwd)
- name: Run Claude PR final review
if: steps.check_fork.outputs.is_pr_review == 'true'
uses: anthropics/claude-code-action@beta
env:
GH_TOKEN: ${{ steps.github-token.outputs.token }}
with:
allowed_bots: 'praisonai-triage-agent[bot]'
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github_token: ${{ steps.github-token.outputs.token }}
model: claude-opus-4-8
trigger_phrase: "@claude"
timeout_minutes: "30"
direct_prompt: |
${{ steps.check_fork.outputs.is_fork == 'true' && 'CRITICAL: FORK PR — comment only, do not push.\n\n' || 'PR on MervinPraison/PraisonAIUI — SAME REPO, NOT A FORK. You HAVE push permission to this branch. NEVER say this is a fork.\n\n' }}
${{ steps.check_fork.outputs.is_fork != 'true' && steps.check_fork.outputs.merge_state == 'DIRTY' && format('PRIORITY — RESOLVE MERGE CONFLICTS (branch: {0}, PR #{1}):\n- DO NOT create a new branch or PR\n- git fetch origin main && git rebase origin/main\n- Resolve conflicts: keep this PR intent, merge in main newer logic\n- git add -A && git rebase --continue (repeat until done)\n- git push --force-with-lease origin {0}\n- Run targeted pytest/ruff on changed paths\n- Comment files resolved and what you skipped\n- Only after conflicts are resolved, proceed with review phases below if still needed\n\n', steps.check_fork.outputs.pr_branch, steps.check_fork.outputs.pr_number) || '' }}
${{ steps.check_fork.outputs.merge_state != 'DIRTY' && format('CONTEXT: PR #{0} on branch {1}. merge_state={2}.\n\n', steps.check_fork.outputs.pr_number, steps.check_fork.outputs.pr_branch, steps.check_fork.outputs.merge_state) || '' }}
You are the Lead Engineer doing FINAL review on this open PR in MervinPraison/PraisonAIUI.
Read ALL reviewer comments (CodeRabbit, Gemini, Qodo, Copilot, human). Follow AGENTS.md.
SCOPE: src/praisonaiui, src/frontend, tests, docs only.
**Phase 1 — Review** architecture, security, tests, backward compat, import-time budget.
Product value gate: change must strengthen PraisonAIUI UX (no scope creep). BLOCK params/modules/exports
duplicating existing UI capabilities or knobs with no live consumer. Core agent logic belongs in
praisonaiagents; agent-callable tools in PraisonAI-Tools — not here.
**Phase 2 — FIX (mandatory)** implement every VALID issue on THIS branch. Push commits before Phase 3.
Do NOT ask the author to fix. Do NOT open a new PR. Do NOT claim fork restrictions unless is_fork is true.
**Phase 3 — Verdict** approve only after fixes are pushed and tests pass; else request changes with specifics.
git config --global user.name "MervinPraison"
git config --global user.email "[email protected]"
gh auth setup-git
git status
git branch --show-current
allowed_tools: |
Bash(git:*)
Bash(python:*)
Bash(pip:*)
Bash(pytest:*)
Bash(ruff:*)
Bash(gh:*)
Bash(npm:*)
View
GlobTool
GrepTool
BatchTool
Edit
Replace
- name: Run Claude Code Action
if: steps.check_fork.outputs.is_pr_review != 'true'
uses: anthropics/claude-code-action@beta
env:
GH_TOKEN: ${{ steps.github-token.outputs.token }}
with:
allowed_bots: 'praisonai-triage-agent[bot]'
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github_token: ${{ steps.github-token.outputs.token }}
model: claude-opus-4-8
mode: ${{ github.event_name == 'workflow_dispatch' && 'agent' || 'tag' }}
trigger_phrase: "@claude"
label_trigger: "claude"
timeout_minutes: "30"
direct_prompt: |
${{ steps.check_fork.outputs.is_fork == 'true' && 'CRITICAL: THIS IS A PULL REQUEST FROM A FORK. YOU DO NOT HAVE PUSH PERMISSIONS TO THIS REPOSITORY. ONLY READ FILES, VALIDATE CODE, AND PROVIDE YOUR COMMENTS/FEEDBACK DIRECTLY IN THIS PR VIA COMMENTS. DO NOT ATTEMPT TO PUSH OR CREATE PULL REQUESTS.\n\n' || 'NOTE: The branch is under MervinPraison/PraisonAIUI (not a fork). You are able to make modifications and push directly to this branch.\n\n' }}
You are implementing changes in **MervinPraison/PraisonAIUI** (YAML-driven server-driven UI with agentic chat).
Follow `AGENTS.md` at the repository root. If a conflict arises between this prompt and `AGENTS.md`, `AGENTS.md` wins.
═══════════════════════════════════════════════════════════════
STEP 0 — GIT & GITHUB CLI
═══════════════════════════════════════════════════════════════
git config --global user.name "MervinPraison"
git config --global user.email "[email protected]"
gh auth setup-git
═══════════════════════════════════════════════════════════════
STEP 1 — READ GUIDELINES
═══════════════════════════════════════════════════════════════
Read `AGENTS.md` and any `docs/principles.md`. Key invariants:
• AIM: This package must stay LIGHTWEIGHT AND POWERFUL. Do a CRITICAL REVIEW at each stage
(issue triage, fix planning, PR review/implementation) and reject scope creep for the sake
of adding features. Prefer existing YAML config, plugins, feature modules, and server hooks
over new server.py/create_app() params or exports. Even when fixing a raised issue, keep the
fix minimal — do NOT expand surface area or add knobs with no live consumer.
• Cold import of `praisonaiui` must stay under 200 ms; module count must not grow.
• Heavy / optional deps (`mcp`, `langchain`, `llama_index`, `semantic_kernel`,
`slack_sdk`, `discord.py`, `botbuilder.core`, `openai`, `anthropic`, `mistralai`,
`google.generativeai`) must be **lazy-imported** — never eager.
• Public API is verb-first. New classes in `src/praisonaiui/message.py` ending
in `Message` with a verb prefix (Ask*, Do*, Request*, …) are forbidden —
use a module-level `async def` verb helper instead (see `prompt`, `error`).
• Deprecations must emit a runtime `warnings.warn(..., DeprecationWarning, stacklevel=2)`
on first access, not only in the docstring.
• Deterministic serialisation in anything reaching the wire (sorted keys, stable field
order). See existing `to_dict` patterns.
• Multi-agent + async safe: no global mutable state without a lock.
═══════════════════════════════════════════════════════════════
STEP 2 — ARCHITECTURE VALIDATION & ROUTING (MANDATORY before any code)
═══════════════════════════════════════════════════════════════
Answer these questions inline in your response, then proceed only if you pass:
ROUTING:
1. **Core aiui** (`src/praisonaiui/`) — protocols, message types, server, features.
2. **Features** (`src/praisonaiui/features/`) — opt-in capabilities (MCP, platforms, lifecycle, audio, auth, sharing, etc.).
3. **UI builders** (`src/praisonaiui/ui.py`) — declarative component helpers.
4. **Frontend** (`src/frontend/`) — React components / stores / SSE handlers.
5. **Tests** (`tests/unit/`, `tests/integration/`) — new features require both smoke and real-agentic tests.
6. **Docs** (`docs/`) — canonical name per feature, migration table for renames.
EXTERNAL REPOS (do NOT edit from within this repo — route the work to the right place and stop):
• Core SDK: `MervinPraison/PraisonAI` (`praisonaiagents`, `praisonai`) — agent runtime.
• Tools: `MervinPraison/PraisonAI-Tools` — tool implementations.
• Docs site: `MervinPraison/PraisonAIDocs` — product docs.
If the requested change belongs in an external repo, comment on the issue explaining
where it should live and DO NOT open a PR in this repo.
CONCEPTUAL CHECKS (if ANY fails — add a comment, do NOT open a PR):
• Does it duplicate an existing public symbol in `src/praisonaiui/__init__.py`?
• Does it duplicate existing UI capability via YAML config, plugins, or feature modules
instead of adding new server params? → Reject as scope creep; prefer existing patterns.
• Does it add core agent runtime logic here instead of praisonaiagents, or agent-callable
tools here instead of PraisonAI-Tools? → Route externally; do not implement here.
• Does it bloat server.py or create_app() with new params without a clear live consumer?
→ Prefer existing config/plugin hooks; only add params if absolutely required.
• Does it add new class-first message types (`class Ask*Message`, `class *Message`
with a verb in the name)? → Use a verb-first helper instead.
• Does it add a new hard dependency to `pyproject.toml` `[project] dependencies`?
→ Move to `[project.optional-dependencies]` and wrap imports lazily.
• Does it break backward compat (rename, signature change, removal)? → Add
a deprecation alias that emits `DeprecationWarning`; never hard-remove before the
documented removal version (e.g. `0.5.0`).
═══════════════════════════════════════════════════════════════
STEP 3 — IMPLEMENT
═══════════════════════════════════════════════════════════════
Single branch for the whole flow (reuse the same name in STEP 5):
BRANCH="claude/issue-${{ env.ISSUE_NUMBER_RESOLVED }}-$(date +%Y%m%d-%H%M%S)"
git checkout -b "$BRANCH"
Make minimal, focused changes:
• Stay within the files listed in the issue's "Scope" / "Files to change" section.
• Prefer extending existing modules over adding new ones.
• No emojis, no comments, no markdown except where the user asked for it.
• Python ≥ 3.10 style (`list[...]` not `List[...]`, `X | None` not `Optional[X]`).
═══════════════════════════════════════════════════════════════
STEP 4 — TEST / VERIFY
═══════════════════════════════════════════════════════════════
Run these targeted checks (NOT the full suite):
# 1. Lint on only the files you changed:
CHANGED=$(git diff --name-only origin/main...HEAD | grep -E '\.py$' | tr '\n' ' ')
[ -n "$CHANGED" ] && ruff check $CHANGED
# 2. Unit tests for the module(s) you touched:
pytest tests/unit/<your_new_test_file>.py -v --tb=short
# 3. Import-time invariant (MUST stay under 200 ms cold):
python -c "import time, sys; t=time.time(); import praisonaiui; dt=(time.time()-t)*1000; heavy=[m for m in sys.modules if any(h in m for h in ['langchain','llama_index','mcp','slack','discord','botbuilder','openai.','anthropic.','mistralai','google.generativeai'])]; print(f'{dt:.1f}ms modules={len(sys.modules)} leaked={heavy}'); assert heavy == [], f'Heavy deps leaked: {heavy}'"
If anything fails, fix the root cause (never skip, xfail, or delete the test).
═══════════════════════════════════════════════════════════════
STEP 5 — COMMIT, PUSH, OPEN PR (MANDATORY if you produced commits)
═══════════════════════════════════════════════════════════════
git add -A
git commit -m "feat: <short summary> (fixes #${{ env.ISSUE_NUMBER_RESOLVED }})"
git push -u origin "$BRANCH"
REPO="${{ github.repository }}"
COUNT=$(gh pr list --repo "$REPO" --head "$BRANCH" --json number --jq 'length')
if [ "$COUNT" -gt 0 ]; then
gh pr list --repo "$REPO" --head "$BRANCH" --json url --jq '.[0].url'
else
gh pr create --repo "$REPO" --base main --head "$BRANCH" \
--title "feat: <short title>" \
--body "Fixes #${{ env.ISSUE_NUMBER_RESOLVED }}
## Summary
<what changed in one paragraph>
## Acceptance criteria
<copy from the issue; tick boxes only with commit SHA + file path evidence>
## Test evidence
\`\`\`
<paste pytest output>
\`\`\`
"
fi
CRITICAL: Do NOT stop after `git push`. You MUST complete `gh pr create` (or print the
existing PR URL from the step above) and post that URL on the issue. Never ask a
human to open the PR manually.
═══════════════════════════════════════════════════════════════
STEP 6 — RESPOND TO REVIEW FEEDBACK (if this run was triggered by a review comment)
═══════════════════════════════════════════════════════════════
If you were invoked by a PR review comment, you are the Lead Engineer for this PR:
• Read ALL reviewer comments in chronological order (CodeRabbit → Copilot → human).
• Fix every VALID issue by pushing commits to THIS branch (do NOT open a new PR).
• Reply with a summary of which comments you addressed, which you disagreed with (and why).
• Never ship a fix that removes/weakens a test.
allowed_tools: |
Bash(git:*)
Bash(python:*)
Bash(pip:*)
Bash(pytest:*)
Bash(ruff:*)
Bash(gh:*)
Bash(npm:*)
Bash(npx:*)
Bash(python -m pytest:*)
Bash(python -m pip:*)
Bash(python -m ruff:*)
View
GlobTool
GrepTool
BatchTool
Edit
Replace
mcp__github__get_issue
mcp__github__get_issue_comments
mcp__github__update_issue
uses: MervinPraison/github-automation-template/.github/workflows/[email protected]
secrets: inherit
with:
hub_ref: v1.1.6
+41
View File
@@ -0,0 +1,41 @@
name: Deploy Docs
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: write
pages: write
id-token: write
concurrency:
group: deploy-docs
cancel-in-progress: true
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install AIUI
run: pip install -e .
- name: Build docs with AIUI
run: |
aiui build --output site --minify
cp CNAME site/CNAME
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./site
force_orphan: true
+17 -136
View File
@@ -1,7 +1,5 @@
name: Auto Merge Conflict → Claude
# Detects internal PRs with mergeStateStatus=DIRTY and posts @claude to trigger claude.yml.
on:
push:
branches: [main]
@@ -16,7 +14,6 @@ concurrency:
cancel-in-progress: true
env:
# PAT posts as MervinPraison so issue_comment triggers claude.yml (GITHUB_TOKEN cannot chain workflows)
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
permissions:
@@ -27,145 +24,29 @@ jobs:
detect-and-trigger:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: MervinPraison/github-automation-template/.github/actions/[email protected]
with:
hub_ref: v1.1.6
- name: Detect conflicting PRs and trigger Claude
uses: actions/github-script@v7
with:
github-token: ${{ env.GH_TOKEN }}
script: |
const path = require('path');
const mg = require(path.join(process.env.HUB_SCRIPTS, 'merge-gate.js'));
const owner = context.repo.owner;
const repo = context.repo.repo;
const baseRepo = `${owner}/${repo}`;
const COOLDOWN_MS = 12 * 60 * 60 * 1000;
const AUTO_ACTORS = ['github-actions[bot]', 'MervinPraison'];
const LABEL = 'claude-conflict-pending';
const COMMENT_BODY = [
'@claude this PR has merge conflicts with `main`.',
'Please rebase onto latest `main`, resolve conflicts (keep this PR\'s intent, merge in newer main logic),',
'run targeted tests, and force-push with `--force-with-lease`.',
'Comment which files you resolved.',
'Do not bloat server.py or create_app() with new params — prefer existing YAML config, plugins, and feature modules unless absolutely required.',
'Keep the resolution minimal — the aim is a lightweight and powerful UI package; do not introduce scope creep while resolving conflicts.',
].join(' ');
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function getMergeState(prNumber) {
const query = `
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
mergeStateStatus
maintainerCanModify
isDraft
headRef { repository { nameWithOwner } }
}
}
}
`;
for (let attempt = 0; attempt < 3; attempt++) {
const result = await github.graphql(query, { owner, repo, number: prNumber });
const prGql = result.repository.pullRequest;
const status = (prGql?.mergeStateStatus || '').toUpperCase();
if (status && status !== 'UNKNOWN') {
return {
status,
isDraft: prGql.isDraft,
headRepo: prGql.headRef?.repository?.nameWithOwner,
maintainerCanModify: prGql.maintainerCanModify === true,
};
}
if (attempt < 2) await sleep(10000);
}
const pr = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
return {
status: (pr.data.mergeable_state || '').toUpperCase(),
isDraft: pr.data.draft,
headRepo: pr.data.head.repo?.full_name,
maintainerCanModify: pr.data.maintainer_can_modify === true,
};
const prs = await github.paginate(github.rest.pulls.list, { owner, repo, state: 'open', per_page: 50 });
for (const pr of prs) {
if (pr.draft) continue;
if (pr.head?.repo?.full_name !== baseRepo) continue;
const state = await mg.getMergeState(github, owner, repo, pr.number);
if (state.status !== 'DIRTY') continue;
const { data: comments } = await github.rest.issues.listComments({ owner, repo, issue_number: pr.number, per_page: 100 });
if (mg.hasRecentConflictComment(comments)) continue;
const body = '@claude this PR has merge conflicts with `main`. Rebase onto latest main, resolve conflicts, run tests, force-push with --force-with-lease.';
await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body });
await github.rest.issues.addLabels({ owner, repo, issue_number: pr.number, labels: ['claude-conflict-pending'] });
}
function hasRecentAutoComment(comments) {
const cutoff = Date.now() - COOLDOWN_MS;
return comments.some((c) => {
if (!AUTO_ACTORS.includes(c.user.login)) return false;
const body = (c.body || '').toLowerCase();
if (!body.includes('@claude') || !body.includes('merge conflict')) return false;
return new Date(c.created_at).getTime() > cutoff;
});
}
async function processPR(prNumber) {
const { status, isDraft, headRepo, maintainerCanModify } = await getMergeState(prNumber);
core.info(`PR #${prNumber}: mergeState=${status}, head=${headRepo}, draft=${isDraft}, maintainerCanModify=${maintainerCanModify}`);
const { data: issue } = await github.rest.issues.get({
owner,
repo,
issue_number: prNumber,
});
const labels = issue.labels.map((l) => l.name);
if (status !== 'DIRTY' && labels.includes(LABEL)) {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: prNumber,
name: LABEL,
});
core.info(`Removed ${LABEL} from PR #${prNumber}`);
}
if (status !== 'DIRTY') return { skipped: true, reason: 'not dirty' };
if (isDraft) return { skipped: true, reason: 'draft' };
if (headRepo && headRepo !== baseRepo && !maintainerCanModify) {
return { skipped: true, reason: 'fork without maintainer edits' };
}
if (labels.includes(LABEL)) {
return { skipped: true, reason: 'conflict resolution pending' };
}
const { data: comments } = await github.rest.issues.listComments({
owner,
repo,
issue_number: prNumber,
per_page: 100,
});
if (hasRecentAutoComment(comments)) {
return { skipped: true, reason: 'recent auto comment' };
}
await github.rest.issues.addLabels({
owner,
repo,
issue_number: prNumber,
labels: [LABEL],
});
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: COMMENT_BODY,
});
return { triggered: true };
}
let prNumbers = [];
if (context.eventName === 'pull_request') {
prNumbers = [context.payload.pull_request.number];
} else {
const prs = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: 'open',
per_page: 100,
});
prNumbers = prs.map((p) => p.number);
}
const results = [];
for (const num of prNumbers) {
results.push({ pr: num, ...(await processPR(num)) });
}
core.summary.addRaw('```json\n' + JSON.stringify(results, null, 2) + '\n```');
await core.summary.write();
+4 -27
View File
@@ -1,7 +1,5 @@
name: Pipeline Status Sync
# Sync pipeline/* stage and blocker labels on open internal PRs.
on:
schedule:
- cron: '*/10 * * * *'
@@ -15,28 +13,7 @@ permissions:
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.CLAUDE_APP_ID }}
private-key: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- name: Sync pipeline labels on open PRs
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
const path = require('path');
const ps = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pipeline-status.js'));
await ps.syncOpenPullRequests(
github, context.repo.owner, context.repo.repo,
{ maxPrs: 30, dispatchMergeGate: true }, core
);
uses: MervinPraison/github-automation-template/.github/workflows/[email protected]
secrets: inherit
with:
hub_ref: v1.1.6
+1
View File
@@ -44,6 +44,7 @@ Thumbs.db
# Project specific
aiui/
*.log
*.err
.praison/
.gemini/
.internal/
+13 -4
View File
@@ -9,6 +9,14 @@ site:
radius: "md"
darkMode: true
seo:
siteUrl: "https://ui.praison.ai"
titleTemplate: "%s | %s"
defaultImage: "/icon.svg"
robots:
index: true
follow: true
# Mintlify-style navigation: few tabs, many groups per tab
navigation:
tabs:
@@ -73,11 +81,12 @@ navigation:
# Navbar actions
navbar:
primary:
type: "github"
href: "https://github.com/MervinPraison/PraisonAIUI"
type: "button"
label: "Get started"
href: "/docs/getting-started/quickstart"
links:
- label: "Submit Issues"
href: "https://github.com/MervinPraison/PraisonAIUI/issues"
- label: "GitHub"
href: "https://github.com/MervinPraison/PraisonAIUI"
# Footer
footer:
+54 -51
View File
@@ -1,73 +1,76 @@
# SEO
Built-in SEO features for better search engine visibility.
Built-in SEO for documentation sites — sitemap, robots, llms.txt, per-page meta tags, and crawlable static HTML.
## Automatic Meta Tags
## Configuration
PraisonAIUI generates meta tags from your configuration:
Top-level `seo` block in `aiui.template.yaml`:
```yaml
site:
title: "My Documentation"
description: "Comprehensive guides for my product"
seo:
siteUrl: "https://ui.praison.ai"
titleTemplate: "%s | %s"
defaultImage: "/icon.svg"
robots:
index: true
follow: true
twitter:
handle: "@yourhandle"
```
Generates:
```html
<title>My Documentation</title>
<meta name="description" content="Comprehensive guides for my product">
```
`siteUrl` is required for absolute canonical URLs, sitemap, and social tags.
## Page-Level Overrides
## Build outputs
Use frontmatter to override per-page:
`aiui build` automatically generates:
| File | Purpose |
|------|---------|
| `sitemap.xml` | All doc URLs with `lastmod` dates |
| `robots.txt` | Crawl rules + sitemap pointer |
| `llms.txt` | AI crawler index ([llmstxt.org](https://llmstxt.org)) |
| `llms-full.txt` | Full markdown export for AI tools |
| `docs/**/*.html` | Per-route HTML with meta tags + crawl nav |
| `.nojekyll` | GitHub Pages Jekyll bypass |
No extra flags are needed — SEO files are emitted whenever `content.docs` is configured.
## Page-level overrides
Use frontmatter on any markdown page:
```markdown
---
title: "Custom Page Title"
description: "Custom meta description for this page"
description: "Custom meta description for search snippets"
noindex: true
---
# Page Content
```
## SEO Configuration
If `description` is omitted, the first paragraph is used automatically.
## Per-page HTML includes
Each route HTML file contains:
- `<link rel="canonical">` (absolute HTTPS)
- `<meta name="description">`
- Open Graph tags (`og:title`, `og:description`, `og:url`, `og:type`, `og:image`)
- Twitter Card tags
- JSON-LD `TechArticle` schema
- Static `<nav>` with links to every doc page (for non-JS crawlers)
- `<noscript>` pre-rendered page content
## Runtime (SPA navigation)
When users navigate via the sidebar, React updates title, canonical, OG, Twitter, robots, and JSON-LD to match the current page.
## Deploy
GitHub Pages deploy runs via the **Deploy Docs** workflow on every push to `main`. Submit your sitemap in [Google Search Console](https://search.google.com/search-console):
```yaml
site:
seo:
titleTemplate: "%s | My Site"
defaultImage: "/og-image.png"
twitterHandle: "@myhandle"
```
## Open Graph
Automatic Open Graph tags for social sharing:
```html
<meta property="og:title" content="Page Title">
<meta property="og:description" content="Page description">
<meta property="og:image" content="/og-image.png">
<meta property="og:type" content="website">
```
## Sitemap
Generate a sitemap during build:
```bash
aiui build --sitemap
```
Creates `sitemap.xml` with all pages.
## Robots.txt
```yaml
site:
seo:
robots:
index: true
follow: true
https://your-domain.com/sitemap.xml
```
@@ -0,0 +1,34 @@
# Meeting Agent example — copy to .env and fill in. Never commit real secrets.
# Used by the PraisonAI agent for summarization/embeddings.
OPENAI_API_KEY=
# Optional: enable Slack summary delivery via the built-in SlackChannelAdapter.
SLACK_BOT_TOKEN=
SLACK_APP_TOKEN=
# Local dev server port for `python app.py`.
MEETING_AGENT_PORT=8000
# ── Recall.ai (EU workspace: Praison) ───────────────────────────────────────
# Region must match dashboard: eu-central-1 (Frankfurt).
RECALL_REGION=eu-central-1
# Optional: your Recall workspace ID (shown in the config-status API only; the
# API key already scopes requests to a workspace).
RECALL_WORKSPACE_ID=
# REST API key from Recall dashboard (Developers → API Keys). Server-side only.
RECALL_API_KEY=
# Workspace verification secret (whsec_…) from Recall MCP or dashboard.
RECALL_WEBHOOK_VERIFICATION_SECRET=
# Stable public HTTPS backend URL (reserved ngrok domain in dev). Required for
# webhooks and Google Calendar V2 OAuth callback forwarding.
PUBLIC_API_BASE_URL=
# Bot display name in meetings.
RECALL_BOT_NAME=Praison Meeting Agent
# Calendar V2: when true (default), eligible future events with video links get bots.
CALENDAR_AUTO_RECORD=true
+115
View File
@@ -0,0 +1,115 @@
# Meeting Agent (Phase 1 upload + Phase 2 Recall.ai live bots)
A PraisonAIUI example that wires the dashboard to **PraisonAI-Tools** meeting
tools for upload → transcribe → summarize → index → search, plus **Recall.ai**
Meeting Bots for live Zoom / Meet / Teams capture.
## What this example includes
| Page | Built from |
|------|-----------|
| Upload | `POST /api/meetings/upload` (audio → Phase 1 pipeline) |
| Live bot | `POST /api/recall/bots` — send a Recall bot to a meeting URL |
| Meetings | dashboard table backed by `list_meetings` |
| Meeting detail | transcript / summary / action items + live status |
| Calendar | Calendar V2 setup instructions + status API |
| Chat | meeting agent with `search_meetings` + optional `schedule_recall_bot` tool |
## Recall.ai workspace
This integration targets your Recall workspace in **EU (Frankfurt)** by default:
- Workspace ID: set `RECALL_WORKSPACE_ID` to your own (optional — the API key
already scopes requests to a workspace)
- API region: `eu-central-1`
Follow the [Recall onboarding guide](https://docs.recall.ai/docs/agent-quickstarts) via Recall MCP (`recall://guides/onboarding`) for credential and webhook setup.
## Run
```bash
cp .env.example .env # set OPENAI_API_KEY + Recall vars (server-side only)
pip install praisonai-tools[meeting] httpx
aiui run app.py
# or
python app.py # honours MEETING_AGENT_PORT
python app.py --smoke # non-binding Recall boundary smoke
```
## Schedule a live bot
```bash
curl -X POST http://127.0.0.1:8000/api/recall/bots \
-H "Content-Type: application/json" \
-d '{"meeting_url":"https://zoom.us/j/123456789","title":"Standup"}'
```
Flow: **Create Bot** → webhooks (`bot.*`, `recording.done`, `transcript.done`) →
async Recall transcription → Phase 1 summarize + index → dashboard `ready`.
## Webhooks (required)
Set `PUBLIC_API_BASE_URL` to a stable **HTTPS** backend URL (reserved ngrok
domain in dev — not localhost). Register in Recall:
- URL: `{PUBLIC_API_BASE_URL}/webhooks/recall`
- Events: `bot.joining_call`, `bot.in_call_recording`, `bot.done`, `bot.fatal`,
`recording.done`, `transcript.done`, `transcript.failed`, `transcript.data`
- Calendar (Part B): `calendar.sync_events`, `calendar.update` on the same URL
Use Recall MCP `create_webhook_endpoint` or the dashboard. Verify with
`send_test_webhook_endpoint`.
## Google Calendar V2
Default **CALENDAR_AUTO_RECORD=true** schedules bots for eligible future events
with video links once Calendar V2 is connected.
1. Set `PUBLIC_API_BASE_URL` (HTTPS tunnel).
2. Run Recall MCP `start_calendar_integration_setup` with
`production_redirect_uri={PUBLIC_API_BASE_URL}/api/recall/calendar/callback`.
3. Bootstrap the app: `POST /api/recall/calendar/setup/bootstrap` with
`setup_id` and `regional_callback_uri` from MCP.
4. Complete Google OAuth per `recall://guides/calendar-v2-setup-google`.
**Recording opt-in:** calendar sync ≠ automatic recording for every meeting;
this app uses `CALENDAR_AUTO_RECORD` (default on). Set `false` to disable
auto-schedule while keeping calendar connected.
## Environment
See `.env.example`:
| Variable | Purpose |
|----------|---------|
| `OPENAI_API_KEY` | Summaries, embeddings, chat |
| `RECALL_REGION` | `eu-central-1` |
| `RECALL_API_KEY` | Recall REST API (backend only) |
| `RECALL_WEBHOOK_VERIFICATION_SECRET` | Webhook signature verification |
| `PUBLIC_API_BASE_URL` | Public HTTPS origin for webhooks + calendar callback |
| `CALENDAR_AUTO_RECORD` | Auto-schedule bots from calendar (default `true`) |
Never commit real secrets. Reuse the existing REST key (`Mervin Praison`) when
its purpose is clear; otherwise create a purpose-named key via Recall MCP.
## Phase 2 Part B — live transcript + calendar auto-schedule
| Feature | API |
|---------|-----|
| Live transcript (SSE) | `GET /api/meetings/{id}/live-transcript` (`Accept: text/event-stream`) |
| Live transcript (poll) | Same URL, default JSON |
| Upcoming calendar events | `GET /api/recall/calendar/upcoming?hours=24` |
| Manual calendar sync | `POST /api/recall/calendar/sync` `{"calendar_id":"..."}` |
| Agent tool | `get_upcoming_meetings(hours=24)` in chat when Recall is configured |
Every new bot schedule **automatically** attaches `recording_config.realtime_endpoints`
pointing at `{PUBLIC_API_BASE_URL}/webhooks/recall` with `transcript.data` — no Recall
dashboard toggle for live transcript. Requires `PUBLIC_API_BASE_URL` in `.env`.
## Tests
From repo root:
```bash
pytest tests/unit/test_meeting_agent_example.py tests/unit/test_meeting_agent_pipeline.py tests/unit/test_meeting_agent_recall.py tests/unit/test_meeting_agent_part_b.py -v
```
@@ -0,0 +1,46 @@
# Meeting Agent — PraisonAIUI example config (PAI-MEET-UI, issue #260)
# Composes existing features (attachments, knowledge, chat, Slack) via YAML.
# Run with: aiui run app.py
site:
title: Meeting Agent
description: Upload meetings, browse summaries and action items, ask questions.
logo: "🗓️"
theme:
preset: slate
darkMode: true
radius: md
style: dashboard
layout:
mode: dashboard
dashboard:
sidebar: true
pageHeader: true
pages:
- upload
- meetings
- meeting-detail
- chat
chat:
enabled: true
name: Meeting Assistant
features:
streaming: true
reasoning: true
tools: true
fileUpload: true
feedback: true
input:
placeholder: "Ask about a past meeting..."
starters:
- label: "What did we decide about billing?"
message: "What did we decide about billing?"
icon: "🔎"
- label: "List open action items"
message: "List open action items and their owners"
icon: "✅"
+733
View File
@@ -0,0 +1,733 @@
"""Meeting Agent — PraisonAIUI example with Phase 1 upload pipeline + Recall.ai live bots.
Composes existing PraisonAIUI features plus PraisonAI-Tools meeting tools:
- Upload API → POST /api/meetings/upload (transcribe → summarize → index)
- Live bot API → POST /api/recall/bots (Recall Meeting Bot join)
- Recall webhooks → POST /webhooks/recall (verified, async processing)
- Calendar callback → GET /api/recall/calendar/callback (Calendar V2 OAuth)
- Meetings list → dashboard page backed by ``list_meetings``
- Meeting detail → latest stored meeting (transcript / summary / actions)
- Chat → PraisonAI Agent with ``search_meetings`` + meeting tools
- Slack delivery → optional via SlackChannelAdapter
Run:
aiui run app.py
python app.py
python smoke_recall.py # non-binding integration smoke
"""
from __future__ import annotations
import asyncio
import os
import sys
import tempfile
from pathlib import Path
from typing import Any
import praisonaiui as aiui
from praisonaiui.server import create_app
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.routing import Route
_EXAMPLE_DIR = Path(__file__).resolve().parent
if str(_EXAMPLE_DIR) not in sys.path:
sys.path.insert(0, str(_EXAMPLE_DIR))
def _load_local_env() -> None:
"""Load ``.env`` from the example dir when vars are not already set."""
env_path = _EXAMPLE_DIR / ".env"
if not env_path.is_file():
return
for raw in env_path.read_text(encoding="utf-8").splitlines():
line = raw.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
key, value = key.strip(), value.strip()
if key and key not in os.environ:
os.environ[key] = value
_load_local_env()
aiui.set_custom_js(_EXAMPLE_DIR / "plugin.js")
from pipeline import retry_pipeline, run_ingest_pipeline
from recall_routes import (
api_meeting_live_transcript,
api_recall_bot_status,
api_recall_calendar_bootstrap,
api_recall_calendar_callback,
api_recall_calendar_status,
api_recall_calendar_sync,
api_recall_calendar_upcoming,
api_recall_cancel_bot,
api_recall_config_status,
api_recall_schedule_bot,
webhook_recall,
)
CHAT_SYSTEM_PROMPT = (
"You are a meeting assistant. For any question about past meetings, decisions, "
"owners, or action items you MUST call the search_meetings tool first and answer "
"only from the retrieved transcripts and summaries. Never answer historical "
"questions from memory. Cite every claim with a citation chip showing the meeting "
"title linked to its detail page."
)
# Fallback demo rows for Slack block-shape tests and offline dashboard preview.
SAMPLE_MEETINGS: list[dict] = [
{
"id": "m-1001",
"title": "Q3 Planning Kickoff",
"date": "2026-07-01",
"status": "Ready",
"duration": "48m",
"transcript": "Alice: Let's align on Q3 goals...\nBob: Priorities are onboarding and billing.",
"summary": "Team aligned on Q3 priorities: onboarding revamp and billing reliability.",
"actions": [
{"task": "Draft onboarding spec", "owner": "Alice", "due": "2026-07-08"},
{"task": "Audit billing retries", "owner": "Bob", "due": "2026-07-10"},
],
},
]
_agent: Any | None = None
def _recall_schedule_tool():
from praisonai_tools.tools.decorator import tool
from meeting_integrations.recall.service import schedule_recall_bot as _schedule
@tool
def schedule_recall_bot(meeting_url: str, title: str = "") -> dict:
"""Schedule a Recall.ai bot to join a meeting URL (Zoom, Meet, Teams)."""
return _schedule(meeting_url, title)
return schedule_recall_bot
def _upcoming_meetings_tool():
from praisonai_tools.tools.decorator import tool
from meeting_integrations.recall.calendar_service import list_upcoming_calendar_meetings
@tool
def get_upcoming_meetings(hours: int = 24) -> list[dict]:
"""List upcoming calendar events with video links and bot schedule status."""
return list_upcoming_calendar_meetings(hours=hours)
return get_upcoming_meetings
def _load_meeting_tools():
"""Lazy-load meeting tools (keeps import-time deps light)."""
from praisonai_tools.tools.meeting_tools import (
extract_action_items,
get_meeting,
index_meeting,
list_meetings,
save_meeting,
search_meetings,
summarize_transcript,
transcribe_file,
)
tools = [
transcribe_file,
save_meeting,
get_meeting,
list_meetings,
summarize_transcript,
extract_action_items,
index_meeting,
search_meetings,
]
if os.getenv("RECALL_API_KEY"):
tools.append(_recall_schedule_tool())
tools.append(_upcoming_meetings_tool())
return tools
def get_agent():
"""Return the meeting agent (created lazily on first chat)."""
global _agent
if _agent is not None:
return _agent
try:
from praisonaiagents import Agent
except ImportError as exc:
raise ImportError(
"praisonaiagents required. Install with: pip install praisonai"
) from exc
_agent = Agent(
name="Meeting Assistant",
instructions=CHAT_SYSTEM_PROMPT,
model="gpt-4o-mini",
tools=_load_meeting_tools(),
)
# Default loop guard kills chat after 120s while search_meetings runs.
from praisonaiagents.escalation.loop_guard import LoopGuard, LoopGuardConfig
max_turn_sec = float(os.getenv("MEETING_AGENT_LOOP_GUARD_MAX_SEC", "600"))
_agent._loop_guard = LoopGuard(
LoopGuardConfig(enabled=True, max_time_per_turn=max_turn_sec)
)
return _agent
def _sync_stale_recall_status(record: dict[str, Any]) -> dict[str, Any]:
"""Refresh Recall bot status for meetings still marked joining/scheduled."""
meta = record.get("metadata") or {}
live = (meta.get("live_status") or "").lower()
bot_id = meta.get("recall_bot_id")
if not bot_id or live in ("ready", "failed", "cancelled", "processing"):
return record
if live not in ("joining", "scheduled", "waiting_room", "live", "ended", ""):
return record
if not os.getenv("RECALL_API_KEY"):
return record
try:
from meeting_integrations.recall.client import RecallClient
from meeting_integrations.recall.config import load_recall_settings
from pipeline import merge_meeting_metadata
bot = RecallClient(load_recall_settings()).get_bot(str(bot_id))
changes = bot.get("status_changes") or []
status = (changes[-1].get("code") if changes else bot.get("status") or "").lower()
meeting_id = record.get("meeting_id", "")
patch: dict[str, Any] = {}
if status == "in_waiting_room":
patch = {
"live_status": "waiting_room",
"status": "Scheduled",
"error": (
"Bot is in the Google Meet waiting room — admit "
"'Praison Meeting Agent' to start recording."
),
}
elif status in ("done", "call_ended") and not (bot.get("recordings") or []):
patch = {
"live_status": "failed",
"status": "failed",
"error": "Meeting ended but bot never recorded (likely not admitted from waiting room).",
}
elif status == "done" and (bot.get("recordings") or []):
patch = {"live_status": "processing"}
if patch and meeting_id:
merge_meeting_metadata(meeting_id, patch)
meta = {**meta, **patch}
record = {**record, "metadata": meta}
except Exception:
pass
return record
def _record_to_view(record: dict[str, Any]) -> dict[str, Any]:
"""Normalise a tool meeting record for dashboard rendering."""
meta = record.get("metadata") or {}
actions = meta.get("action_items") or []
return {
"id": record.get("meeting_id", ""),
"title": record.get("title", "Untitled"),
"date": (record.get("created_at") or "")[:10],
"status": meta.get("status", "Ready").replace("ready", "Ready").title(),
"live_status": meta.get("live_status") or "",
"recall_bot_id": meta.get("recall_bot_id") or "",
"meeting_url": meta.get("meeting_url") or record.get("source") or "",
"duration": _format_duration(meta.get("duration_seconds")),
"transcript": meta.get("transcript") or "",
"summary": meta.get("summary") or "",
"error": meta.get("error") or "",
"actions": [
{
"task": a.get("description", ""),
"owner": a.get("owner") or "—",
"due": a.get("due_date") or "—",
}
for a in actions
if isinstance(a, dict)
],
}
def _format_duration(seconds: Any) -> str:
if not seconds:
return "—"
try:
total = int(float(seconds))
except (TypeError, ValueError):
return "—"
minutes, secs = divmod(total, 60)
return f"{minutes}m {secs}s" if minutes else f"{secs}s"
def _list_meeting_views() -> list[dict[str, Any]]:
try:
from praisonai_tools.tools.meeting_tools import list_meetings
rows = list_meetings.__wrapped__(limit=50, offset=0)
if rows and isinstance(rows[0], dict) and "error" in rows[0]:
return SAMPLE_MEETINGS
rows = [_sync_stale_recall_status(r) for r in rows]
views = [_record_to_view(r) for r in rows]
return views or SAMPLE_MEETINGS
except ImportError:
return SAMPLE_MEETINGS
def _meeting(meeting_id: str) -> dict | None:
try:
from praisonai_tools.tools.meeting_tools import get_meeting
record = get_meeting.__wrapped__(meeting_id)
if "error" not in record:
return _record_to_view(record)
except ImportError:
pass
return next((m for m in SAMPLE_MEETINGS if m["id"] == meeting_id), None)
def _pick_detail_meeting(meetings: list[dict[str, Any]]) -> dict[str, Any]:
"""Prefer an in-progress live meeting for the detail view."""
for live in ("live", "joining", "waiting_room"):
for m in meetings:
if (m.get("live_status") or "").lower() == live:
return m
return meetings[0] if meetings else SAMPLE_MEETINGS[0]
def _status_variant(status: str) -> str:
return {
"Ready": "default",
"Indexing": "secondary",
"Summarizing": "secondary",
"Transcribing": "secondary",
"Uploading": "secondary",
"Error": "destructive",
"Failed": "destructive",
"Scheduled": "secondary",
"Live": "default",
"Processing": "secondary",
}.get(status, "outline")
aiui.set_style("dashboard")
aiui.set_branding(title="Meeting Agent", logo="🗓️")
aiui.set_pages(
["upload", "live-bot", "meetings", "meeting-detail", "calendar", "chat"]
)
aiui.set_dashboard(sidebar=True, page_header=True)
aiui.set_chat_features(file_upload=True, feedback=True)
async def _emit_pipeline_chat_result(result: dict[str, Any]) -> None:
"""Surface pipeline progress/errors via standard chat status events."""
if result.get("status") == "ready":
await aiui.say(
f"✅ **{result.get('title', 'Meeting')}** is ready. "
f"Indexed {result.get('chunks_indexed', 0)} chunk(s)."
)
return
if "error" in result:
await aiui.say(f"❌ {result['error']}")
meeting_id = result.get("meeting_id")
if meeting_id:
await aiui.action_buttons(
[
{
"name": "retry-meeting",
"label": "Retry",
"payload": {"meeting_id": meeting_id},
}
]
)
return
await aiui.say(str(result))
async def _chat_run_pipeline(fn, *args: Any) -> None:
await aiui.think("Running meeting pipeline...")
result = await asyncio.to_thread(fn, *args)
await _emit_pipeline_chat_result(result)
@aiui.reply
async def on_message(message: str):
"""Chat: Q&A via meeting agent; retry failed uploads via chat commands."""
text = str(message).strip()
lower = text.lower()
if lower.startswith("retry meeting "):
meeting_id = text.split(maxsplit=2)[-1].strip()
await _chat_run_pipeline(retry_pipeline, meeting_id)
return
if lower.startswith("/retry "):
meeting_id = text.split(maxsplit=1)[-1].strip()
await _chat_run_pipeline(retry_pipeline, meeting_id)
return
if not os.getenv("OPENAI_API_KEY"):
await aiui.say("Set `OPENAI_API_KEY` to chat with the meeting assistant.")
return
await aiui.think("Searching meetings before answering...")
agent = get_agent()
response = await asyncio.to_thread(agent.chat, text)
await aiui.say(str(response))
def _register_retry_action() -> None:
from praisonaiui.actions import register_action_callback
async def _on_retry_meeting(action) -> None:
meeting_id = (action.payload or {}).get("meeting_id", "")
if not meeting_id:
await aiui.say("Missing meeting id for retry.")
return
await _chat_run_pipeline(retry_pipeline, meeting_id)
register_action_callback("retry-meeting", _on_retry_meeting)
_register_retry_action()
@aiui.page("upload", title="Upload", icon="⬆️", group="Meetings", order=1)
async def upload_page():
steps = ["Uploading", "Transcribing", "Summarizing", "Indexing", "Ready"]
return aiui.layout(
[
aiui.text("Drop an audio or transcript file to add a meeting."),
aiui.alert(
"POST a multipart file to /api/meetings/upload with field ``file`` "
"(optional ``title``). Supported: mp3, mp4, wav, webm, m4a, ogg.",
variant="info",
title="Upload API",
),
aiui.code_block(
'curl -F "[email protected]" -F "title=Weekly standup" '
"http://127.0.0.1:8000/api/meetings/upload",
language="bash",
),
aiui.card("Processing pipeline", footer=" → ".join(steps)),
aiui.text(
"On error the pipeline surfaces a retry action via the standard "
"chat status events; no bespoke state machine lives in the UI. "
"In Chat, send ``retry meeting <id>`` or click **Retry** when offered."
),
]
)
@aiui.page("live-bot", title="Live bot", icon="🤖", group="Meetings", order=2)
async def live_bot_page():
port = os.environ.get("MEETING_AGENT_PORT", "8000")
return aiui.layout(
[
aiui.text(
"Send a Recall.ai Meeting Bot to a Zoom, Google Meet, or Teams URL. "
"Real-time ``transcript.data`` chunks stream during the call; when the "
"call ends, the Phase 1 summarize + index pipeline runs on the full transcript."
),
aiui.alert(
"Requires RECALL_API_KEY, RECALL_WEBHOOK_VERIFICATION_SECRET, and "
"PUBLIC_API_BASE_URL (HTTPS tunnel). Register ``transcript.data`` on the "
"same /webhooks/recall endpoint for live captions.",
variant="info",
title="Recall credentials",
),
aiui.code_block(
'curl -X POST http://127.0.0.1:'
f"{port}/api/recall/bots "
'-H "Content-Type: application/json" '
'-d \'{"meeting_url":"https://zoom.us/j/123456789","title":"Standup"}\'',
language="bash",
),
aiui.text(
"Live transcript SSE: ``GET /api/meetings/{meeting_id}/live-transcript`` "
"with ``Accept: text/event-stream``. Poll fallback: same URL without SSE."
),
]
)
@aiui.page("calendar", title="Calendar", icon="📅", group="Meetings", order=5)
async def calendar_page():
upcoming: list[dict[str, Any]] = []
status_note = "Set RECALL_API_KEY to load calendar status."
if os.getenv("RECALL_API_KEY"):
try:
from meeting_integrations.recall.calendar_service import list_upcoming_calendar_meetings
upcoming = list_upcoming_calendar_meetings(hours=48)
status_note = f"{len(upcoming)} upcoming event(s) with video links (48h window)."
except Exception as exc: # noqa: BLE001
status_note = f"Calendar API unavailable: {exc}"
rows = [
[
e.get("title", "—"),
str(e.get("start", ""))[:16],
e.get("platform") or "—",
"Yes" if e.get("bot_scheduled") else "No",
]
for e in upcoming[:10]
]
return aiui.layout(
[
aiui.text(
"Google Calendar V2 auto-schedules Recall bots for eligible future "
"events with video links when CALENDAR_AUTO_RECORD=true (default)."
),
aiui.alert(status_note, variant="info", title="Calendar status"),
aiui.table(
headers=["Title", "Start", "Platform", "Bot scheduled"],
rows=rows or [["—", "—", "—", "—"]],
),
aiui.alert(
"Complete Calendar V2 setup via Recall MCP, bootstrap OAuth callback, "
"then register calendar webhooks (calendar.sync_events, calendar.update) "
"on /webhooks/recall.",
variant="info",
title="Setup",
),
aiui.code_block(
"GET /api/recall/calendar/status\n"
"GET /api/recall/calendar/upcoming?hours=24\n"
"POST /api/recall/calendar/sync {\"calendar_id\":\"...\"}",
language="bash",
),
]
)
@aiui.page("meetings", title="Meetings", icon="📋", group="Meetings", order=3)
async def meetings_page():
meetings = _list_meeting_views()
rows = [
[
m["title"],
m["date"],
m["status"],
m.get("live_status") or "—",
m["id"],
m["duration"],
]
for m in meetings
]
return aiui.layout(
[
aiui.metric("Meetings", value=len(meetings)),
aiui.table(
headers=["Title", "Date", "Status", "Live", "ID", "Duration"],
rows=rows,
),
]
)
@aiui.page("meeting-detail", title="Meeting detail", icon="📝", group="Meetings", order=4)
async def meeting_detail_page():
meetings = _list_meeting_views()
meeting = _pick_detail_meeting(meetings)
action_rows = [[a["task"], a["owner"], a["due"]] for a in meeting["actions"]]
live = (meeting.get("live_status") or "").lower()
header: list[Any] = [
aiui.text(meeting["title"]),
aiui.badge(meeting["status"], variant=_status_variant(meeting["status"])),
aiui.text(f"ID: {meeting['id']}"),
]
if live:
variant = "default" if live == "live" else "outline"
header.append(aiui.badge(live.title(), variant=variant))
body: list[Any] = []
if meeting.get("error"):
body.append(aiui.alert(meeting["error"], variant="warning"))
if live == "live":
body.append(
aiui.alert(
"Live transcript auto-refreshes every 3 seconds on this page.",
variant="info",
title="Live",
)
)
return aiui.layout(
header
+ body
+ [
aiui.tabs(
[
{
"label": "Transcript",
"children": [aiui.code_block(meeting["transcript"] or "(empty)", language="text")],
},
{
"label": "Summary",
"children": [aiui.text(meeting["summary"] or "(not summarised yet)")],
},
{
"label": "Action items",
"children": [
aiui.table(
headers=["Task", "Owner", "Due"],
rows=action_rows or [["—", "—", "—"]],
)
],
},
]
),
]
)
def _slack_adapter():
"""Construct a Slack adapter only when tokens are configured (lazy slack_sdk)."""
bot = os.environ.get("SLACK_BOT_TOKEN", "")
app_token = os.environ.get("SLACK_APP_TOKEN", "")
if not bot or not app_token:
return None
from praisonaiui.features.platform_adapters.slack import SlackChannelAdapter
return SlackChannelAdapter({"bot_token": bot, "app_token": app_token})
def _summary_blocks(meeting: dict) -> list[dict]:
action_lines = "\n".join(
f"• {a['task']} — {a['owner']} (due {a['due']})" for a in meeting["actions"]
) or "• (none)"
return [
{"type": "header", "text": {"type": "plain_text", "text": meeting["title"]}},
{"type": "section", "text": {"type": "mrkdwn", "text": f"*Date:* {meeting['date']}"}},
{"type": "section", "text": {"type": "mrkdwn", "text": meeting["summary"]}},
{"type": "section", "text": {"type": "mrkdwn", "text": f"*Action items*\n{action_lines}"}},
{
"type": "context",
"elements": [{"type": "mrkdwn", "text": f"Dashboard: /meeting-detail#{meeting['id']}"}],
},
]
async def post_meeting_summary(meeting_id: str, channel_id: str) -> bool:
"""Deliver a meeting summary to Slack (/meeting-summary <id>)."""
meeting = _meeting(meeting_id)
if meeting is None:
return False
adapter = _slack_adapter()
if adapter is None:
return False
await adapter.start()
try:
await adapter.send_message(
channel_id,
meeting["summary"],
blocks=_summary_blocks(meeting),
)
finally:
await adapter.stop()
return True
async def api_meetings_upload(request: Request) -> JSONResponse:
"""Accept multipart upload and run the ingest pipeline."""
form = await request.form()
upload = form.get("file")
title = str(form.get("title") or "").strip() or None
if upload is None or not getattr(upload, "filename", None):
return JSONResponse({"error": "file field is required"}, status_code=400)
suffix = Path(upload.filename).suffix.lower() or ".bin"
from pipeline import AUDIO_SUFFIXES
if suffix not in AUDIO_SUFFIXES:
return JSONResponse(
{"error": f"Unsupported file type: {suffix}. Allowed: {sorted(AUDIO_SUFFIXES)}"},
status_code=400,
)
data = await upload.read()
if not data:
return JSONResponse({"error": "empty file"}, status_code=400)
uploads_dir = Path(tempfile.gettempdir()) / "praisonai_meeting_uploads"
uploads_dir.mkdir(parents=True, exist_ok=True)
dest = uploads_dir / f"{os.urandom(8).hex()}{suffix}"
dest.write_bytes(data)
result = await asyncio.to_thread(run_ingest_pipeline, str(dest), title)
if "error" in result and result.get("meeting_id"):
result["retry_hint"] = f"retry meeting {result['meeting_id']}"
result["retry_url"] = f"/api/meetings/{result['meeting_id']}/retry"
status = 200 if result.get("status") == "ready" else 400 if "error" in result else 500
return JSONResponse(result, status_code=status)
async def api_meeting_retry(request: Request) -> JSONResponse:
meeting_id = request.path_params["meeting_id"]
result = await asyncio.to_thread(retry_pipeline, meeting_id)
if "error" in result and result.get("meeting_id"):
result["retry_hint"] = f"retry meeting {result['meeting_id']}"
status = 200 if result.get("status") == "ready" else 400 if "error" in result else 500
return JSONResponse(result, status_code=status)
async def api_meetings_list(_request: Request) -> JSONResponse:
return JSONResponse({"meetings": _list_meeting_views()})
async def api_meeting_detail(request: Request) -> JSONResponse:
meeting_id = request.path_params["meeting_id"]
meeting = _meeting(meeting_id)
if meeting is None:
return JSONResponse({"error": "not found"}, status_code=404)
return JSONResponse(meeting)
app = create_app()
_meeting_routes = [
Route("/api/meetings/upload", api_meetings_upload, methods=["POST"]),
Route("/api/meetings/{meeting_id}/retry", api_meeting_retry, methods=["POST"]),
Route("/api/meetings", api_meetings_list, methods=["GET"]),
Route("/api/meetings/{meeting_id}", api_meeting_detail, methods=["GET"]),
Route("/api/recall/bots", api_recall_schedule_bot, methods=["POST"]),
Route("/api/recall/bots/{meeting_id}", api_recall_bot_status, methods=["GET"]),
Route("/api/recall/bots/{meeting_id}/cancel", api_recall_cancel_bot, methods=["POST"]),
Route("/api/recall/config", api_recall_config_status, methods=["GET"]),
Route("/webhooks/recall", webhook_recall, methods=["POST"]),
Route("/api/recall/calendar/callback", api_recall_calendar_callback, methods=["GET"]),
Route(
"/api/recall/calendar/setup/bootstrap",
api_recall_calendar_bootstrap,
methods=["POST"],
),
Route("/api/recall/calendar/status", api_recall_calendar_status, methods=["GET"]),
Route("/api/recall/calendar/upcoming", api_recall_calendar_upcoming, methods=["GET"]),
Route("/api/recall/calendar/sync", api_recall_calendar_sync, methods=["POST"]),
Route(
"/api/meetings/{meeting_id}/live-transcript",
api_meeting_live_transcript,
methods=["GET"],
),
]
# Prepend so custom /api/* routes win over the SPA catch-all from create_app().
app.routes[0:0] = _meeting_routes
if __name__ == "__main__":
if "--smoke" in sys.argv:
from smoke_recall import run_smoke
run_smoke()
raise SystemExit(0)
import uvicorn
host = os.environ.get("HOST", "127.0.0.1")
port = int(os.environ.get("MEETING_AGENT_PORT", "8000"))
uvicorn.run(app, host=host, port=port)
@@ -0,0 +1 @@
"""Meeting-agent integrations (Recall.ai, etc.)."""
@@ -0,0 +1,13 @@
"""Recall.ai Meeting Bot integration for the meeting-agent example."""
from meeting_integrations.recall.client import RecallClient
from meeting_integrations.recall.config import RecallSettings, load_recall_settings
from meeting_integrations.recall.verify import VerificationError, verify_request_from_recall
__all__ = [
"RecallClient",
"RecallSettings",
"VerificationError",
"load_recall_settings",
"verify_request_from_recall",
]
@@ -0,0 +1,33 @@
"""Calendar V2 OAuth callback forwarding for Recall-hosted setup."""
from __future__ import annotations
import logging
from typing import Any
from urllib.parse import urlencode
import httpx
logger = logging.getLogger(__name__)
FORWARD_PARAMS = ("state", "code", "error", "recall_calendar_setup_probe")
def forward_calendar_callback(
*,
regional_callback_uri: str,
query_params: dict[str, str],
) -> tuple[int, str]:
"""Forward allowed query params to Recall's regional callback URI."""
filtered = {k: v for k, v in query_params.items() if k in FORWARD_PARAMS and v}
if not filtered.get("state") and "recall_calendar_setup_probe" not in filtered:
return 400, "missing state"
url = regional_callback_uri
if filtered:
url = f"{url}?{urlencode(filtered)}"
logger.info("Forwarding calendar callback to Recall (state redacted)")
with httpx.Client(timeout=30.0, follow_redirects=True) as client:
response = client.get(url)
return response.status_code, response.text[:500]
@@ -0,0 +1,261 @@
"""Google Calendar V2 sync and auto-schedule via Recall Calendar API."""
from __future__ import annotations
import logging
from datetime import datetime, timedelta, timezone
from typing import Any
from meeting_integrations.recall.client import RecallAPIError, RecallClient
from meeting_integrations.recall.config import RecallSettings
from meeting_integrations.recall.store import RecallStore
logger = logging.getLogger(__name__)
def _parse_iso(value: str | None) -> datetime | None:
if not value:
return None
try:
normalized = value.replace("Z", "+00:00")
if normalized.endswith("+0000"):
normalized = normalized[:-5] + "+00:00"
return datetime.fromisoformat(normalized)
except ValueError:
return None
def _event_title(event: dict[str, Any]) -> str:
raw = event.get("raw") or {}
if isinstance(raw, dict):
summary = raw.get("summary") or raw.get("subject")
if summary:
return str(summary)
return "Calendar meeting"
def _deduplication_key(event: dict[str, Any]) -> str:
start = str(event.get("start_time") or "")
url = str(event.get("meeting_url") or "")
return f"{start}-{url}"
def is_eligible_for_auto_record(event: dict[str, Any]) -> bool:
"""Return True when Recall should schedule a bot for this calendar event."""
if event.get("is_deleted"):
return False
if not event.get("meeting_url"):
return False
end = _parse_iso(str(event.get("end_time") or ""))
if end and end < datetime.now(timezone.utc):
return False
raw = event.get("raw") or {}
if isinstance(raw, dict):
status = str(raw.get("status") or "").lower()
if status == "cancelled":
return False
return True
def list_upcoming_calendar_meetings(
*,
hours: int = 24,
settings: RecallSettings | None = None,
client: RecallClient | None = None,
) -> list[dict[str, Any]]:
"""List upcoming Recall calendar events with video links (for UI + agent tool)."""
from meeting_integrations.recall.config import load_recall_settings
settings = settings or load_recall_settings()
client = client or RecallClient(settings)
try:
events = client.list_calendar_events(is_deleted=False)
except RecallAPIError as exc:
logger.warning("Calendar events unavailable: %s", exc)
return []
now = datetime.now(timezone.utc)
horizon = now + timedelta(hours=max(1, hours))
rows: list[dict[str, Any]] = []
for event in events:
if not is_eligible_for_auto_record(event):
continue
start = _parse_iso(str(event.get("start_time") or ""))
if start and start > horizon:
continue
bots = event.get("bots") or []
rows.append(
{
"event_id": event.get("id"),
"title": _event_title(event),
"start": event.get("start_time"),
"end": event.get("end_time"),
"platform": (event.get("meeting_platform") or {}).get("name")
if isinstance(event.get("meeting_platform"), dict)
else event.get("meeting_platform"),
"meeting_url": event.get("meeting_url"),
"bot_scheduled": bool(bots),
"calendar_id": event.get("calendar_id"),
}
)
rows.sort(key=lambda r: str(r.get("start") or ""))
return rows
def _ensure_local_meeting_for_event(
event: dict[str, Any],
*,
settings: RecallSettings,
) -> str:
"""Create or reuse a local meeting row for a calendar event."""
event_id = str(event.get("id") or "")
if RecallStore.is_event_scheduled(event_id):
row = RecallStore.get_scheduled_bot(event_id)
if row and row.get("meeting_id"):
return str(row["meeting_id"])
from praisonai_tools.tools.meeting_tools import save_meeting
title = _event_title(event)
url = str(event.get("meeting_url") or "")
saved = save_meeting.__wrapped__(
title=title,
source=url,
metadata={
"status": "scheduled",
"live_status": "scheduled",
"meeting_url": url,
"source": "calendar",
"recall_calendar_event_id": event_id,
},
)
if "error" in saved:
raise RecallAPIError(0, saved["error"])
return str(saved["meeting_id"])
def sync_calendar_events(
calendar_id: str,
*,
updated_since: str | None = None,
settings: RecallSettings,
client: RecallClient | None = None,
auto_record: bool | None = None,
) -> dict[str, Any]:
"""Fetch changed calendar events and schedule/remove bots per policy."""
client = client or RecallClient(settings)
should_record = (
settings.calendar_auto_record if auto_record is None else auto_record
)
events = client.list_calendar_events(
calendar_id=calendar_id,
updated_at_gte=updated_since,
)
scheduled = 0
removed = 0
skipped = 0
errors: list[str] = []
for event in events:
event_id = str(event.get("id") or "")
if not event_id:
continue
eligible = is_eligible_for_auto_record(event) and should_record
has_bots = bool(event.get("bots"))
if not eligible:
if has_bots and (event.get("is_deleted") or not should_record):
try:
client.remove_bot_from_calendar_event(event_id)
removed += 1
except RecallAPIError as exc:
errors.append(f"{event_id}: remove failed: {exc.detail}")
else:
skipped += 1
continue
if RecallStore.is_event_scheduled(event_id) and has_bots:
skipped += 1
continue
try:
meeting_id = _ensure_local_meeting_for_event(event, settings=settings)
bot_config = client.build_bot_config(
meeting_id=meeting_id,
title=_event_title(event),
)
updated = client.schedule_bot_for_calendar_event(
event_id,
deduplication_key=_deduplication_key(event),
bot_config=bot_config,
)
bot_id = None
bots = updated.get("bots") or []
if bots and isinstance(bots[0], dict):
bot_id = bots[0].get("bot_id") or bots[0].get("id")
RecallStore.record_scheduled_bot(event_id, meeting_id, bot_id)
from pipeline import merge_meeting_metadata
merge_meeting_metadata(
meeting_id,
{
"recall_calendar_event_id": event_id,
"recall_bot_id": bot_id,
"status": "scheduled",
"live_status": "scheduled",
},
)
scheduled += 1
except RecallAPIError as exc:
logger.warning("Calendar schedule failed for %s: %s", event_id, exc.detail)
errors.append(f"{event_id}: {exc.detail}")
return {
"calendar_id": calendar_id,
"fetched": len(events),
"scheduled": scheduled,
"removed": removed,
"skipped": skipped,
"errors": errors,
}
def process_calendar_webhook(
event_type: str,
payload: dict[str, Any],
*,
settings: RecallSettings,
client: RecallClient | None = None,
) -> None:
"""Handle Recall Calendar V2 webhooks (calendar.update, calendar.sync_events)."""
data = payload.get("data") or {}
if not isinstance(data, dict):
return
calendar_id = str(data.get("calendar_id") or "")
if not calendar_id:
logger.warning("Calendar webhook %s missing calendar_id", event_type)
return
if event_type == "calendar.update":
logger.info("Calendar %s updated — refresh status in UI", calendar_id)
return
if event_type == "calendar.sync_events":
updated_since = str(data.get("last_updated_ts") or "") or None
result = sync_calendar_events(
calendar_id,
updated_since=updated_since,
settings=settings,
client=client,
)
logger.info("Calendar sync %s: %s", calendar_id, result)
return
logger.debug("Ignoring calendar webhook event %s", event_type)
@@ -0,0 +1,290 @@
"""Minimal Recall.ai REST client (EU region, v1.11)."""
from __future__ import annotations
import json
import logging
import random
import time
from typing import Any
import httpx
from meeting_integrations.recall.config import RecallSettings
logger = logging.getLogger(__name__)
class RecallAPIError(RuntimeError):
"""Recall API returned a non-success response."""
def __init__(self, status_code: int, detail: str):
super().__init__(detail)
self.status_code = status_code
self.detail = detail
class RecallClient:
"""HTTP client for Recall Meeting Bots and async transcription."""
def __init__(self, settings: RecallSettings):
self._settings = settings
self._base = settings.api_base.rstrip("/")
def _headers(self) -> dict[str, str]:
return {
"Authorization": self._settings.api_key,
"Accept": "application/json",
"Content-Type": "application/json",
}
def _request(
self,
method: str,
path: str,
*,
json_body: dict[str, Any] | None = None,
max_attempts: int = 4,
) -> dict[str, Any]:
url = f"{self._base}{path}"
last_error: Exception | None = None
for attempt in range(max_attempts):
try:
with httpx.Client(timeout=30.0) as client:
response = client.request(
method,
url,
headers=self._headers(),
json=json_body,
)
except httpx.HTTPError as exc:
last_error = exc
if attempt + 1 >= max_attempts:
raise RecallAPIError(0, str(exc)) from exc
time.sleep(min(2**attempt + random.random(), 8))
continue
if response.status_code == 429:
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else min(2**attempt, 30)
time.sleep(delay + random.random())
continue
if response.status_code in {503, 507} and attempt + 1 < max_attempts:
time.sleep(min(2**attempt + random.random(), 8))
continue
if response.status_code >= 400:
detail = response.text[:500] or response.reason_phrase
raise RecallAPIError(response.status_code, detail)
if not response.content:
return {}
return response.json()
raise RecallAPIError(0, str(last_error or "request failed"))
def create_bot(
self,
*,
meeting_url: str,
meeting_id: str,
title: str,
join_at: str | None = None,
enable_realtime_transcription: bool = True,
) -> dict[str, Any]:
"""Schedule a Recall bot; attach local meeting_id in metadata for webhook routing."""
body: dict[str, Any] = {
"meeting_url": meeting_url,
"bot_name": self._settings.bot_name,
"metadata": {"meeting_id": meeting_id, "title": title},
}
if join_at:
body["join_at"] = join_at
if enable_realtime_transcription:
recording_config = self._settings.realtime_recording_config()
if recording_config:
body["recording_config"] = recording_config
logger.info(
"Live transcript enabled for meeting_id=%s → %s",
meeting_id,
self._settings.webhook_url,
)
else:
logger.warning(
"Live transcript skipped for meeting_id=%s — set PUBLIC_API_BASE_URL",
meeting_id,
)
logger.info(
"Creating Recall bot for meeting_id=%s (url redacted)",
meeting_id,
)
return self._request("POST", "/api/v1/bot/", json_body=body)
def build_bot_config(
self,
*,
meeting_id: str,
title: str,
enable_realtime_transcription: bool = True,
) -> dict[str, Any]:
"""Bot config payload for Calendar V2 schedule-bot requests."""
config: dict[str, Any] = {
"bot_name": self._settings.bot_name,
"metadata": {"meeting_id": meeting_id, "title": title},
}
if enable_realtime_transcription:
recording_config = self._settings.realtime_recording_config()
if recording_config:
config["recording_config"] = recording_config
return config
def get_bot(self, bot_id: str) -> dict[str, Any]:
return self._request("GET", f"/api/v1/bot/{bot_id}/")
def cancel_bot(self, bot_id: str) -> dict[str, Any]:
return self._request("POST", f"/api/v1/bot/{bot_id}/leave_call/")
def create_async_transcript(self, recording_id: str) -> dict[str, Any]:
body = {
"provider": {"recallai_async": {"language_code": "auto"}},
"diarization": {"use_separate_streams_when_available": True},
}
return self._request(
"POST",
f"/api/v1/recording/{recording_id}/create_transcript/",
json_body=body,
)
def get_transcript(self, transcript_id: str) -> dict[str, Any]:
return self._request("GET", f"/api/v1/transcript/{transcript_id}/")
@staticmethod
def extract_recording_id(payload: dict[str, Any]) -> str | None:
data = payload.get("data") or {}
if isinstance(data, dict):
recording = data.get("recording") or {}
if isinstance(recording, dict) and recording.get("id"):
return str(recording["id"])
inner = data.get("data") or {}
if isinstance(inner, dict):
rec = inner.get("recording") or {}
if isinstance(rec, dict) and rec.get("id"):
return str(rec["id"])
return None
@staticmethod
def extract_transcript_id(payload: dict[str, Any]) -> str | None:
data = payload.get("data") or {}
if isinstance(data, dict):
transcript = data.get("transcript") or {}
if isinstance(transcript, dict) and transcript.get("id"):
return str(transcript["id"])
return None
@staticmethod
def extract_bot_id(payload: dict[str, Any]) -> str | None:
data = payload.get("data") or {}
if isinstance(data, dict):
bot = data.get("bot") or {}
if isinstance(bot, dict) and bot.get("id"):
return str(bot["id"])
return None
@staticmethod
def extract_meeting_id_from_bot_metadata(payload: dict[str, Any]) -> str | None:
data = payload.get("data") or {}
if not isinstance(data, dict):
return None
bot = data.get("bot") or {}
if not isinstance(bot, dict):
return None
metadata = bot.get("metadata") or {}
if isinstance(metadata, dict):
mid = metadata.get("meeting_id")
return str(mid) if mid else None
return None
@staticmethod
def download_json(url: str) -> Any:
with httpx.Client(timeout=60.0) as client:
response = client.get(url)
response.raise_for_status()
return response.json()
def list_calendars(self, *, status: str | None = "connected") -> list[dict[str, Any]]:
params: dict[str, str] = {}
if status:
params["status"] = status
results: list[dict[str, Any]] = []
cursor: str | None = None
while True:
query = dict(params)
if cursor:
query["cursor"] = cursor
path = "/api/v2/calendars/"
if query:
from urllib.parse import urlencode
path = f"{path}?{urlencode(query)}"
page = self._request("GET", path)
batch = page.get("results") or []
if isinstance(batch, list):
results.extend(item for item in batch if isinstance(item, dict))
cursor = page.get("next")
if not cursor:
break
return results
def list_calendar_events(
self,
*,
calendar_id: str | None = None,
updated_at_gte: str | None = None,
is_deleted: bool | None = False,
) -> list[dict[str, Any]]:
from urllib.parse import urlencode
params: dict[str, str] = {}
if calendar_id:
params["calendar_id"] = calendar_id
if updated_at_gte:
params["updated_at__gte"] = updated_at_gte
if is_deleted is not None:
params["is_deleted"] = "true" if is_deleted else "false"
results: list[dict[str, Any]] = []
cursor: str | None = None
while True:
query = dict(params)
if cursor:
query["cursor"] = cursor
path = f"/api/v2/calendar-events/?{urlencode(query)}" if query else "/api/v2/calendar-events/"
page = self._request("GET", path)
batch = page.get("results") or []
if isinstance(batch, list):
results.extend(item for item in batch if isinstance(item, dict))
cursor = page.get("next")
if not cursor:
break
return results
def schedule_bot_for_calendar_event(
self,
event_id: str,
*,
deduplication_key: str,
bot_config: dict[str, Any],
) -> dict[str, Any]:
body = {
"deduplication_key": deduplication_key,
"bot_config": bot_config,
}
return self._request(
"POST",
f"/api/v2/calendar-events/{event_id}/bot/",
json_body=body,
)
def remove_bot_from_calendar_event(self, event_id: str) -> dict[str, Any]:
return self._request("DELETE", f"/api/v2/calendar-events/{event_id}/bot/")
@@ -0,0 +1,100 @@
"""Recall.ai runtime configuration — credentials stay server-side only."""
from __future__ import annotations
import os
from dataclasses import dataclass
from typing import Any
from urllib.parse import urlparse
class RecallConfigError(ValueError):
"""Raised when required Recall settings are missing or invalid."""
@dataclass(frozen=True)
class RecallSettings:
"""Validated Recall workspace settings injected into the API client."""
region: str
api_key: str
webhook_verification_secret: str
public_api_base_url: str
workspace_id: str
bot_name: str
calendar_auto_record: bool
@property
def api_base(self) -> str:
return f"https://{self.region}.recall.ai"
@property
def webhook_url(self) -> str:
return f"{self.public_api_base_url.rstrip('/')}/webhooks/recall"
@property
def calendar_callback_url(self) -> str:
return f"{self.public_api_base_url.rstrip('/')}/api/recall/calendar/callback"
def realtime_recording_config(self) -> dict[str, Any] | None:
"""Recording config with live ``transcript.data`` webhooks (no dashboard toggle)."""
if not self.public_api_base_url:
return None
return {
"transcript": {
"provider": {
"recallai_streaming": {
"mode": "prioritize_low_latency",
"language_code": "en",
}
},
"diarization": {
"use_separate_streams_when_available": True,
},
},
"realtime_endpoints": [
{
"type": "webhook",
"url": self.webhook_url,
"events": ["transcript.data", "transcript.partial_data"],
}
],
}
def _require(name: str) -> str:
value = os.getenv(name, "").strip()
if not value:
raise RecallConfigError(f"{name} is required for Recall integration")
return value
def load_recall_settings(*, require_public_url: bool = False) -> RecallSettings:
"""Load Recall settings from the process environment."""
region = os.getenv("RECALL_REGION", "eu-central-1").strip() or "eu-central-1"
public_url = os.getenv("PUBLIC_API_BASE_URL", "").strip()
if require_public_url and not public_url:
raise RecallConfigError(
"PUBLIC_API_BASE_URL is required (stable HTTPS backend URL for webhooks "
"and calendar OAuth callback forwarding)"
)
if public_url:
parsed = urlparse(public_url)
if parsed.scheme not in {"https", "http"} or not parsed.netloc:
raise RecallConfigError("PUBLIC_API_BASE_URL must be an absolute http(s) URL")
if parsed.hostname in {"localhost", "127.0.0.1"}:
raise RecallConfigError(
"PUBLIC_API_BASE_URL must not be localhost; use a reserved tunnel domain"
)
auto_record = os.getenv("CALENDAR_AUTO_RECORD", "true").strip().lower()
return RecallSettings(
region=region,
api_key=_require("RECALL_API_KEY"),
webhook_verification_secret=_require("RECALL_WEBHOOK_VERIFICATION_SECRET"),
public_api_base_url=public_url,
workspace_id=os.getenv("RECALL_WORKSPACE_ID", "").strip(),
bot_name=os.getenv("RECALL_BOT_NAME", "Praison Meeting Agent").strip()
or "Praison Meeting Agent",
calendar_auto_record=auto_record not in {"0", "false", "no", "off"},
)
@@ -0,0 +1,83 @@
"""In-process live transcript fan-out for SSE subscribers."""
from __future__ import annotations
import threading
from collections import defaultdict
from dataclasses import dataclass, field
from typing import Any
@dataclass
class LiveTranscriptState:
"""Latest live transcript snapshot for a meeting."""
lines: list[str] = field(default_factory=list)
text: str = ""
live_status: str = ""
_lock = threading.Lock()
_states: dict[str, LiveTranscriptState] = {}
_subscribers: dict[str, list[threading.Event]] = defaultdict(list)
def get_live_state(meeting_id: str) -> LiveTranscriptState | None:
with _lock:
return _states.get(meeting_id)
def publish_live_chunk(
meeting_id: str,
*,
line: str,
full_text: str,
live_status: str = "live",
) -> None:
"""Record a new utterance and notify SSE waiters."""
with _lock:
state = _states.setdefault(meeting_id, LiveTranscriptState())
if line:
state.lines.append(line)
state.text = full_text
state.live_status = live_status
waiters = list(_subscribers.get(meeting_id, []))
for event in waiters:
event.set()
def set_live_status(meeting_id: str, live_status: str) -> None:
with _lock:
state = _states.setdefault(meeting_id, LiveTranscriptState())
state.live_status = live_status
waiters = list(_subscribers.get(meeting_id, []))
for event in waiters:
event.set()
def wait_for_update(meeting_id: str, timeout: float = 25.0) -> LiveTranscriptState | None:
"""Block until the meeting transcript changes or timeout (for SSE long-poll)."""
event = threading.Event()
with _lock:
_subscribers[meeting_id].append(event)
state = _states.get(meeting_id)
event.wait(timeout=timeout)
with _lock:
subs = _subscribers.get(meeting_id, [])
if event in subs:
subs.remove(event)
return _states.get(meeting_id)
def snapshot_for_api(meeting_id: str, record: dict[str, Any] | None = None) -> dict[str, Any]:
"""Merge in-memory live state with persisted meeting metadata."""
meta = (record or {}).get("metadata") or {}
live = get_live_state(meeting_id)
text = (live.text if live and live.text else meta.get("transcript")) or ""
return {
"meeting_id": meeting_id,
"live_status": (live.live_status if live and live.live_status else meta.get("live_status"))
or "",
"transcript": text,
"line_count": len([ln for ln in text.splitlines() if ln.strip()]),
}
@@ -0,0 +1,226 @@
"""Async Recall webhook event processing."""
from __future__ import annotations
import logging
from typing import Any
from meeting_integrations.recall.client import RecallAPIError, RecallClient
from meeting_integrations.recall.config import RecallSettings
from meeting_integrations.recall.store import RecallStore, webhook_event_key
from meeting_integrations.recall.transcript import transcript_data_to_line, transcript_download_to_text
logger = logging.getLogger(__name__)
def _speaker_prefix(line: str) -> str:
if ":" in line:
return line.split(":", 1)[0] + ":"
return ""
def _upsert_live_transcript(meeting_id: str, line: str, *, partial: bool = False) -> str:
"""Merge one live caption line; partial updates replace the last same-speaker line."""
from praisonai_tools.tools.meeting_tools import get_meeting
from meeting_integrations.recall.live import publish_live_chunk
from pipeline import _metadata_lock
# Hold the merge lock across the read too: streaming transcript events are
# dispatched one-thread-each, so an unsynchronized read-append-write would
# let concurrent captions clobber each other and drop lines.
with _metadata_lock:
record = get_meeting.__wrapped__(meeting_id)
meta = record.get("metadata") or {} if isinstance(record, dict) else {}
existing = str(meta.get("transcript") or "")
lines = existing.splitlines()
prefix = _speaker_prefix(line)
if lines and lines[-1] == line:
full = existing
elif partial and lines and prefix and lines[-1].startswith(prefix):
lines[-1] = line
full = "\n".join(lines)
elif not partial and lines and prefix and lines[-1].startswith(prefix):
# Final utterance replaces the in-progress partial for this speaker.
lines[-1] = line
full = "\n".join(lines)
else:
full = f"{existing}\n{line}".strip() if existing else line
_merge_metadata(
meeting_id,
{"transcript": full, "live_status": "live", "status": "live"},
)
publish_live_chunk(meeting_id, line=line, full_text=full, live_status="live")
return full
def _append_live_transcript(meeting_id: str, line: str) -> str:
return _upsert_live_transcript(meeting_id, line, partial=False)
def _merge_metadata(meeting_id: str, patch: dict[str, Any]) -> None:
from pipeline import merge_meeting_metadata
merge_meeting_metadata(meeting_id, patch)
def _resolve_meeting_id(payload: dict[str, Any], bot_id: str | None) -> str | None:
meeting_id = RecallClient.extract_meeting_id_from_bot_metadata(payload)
if meeting_id:
return meeting_id
if not bot_id:
return None
try:
from praisonai_tools.tools.meeting_tools import list_meetings
rows = list_meetings.__wrapped__(limit=200, offset=0)
for row in rows:
if not isinstance(row, dict):
continue
meta = row.get("metadata") or {}
if meta.get("recall_bot_id") == bot_id:
return row.get("meeting_id")
except Exception as exc: # noqa: BLE001
logger.warning("Could not resolve meeting by bot_id: %s", exc)
return None
def process_recall_webhook(
event_type: str,
payload: dict[str, Any],
*,
settings: RecallSettings,
client: RecallClient | None = None,
) -> None:
"""Handle a verified Recall webhook (runs off the request thread)."""
event_key = webhook_event_key(event_type, payload)
if not RecallStore.mark_event_processed(event_key, event_type):
logger.info("Skipping duplicate Recall event %s", event_type)
return
client = client or RecallClient(settings)
bot_id = RecallClient.extract_bot_id(payload)
meeting_id = _resolve_meeting_id(payload, bot_id)
if event_type == "bot.joining_call" and meeting_id:
_merge_metadata(meeting_id, {"live_status": "joining", "recall_bot_id": bot_id})
return
if event_type == "bot.in_waiting_room" and meeting_id:
_merge_metadata(
meeting_id,
{
"live_status": "waiting_room",
"status": "Scheduled",
"error": "Bot is in the meeting waiting room — admit 'Praison Meeting Agent' to record.",
},
)
return
if event_type == "bot.in_call_recording" and meeting_id:
_merge_metadata(meeting_id, {"live_status": "live", "status": "live", "error": ""})
from meeting_integrations.recall.live import set_live_status
set_live_status(meeting_id, "live")
return
if event_type == "transcript.data" and meeting_id:
line = transcript_data_to_line(payload)
if line:
_append_live_transcript(meeting_id, line)
return
if event_type == "transcript.partial_data" and meeting_id:
line = transcript_data_to_line(payload)
if line:
_upsert_live_transcript(meeting_id, line, partial=True)
return
if event_type == "bot.call_ended" and meeting_id:
_merge_metadata(meeting_id, {"live_status": "ended"})
from meeting_integrations.recall.live import set_live_status
set_live_status(meeting_id, "ended")
return
if event_type == "bot.fatal" and meeting_id:
sub = ((payload.get("data") or {}).get("data") or {}).get("sub_code")
_merge_metadata(
meeting_id,
{
"live_status": "failed",
"status": "failed",
"error": f"Recall bot fatal: {sub or 'unknown'}",
},
)
return
if event_type == "bot.done" and meeting_id:
_merge_metadata(meeting_id, {"live_status": "processing"})
return
if event_type == "recording.done":
recording_id = RecallClient.extract_recording_id(payload)
if not recording_id:
logger.warning("recording.done without recording id")
return
if meeting_id:
_merge_metadata(meeting_id, {"recall_recording_id": recording_id})
started_at = ((payload.get("data") or {}).get("recording") or {}).get("started_at")
completed_at = ((payload.get("data") or {}).get("recording") or {}).get("completed_at")
if started_at and completed_at:
try:
from datetime import datetime
fmt = "%Y-%m-%dT%H:%M:%S.%f%z"
start = datetime.strptime(started_at.replace("Z", "+0000"), fmt)
end = datetime.strptime(completed_at.replace("Z", "+0000"), fmt)
_merge_metadata(
meeting_id,
{"duration_seconds": int((end - start).total_seconds())},
)
except (TypeError, ValueError):
pass
try:
client.create_async_transcript(recording_id)
logger.info("Started async transcript for recording %s", recording_id)
except RecallAPIError as exc:
logger.error("create_async_transcript failed: %s", exc)
if meeting_id:
_merge_metadata(
meeting_id,
{"status": "failed", "error": f"Transcript start failed: {exc}"},
)
return
if event_type == "transcript.failed" and meeting_id:
_merge_metadata(
meeting_id,
{"status": "failed", "error": "Recall transcript generation failed"},
)
return
if event_type == "transcript.done":
transcript_id = RecallClient.extract_transcript_id(payload)
if not transcript_id:
logger.warning("transcript.done without transcript id")
return
if not meeting_id:
logger.warning("transcript.done without meeting_id")
return
try:
artifact = client.get_transcript(transcript_id)
download_url = (artifact.get("data") or {}).get("download_url")
if not download_url:
raise RecallAPIError(0, "missing transcript download_url")
raw = client.download_json(download_url)
text = transcript_download_to_text(raw)
if not text.strip():
raise RecallAPIError(0, "empty transcript")
from pipeline import run_post_transcript_pipeline
run_post_transcript_pipeline(meeting_id, text)
except Exception as exc: # noqa: BLE001
logger.exception("Post-transcript pipeline failed")
_merge_metadata(meeting_id, {"status": "failed", "error": str(exc)})
@@ -0,0 +1,138 @@
"""High-level Recall bot scheduling used by API routes and the agent."""
from __future__ import annotations
import logging
import re
from typing import Any
from meeting_integrations.recall.client import RecallAPIError, RecallClient
from meeting_integrations.recall.config import RecallConfigError, RecallSettings, load_recall_settings
logger = logging.getLogger(__name__)
_MEETING_URL_RE = re.compile(r"^https?://", re.I)
def schedule_recall_bot(
meeting_url: str,
title: str = "",
*,
join_at: str | None = None,
settings: RecallSettings | None = None,
client: RecallClient | None = None,
) -> dict[str, Any]:
"""Persist local intent, create the Recall bot, and store the returned bot id."""
url = (meeting_url or "").strip()
if not _MEETING_URL_RE.match(url):
return {"error": "meeting_url must be an http(s) URL"}
meeting_title = (title or "Live meeting").strip() or "Live meeting"
try:
settings = settings or load_recall_settings()
except RecallConfigError as exc:
return {"error": str(exc)}
from praisonai_tools.tools.meeting_tools import save_meeting
saved = save_meeting.__wrapped__(
title=meeting_title,
source=url,
metadata={
"status": "scheduled",
"live_status": "scheduled",
"meeting_url": url,
"source": "recall",
},
)
if "error" in saved:
return saved
meeting_id = saved["meeting_id"]
api = client or RecallClient(settings)
try:
bot = api.create_bot(
meeting_url=url,
meeting_id=meeting_id,
title=meeting_title,
join_at=join_at,
)
except RecallAPIError as exc:
from pipeline import merge_meeting_metadata
merge_meeting_metadata(
meeting_id,
{
"status": "failed",
"live_status": "failed",
"error": f"Recall create bot failed: {exc.detail}",
},
)
return {
"meeting_id": meeting_id,
"error": f"Recall create bot failed: {exc.detail}",
}
bot_id = bot.get("id")
from pipeline import merge_meeting_metadata
merge_meeting_metadata(
meeting_id,
{
"recall_bot_id": bot_id,
"status": "scheduled",
"live_status": "scheduled",
"meeting_url": url,
},
)
realtime = settings.realtime_recording_config() is not None
logger.info("Scheduled Recall bot %s for meeting %s", bot_id, meeting_id)
return {
"meeting_id": meeting_id,
"recall_bot_id": bot_id,
"status": "scheduled",
"live_status": "scheduled",
"title": meeting_title,
"meeting_url": url,
"realtime_transcript_enabled": realtime,
"realtime_webhook_url": settings.webhook_url if realtime else None,
}
def cancel_recall_bot(
meeting_id: str,
*,
settings: RecallSettings | None = None,
client: RecallClient | None = None,
) -> dict[str, Any]:
"""Cancel a scheduled or in-call Recall bot."""
from praisonai_tools.tools.meeting_tools import get_meeting
from pipeline import merge_meeting_metadata
record = get_meeting.__wrapped__(meeting_id)
if "error" in record:
return record
bot_id = (record.get("metadata") or {}).get("recall_bot_id")
if not bot_id:
return {"error": "No Recall bot linked to this meeting", "meeting_id": meeting_id}
try:
settings = settings or load_recall_settings()
except RecallConfigError as exc:
return {"error": str(exc)}
api = client or RecallClient(settings)
try:
api.cancel_bot(str(bot_id))
except RecallAPIError as exc:
return {"meeting_id": meeting_id, "error": str(exc)}
merge_meeting_metadata(
meeting_id,
{"live_status": "cancelled", "status": "cancelled"},
)
return {"meeting_id": meeting_id, "recall_bot_id": bot_id, "status": "cancelled"}
@@ -0,0 +1,213 @@
"""Persist Recall webhook idempotency and calendar setup state."""
from __future__ import annotations
import json
import os
import sqlite3
from contextlib import closing
from pathlib import Path
from typing import Any
from meeting_integrations.recall.client import RecallClient
def _state_db_path() -> str:
env = os.getenv("PRAISONAI_MEETINGS_DIR")
base = Path(env) if env else Path.home() / ".praisonai" / "meetings"
base.mkdir(parents=True, exist_ok=True)
return str(base / "recall_state.db")
def _connect() -> sqlite3.Connection:
conn = sqlite3.connect(_state_db_path())
conn.execute(
"""
CREATE TABLE IF NOT EXISTS recall_webhook_events (
event_key TEXT PRIMARY KEY,
event_type TEXT NOT NULL,
processed_at TEXT NOT NULL
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS recall_calendar_setup (
setup_id TEXT PRIMARY KEY,
platform TEXT NOT NULL,
state_json TEXT NOT NULL,
updated_at TEXT NOT NULL
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS recall_scheduled_bots (
event_id TEXT PRIMARY KEY,
meeting_id TEXT NOT NULL,
bot_id TEXT,
scheduled_at TEXT NOT NULL
)
"""
)
return conn
def webhook_event_key(event_type: str, payload: dict[str, Any]) -> str:
"""Build a stable idempotency key from webhook metadata."""
data = payload.get("data") or {}
# Streaming transcript events fire many times per bot — never dedupe on bot_id alone.
if event_type in ("transcript.data", "transcript.partial_data"):
inner = data.get("data") if isinstance(data, dict) else {}
bot_id = RecallClient.extract_bot_id(payload) or ""
if isinstance(inner, dict):
words = inner.get("words")
if isinstance(words, list) and words:
first = words[0] if isinstance(words[0], dict) else {}
last = words[-1] if isinstance(words[-1], dict) else {}
word_text = " ".join(
w.get("text", "") for w in words if isinstance(w, dict)
).strip()
return (
f"{event_type}:{bot_id}:"
f"{first.get('start', first.get('start_time', ''))}:"
f"{last.get('end', last.get('end_time', ''))}:"
f"{word_text}"
)
text = str(inner.get("text") or "").strip()
start = inner.get("start") or inner.get("start_time") or ""
end = inner.get("end") or inner.get("end_time") or ""
speaker = inner.get("speaker") or inner.get("speaker_name") or ""
return f"{event_type}:{bot_id}:{start}:{end}:{speaker}:{text}"
return f"{event_type}:{json.dumps(payload, sort_keys=True)}"
for part in (
payload.get("id"),
data.get("id") if isinstance(data, dict) else None,
):
if part:
return f"{event_type}:{part}"
bot_id = RecallStore.extract_bot_id(payload)
recording_id = RecallStore.extract_recording_id(payload)
transcript_id = RecallStore.extract_transcript_id(payload)
suffix = bot_id or recording_id or transcript_id or json.dumps(payload, sort_keys=True)
return f"{event_type}:{suffix}"
class RecallStore:
"""SQLite-backed Recall integration state."""
@staticmethod
def extract_bot_id(payload: dict[str, Any]) -> str | None:
from meeting_integrations.recall.client import RecallClient
return RecallClient.extract_bot_id(payload)
@staticmethod
def extract_recording_id(payload: dict[str, Any]) -> str | None:
from meeting_integrations.recall.client import RecallClient
return RecallClient.extract_recording_id(payload)
@staticmethod
def extract_transcript_id(payload: dict[str, Any]) -> str | None:
from meeting_integrations.recall.client import RecallClient
return RecallClient.extract_transcript_id(payload)
@staticmethod
def mark_event_processed(event_key: str, event_type: str) -> bool:
"""Claim an event for processing; return False if already processed.
Uses an atomic ``INSERT OR IGNORE`` so concurrent duplicate webhook
deliveries (Svix retries/fan-out) cannot both pass the check — the
second insert is ignored and reports zero changed rows.
"""
from datetime import datetime, timezone
with closing(_connect()) as conn, conn:
cursor = conn.execute(
"""
INSERT OR IGNORE INTO recall_webhook_events
(event_key, event_type, processed_at)
VALUES (?, ?, ?)
""",
(event_key, event_type, datetime.now(timezone.utc).isoformat()),
)
return cursor.rowcount > 0
@staticmethod
def save_calendar_setup(setup_id: str, platform: str, state: dict[str, Any]) -> None:
from datetime import datetime, timezone
with closing(_connect()) as conn, conn:
conn.execute(
"""
INSERT INTO recall_calendar_setup (setup_id, platform, state_json, updated_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(setup_id) DO UPDATE SET
platform = excluded.platform,
state_json = excluded.state_json,
updated_at = excluded.updated_at
""",
(
setup_id,
platform,
json.dumps(state, sort_keys=True),
datetime.now(timezone.utc).isoformat(),
),
)
@staticmethod
def get_calendar_setup(setup_id: str) -> dict[str, Any] | None:
with closing(_connect()) as conn:
row = conn.execute(
"SELECT state_json FROM recall_calendar_setup WHERE setup_id = ?",
(setup_id,),
).fetchone()
if not row:
return None
return json.loads(row[0])
@staticmethod
def record_scheduled_bot(event_id: str, meeting_id: str, bot_id: str | None) -> None:
from datetime import datetime, timezone
with closing(_connect()) as conn, conn:
conn.execute(
"""
INSERT INTO recall_scheduled_bots (event_id, meeting_id, bot_id, scheduled_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(event_id) DO NOTHING
""",
(event_id, meeting_id, bot_id, datetime.now(timezone.utc).isoformat()),
)
@staticmethod
def is_event_scheduled(event_id: str) -> bool:
with closing(_connect()) as conn:
row = conn.execute(
"SELECT 1 FROM recall_scheduled_bots WHERE event_id = ?",
(event_id,),
).fetchone()
return row is not None
@staticmethod
def get_scheduled_bot(event_id: str) -> dict[str, Any] | None:
with closing(_connect()) as conn:
row = conn.execute(
"""
SELECT event_id, meeting_id, bot_id, scheduled_at
FROM recall_scheduled_bots WHERE event_id = ?
""",
(event_id,),
).fetchone()
if not row:
return None
return {
"event_id": row[0],
"meeting_id": row[1],
"bot_id": row[2],
"scheduled_at": row[3],
}
@@ -0,0 +1,62 @@
"""Convert Recall transcript download payloads to plain text."""
from __future__ import annotations
from typing import Any
def transcript_data_to_line(payload: dict[str, Any]) -> str:
"""Flatten a verified ``transcript.data`` webhook payload to one line."""
data = payload.get("data") or {}
if not isinstance(data, dict):
return ""
inner = data.get("data") or data
if not isinstance(inner, dict):
return ""
words = inner.get("words")
if isinstance(words, list) and words:
text = " ".join(
w.get("text", "") for w in words if isinstance(w, dict)
).strip()
else:
text = str(inner.get("text") or "").strip()
if not text:
return ""
speaker = inner.get("speaker") or inner.get("speaker_name")
participant = inner.get("participant")
if not speaker and isinstance(participant, dict):
speaker = participant.get("name")
return f"{speaker}: {text}" if speaker else text
def transcript_download_to_text(data: Any) -> str:
"""Flatten Recall transcript JSON into a readable multi-line string."""
if isinstance(data, str):
return data.strip()
if not isinstance(data, list):
return str(data)
lines: list[str] = []
for item in data:
if not isinstance(item, dict):
continue
speaker = item.get("speaker") or item.get("speaker_name")
participant = item.get("participant")
if not speaker and isinstance(participant, dict):
speaker = participant.get("name")
words = item.get("words")
if isinstance(words, list) and words:
text = " ".join(
w.get("text", "") for w in words if isinstance(w, dict)
).strip()
else:
text = str(item.get("text") or "").strip()
if not text:
continue
prefix = f"{speaker}: " if speaker else ""
lines.append(f"{prefix}{text}")
return "\n".join(lines)
@@ -0,0 +1,48 @@
"""Verify incoming Recall webhook and callback requests."""
from __future__ import annotations
import base64
import hmac
import hashlib
class VerificationError(ValueError):
"""Signature verification failed."""
def verify_request_from_recall(
*,
secret: str,
headers: dict[str, str],
payload: str | None,
) -> None:
"""Verify Recall/Svix-style webhook signatures on the raw request body."""
if not secret or not secret.startswith("whsec_"):
raise VerificationError("Verification secret is missing or invalid")
lowered = {k.lower(): v for k, v in headers.items()}
msg_id = lowered.get("webhook-id") or lowered.get("svix-id")
msg_timestamp = lowered.get("webhook-timestamp") or lowered.get("svix-timestamp")
msg_signature = lowered.get("webhook-signature") or lowered.get("svix-signature")
if not msg_id or not msg_timestamp or not msg_signature:
raise VerificationError("Missing webhook id, timestamp, or signature")
key = base64.b64decode(secret.removeprefix("whsec_"))
payload_str = payload or ""
to_sign = f"{msg_id}.{msg_timestamp}.{payload_str}"
expected_sig = base64.b64encode(
hmac.new(key, to_sign.encode("utf-8"), hashlib.sha256).digest()
).decode("ascii")
for versioned_sig in msg_signature.split(" "):
if "," not in versioned_sig:
continue
version, signature = versioned_sig.split(",", 1)
if version != "v1":
continue
if hmac.compare_digest(signature, expected_sig):
return
raise VerificationError("No matching signature found")
+295
View File
@@ -0,0 +1,295 @@
"""Meeting ingest pipeline — upload → transcribe → summarize → index.
Uses PraisonAI-Tools meeting tools with lazy imports so the UI app stays
lightweight at import time. Updates meeting metadata in SQLite between steps.
Status lives in meeting metadata (tools layer), not in UI state.
"""
from __future__ import annotations
import json
import logging
import re
import threading
from contextlib import closing
from pathlib import Path
from typing import Any
logger = logging.getLogger(__name__)
# Serializes metadata read-modify-write. Webhooks are dispatched one-thread-each
# (asyncio.to_thread), so concurrent transcript events would otherwise clobber
# each other's appends. Reentrant so callers can hold it across their own
# read + merge (see processor._upsert_live_transcript).
_metadata_lock = threading.RLock()
AUDIO_SUFFIXES = {".mp3", ".mp4", ".m4a", ".wav", ".webm", ".ogg", ".mpeg", ".mpga"}
_MEETING_ID_RE = re.compile(
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
re.I,
)
def merge_meeting_metadata(meeting_id: str, patch: dict[str, Any]) -> dict[str, Any]:
"""Merge keys into a meeting's metadata blob."""
from praisonai_tools.tools import meeting_tools as mt
with _metadata_lock:
record = mt.get_meeting.__wrapped__(meeting_id)
if "error" in record:
return record
meta = {**(record.get("metadata") or {}), **patch}
try:
with closing(mt._connect()) as conn, conn:
conn.execute(
"UPDATE meetings SET metadata = ? WHERE meeting_id = ?",
(json.dumps(meta, sort_keys=True), meeting_id),
)
except Exception as exc: # noqa: BLE001
logger.error("merge_meeting_metadata failed: %s", exc)
return {"error": str(exc)}
return {"meeting_id": meeting_id, "metadata": meta}
def _pipeline_from_transcribe(
meeting_id: str,
file_path: str,
meeting_title: str,
) -> dict[str, Any]:
"""Shared tail: transcribe → summarize → index for a known meeting_id."""
from praisonai_tools.tools.meeting_tools import (
extract_action_items,
index_meeting,
summarize_transcript,
transcribe_file,
)
path = Path(file_path)
if not path.exists():
merge_meeting_metadata(
meeting_id,
{"status": "failed", "error": f"File not found: {file_path}"},
)
return {"meeting_id": meeting_id, "error": f"File not found: {file_path}"}
merge_meeting_metadata(
meeting_id,
{"status": "transcribing", "file_path": str(path), "error": None},
)
transcript_result = transcribe_file.__wrapped__(str(path))
if "error" in transcript_result:
merge_meeting_metadata(
meeting_id,
{"status": "failed", "error": transcript_result["error"]},
)
return {"meeting_id": meeting_id, **transcript_result}
transcript = transcript_result.get("text", "")
segments = transcript_result.get("segments") or []
merge_meeting_metadata(
meeting_id,
{
"status": "summarizing",
"transcript": transcript,
"segments": segments,
"duration_seconds": transcript_result.get("duration_seconds"),
"language": transcript_result.get("language"),
},
)
summary_result = summarize_transcript.__wrapped__(transcript)
if "error" in summary_result:
merge_meeting_metadata(
meeting_id,
{"status": "failed", "error": summary_result["error"]},
)
return {"meeting_id": meeting_id, **summary_result}
actions_result = extract_action_items.__wrapped__(transcript)
if "error" in actions_result:
merge_meeting_metadata(
meeting_id,
{"status": "failed", "error": actions_result["error"]},
)
return {"meeting_id": meeting_id, **actions_result}
merge_meeting_metadata(
meeting_id,
{
"status": "indexing",
"summary": summary_result.get("summary", ""),
"decisions": summary_result.get("decisions", []),
"topics": summary_result.get("topics", []),
"key_quotes": summary_result.get("key_quotes", []),
"action_items": actions_result.get("action_items", []),
},
)
index_result = index_meeting.__wrapped__(
meeting_id,
transcript=transcript,
segments=segments,
)
if "error" in index_result:
merge_meeting_metadata(
meeting_id,
{"status": "failed", "error": index_result["error"]},
)
return {"meeting_id": meeting_id, **index_result}
merge_meeting_metadata(meeting_id, {"status": "ready", "error": None})
return {
"meeting_id": meeting_id,
"status": "ready",
"title": meeting_title,
"summary": summary_result.get("summary", ""),
"action_items": actions_result.get("action_items", []),
"chunks_indexed": index_result.get("chunks_indexed", 0),
}
def run_ingest_pipeline(
file_path: str,
title: str | None = None,
*,
meeting_id: str | None = None,
) -> dict[str, Any]:
"""Run the full Phase 1 pipeline for an audio/video upload."""
from praisonai_tools.tools.meeting_tools import get_meeting, save_meeting
path = Path(file_path)
if not path.exists():
return {"error": f"File not found: {file_path}"}
meeting_title = (title or path.stem).strip() or "Untitled meeting"
if meeting_id:
record = get_meeting.__wrapped__(meeting_id)
if "error" in record:
return record
if title:
meeting_title = title
elif record.get("title"):
meeting_title = record["title"]
else:
saved = save_meeting.__wrapped__(
title=meeting_title,
source=str(path),
metadata={"status": "transcribing", "file_path": str(path)},
)
if "error" in saved:
return saved
meeting_id = saved["meeting_id"]
return _pipeline_from_transcribe(meeting_id, str(path), meeting_title)
def run_post_transcript_pipeline(
meeting_id: str,
transcript: str,
*,
segments: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Run summarize → index for a Recall (or other) transcript without transcribe step."""
from praisonai_tools.tools.meeting_tools import (
extract_action_items,
get_meeting,
index_meeting,
summarize_transcript,
)
record = get_meeting.__wrapped__(meeting_id)
if "error" in record:
return record
meeting_title = record.get("title") or "Untitled meeting"
merge_meeting_metadata(
meeting_id,
{
"status": "summarizing",
"transcript": transcript,
"segments": segments or [],
"source": "recall",
"live_status": "ready",
"error": None,
},
)
summary_result = summarize_transcript.__wrapped__(transcript)
if "error" in summary_result:
merge_meeting_metadata(
meeting_id,
{"status": "failed", "error": summary_result["error"]},
)
return {"meeting_id": meeting_id, **summary_result}
actions_result = extract_action_items.__wrapped__(transcript)
if "error" in actions_result:
merge_meeting_metadata(
meeting_id,
{"status": "failed", "error": actions_result["error"]},
)
return {"meeting_id": meeting_id, **actions_result}
merge_meeting_metadata(
meeting_id,
{
"status": "indexing",
"summary": summary_result.get("summary", ""),
"decisions": summary_result.get("decisions", []),
"topics": summary_result.get("topics", []),
"key_quotes": summary_result.get("key_quotes", []),
"action_items": actions_result.get("action_items", []),
},
)
index_result = index_meeting.__wrapped__(
meeting_id,
transcript=transcript,
segments=segments or [],
)
if "error" in index_result:
merge_meeting_metadata(
meeting_id,
{"status": "failed", "error": index_result["error"]},
)
return {"meeting_id": meeting_id, **index_result}
merge_meeting_metadata(meeting_id, {"status": "ready", "error": None})
return {
"meeting_id": meeting_id,
"status": "ready",
"title": meeting_title,
"summary": summary_result.get("summary", ""),
"action_items": actions_result.get("action_items", []),
"chunks_indexed": index_result.get("chunks_indexed", 0),
}
def retry_pipeline(meeting_id: str) -> dict[str, Any]:
"""Re-run ingest for a failed meeting using its stored file path."""
if not meeting_id or not _MEETING_ID_RE.match(meeting_id):
return {"error": "invalid meeting_id", "meeting_id": meeting_id}
from praisonai_tools.tools.meeting_tools import get_meeting
record = get_meeting.__wrapped__(meeting_id)
if "error" in record:
return record
meta = record.get("metadata") or {}
file_path = meta.get("file_path") or record.get("source")
if not file_path:
return {
"meeting_id": meeting_id,
"error": "No uploaded file stored for this meeting",
}
return run_ingest_pipeline(
str(file_path),
title=record.get("title"),
meeting_id=meeting_id,
)
+57
View File
@@ -0,0 +1,57 @@
/**
* Meeting Agent dashboard hooks — auto-refresh live transcript on meeting-detail.
*/
(function () {
'use strict';
let pollTimer = null;
function stopPolling() {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
}
async function refreshLiveTranscript() {
const host = document.querySelector('[data-page="meeting-detail"]');
if (!host) return;
const code = host.querySelector('.db-code-block code');
if (!code) return;
try {
const meetingsRes = await fetch('/api/meetings');
if (!meetingsRes.ok) return;
const meetingsJson = await meetingsRes.json();
const meetings = meetingsJson.meetings || [];
const live = meetings.find((m) =>
['live', 'joining', 'waiting_room'].includes(String(m.live_status || '').toLowerCase())
);
if (!live || !live.id) return;
const snapRes = await fetch(`/api/meetings/${live.id}/live-transcript`);
if (!snapRes.ok) return;
const snap = await snapRes.json();
const text = snap.transcript || '(empty)';
if (code.textContent !== text) {
code.textContent = text;
}
} catch (_err) {
/* ignore transient network errors */
}
}
function startPolling() {
stopPolling();
refreshLiveTranscript();
pollTimer = setInterval(refreshLiveTranscript, 3000);
}
window.addEventListener('aiui:page-change', (event) => {
stopPolling();
if (event.detail && event.detail.pageId === 'meeting-detail') {
startPolling();
}
});
})();
@@ -0,0 +1,61 @@
# Poll live transcript for a meeting (Part B dev test).
# Usage:
# .\poll_live_transcript.ps1
# .\poll_live_transcript.ps1 -MeetingId "aecf34bc-7194-4123-9c67-a298b56011da"
# .\poll_live_transcript.ps1 -IntervalSec 2
param(
[string]$MeetingId = "",
[int]$Port = 8000,
[int]$IntervalSec = 3
)
$ErrorActionPreference = "Stop"
$base = "http://127.0.0.1:$Port"
if (-not $MeetingId) {
$meetings = Invoke-RestMethod -Uri "$base/api/meetings"
$live = $meetings.meetings | Where-Object { @("live","joining","waiting_room") -contains ($_.live_status).ToLower() } | Select-Object -First 1
if ($live) {
$MeetingId = $live.id
} elseif ($meetings.meetings.Count -gt 0) {
$MeetingId = $meetings.meetings[0].id
} else {
throw "No meetings found. Schedule a bot first."
}
}
$url = "$base/api/meetings/$MeetingId/live-transcript"
Write-Host "Polling $url every ${IntervalSec}s (Ctrl+C to stop)"
Write-Host ""
$lastCount = -1
$lastText = ""
while ($true) {
try {
$r = Invoke-RestMethod -Uri $url -TimeoutSec 10
$count = [int]$r.line_count
$status = $r.live_status
$text = [string]$r.transcript
if ($count -ne $lastCount -or $text -ne $lastText) {
$ts = Get-Date -Format "HH:mm:ss"
Write-Host "[$ts] live_status=$status lines=$count"
if ($text) {
Write-Host "---"
Write-Host $text
Write-Host "---"
} else {
Write-Host "(empty transcript)"
}
Write-Host ""
$lastCount = $count
$lastText = $text
}
} catch {
Write-Host "[$(Get-Date -Format 'HH:mm:ss')] ERROR: $($_.Exception.Message)"
}
Start-Sleep -Seconds $IntervalSec
}
@@ -0,0 +1,337 @@
"""Recall.ai HTTP routes for the meeting-agent example."""
from __future__ import annotations
import asyncio
import json
import logging
import os
from typing import Any
from starlette.requests import Request
from starlette.responses import JSONResponse, PlainTextResponse, Response, StreamingResponse
from meeting_integrations.recall.calendar import forward_calendar_callback
from meeting_integrations.recall.calendar_service import (
list_upcoming_calendar_meetings,
process_calendar_webhook,
sync_calendar_events,
)
from meeting_integrations.recall.config import RecallConfigError, load_recall_settings
from meeting_integrations.recall.live import snapshot_for_api, wait_for_update
from meeting_integrations.recall.processor import process_recall_webhook
from meeting_integrations.recall.service import cancel_recall_bot, schedule_recall_bot
from meeting_integrations.recall.store import RecallStore
from meeting_integrations.recall.verify import VerificationError, verify_request_from_recall
logger = logging.getLogger(__name__)
_calendar_regional_uri: str | None = None
# asyncio keeps only weak references to tasks, so a bare create_task() can be
# garbage-collected mid-flight and silently drop a webhook event. Hold a strong
# reference until each background task finishes.
_background_tasks: set[asyncio.Task] = set()
def _dispatch_background(coro) -> None:
task = asyncio.create_task(coro)
_background_tasks.add(task)
task.add_done_callback(_background_tasks.discard)
def set_calendar_regional_callback_uri(uri: str | None) -> None:
global _calendar_regional_uri
_calendar_regional_uri = uri
async def api_recall_schedule_bot(request: Request) -> JSONResponse:
try:
body = await request.json()
except json.JSONDecodeError:
return JSONResponse({"error": "invalid JSON body"}, status_code=400)
meeting_url = str(body.get("meeting_url") or "").strip()
title = str(body.get("title") or "").strip()
join_at = body.get("join_at")
join_at_str = str(join_at).strip() if join_at else None
result = await asyncio.to_thread(
schedule_recall_bot,
meeting_url,
title,
join_at=join_at_str,
)
status = 201 if result.get("recall_bot_id") else 400 if "error" in result else 500
return JSONResponse(result, status_code=status)
async def api_recall_cancel_bot(request: Request) -> JSONResponse:
meeting_id = request.path_params["meeting_id"]
result = await asyncio.to_thread(cancel_recall_bot, meeting_id)
status = 200 if "error" not in result else 400
return JSONResponse(result, status_code=status)
async def api_recall_bot_status(request: Request) -> JSONResponse:
meeting_id = request.path_params["meeting_id"]
try:
from praisonai_tools.tools.meeting_tools import get_meeting
record = get_meeting.__wrapped__(meeting_id)
except ImportError:
return JSONResponse({"error": "meeting tools unavailable"}, status_code=503)
if "error" in record:
return JSONResponse(record, status_code=404)
meta = record.get("metadata") or {}
return JSONResponse(
{
"meeting_id": meeting_id,
"title": record.get("title"),
"status": meta.get("status"),
"live_status": meta.get("live_status"),
"recall_bot_id": meta.get("recall_bot_id"),
"meeting_url": meta.get("meeting_url"),
"transcript_preview": (meta.get("transcript") or "")[:500],
}
)
async def webhook_recall(request: Request) -> Response:
raw = (await request.body()).decode("utf-8")
try:
settings = load_recall_settings()
except RecallConfigError as exc:
return JSONResponse({"error": str(exc)}, status_code=503)
headers = {k: v for k, v in request.headers.items()}
try:
verify_request_from_recall(
secret=settings.webhook_verification_secret,
headers=headers,
payload=raw,
)
except VerificationError:
return JSONResponse({"error": "invalid signature"}, status_code=401)
try:
payload = json.loads(raw) if raw else {}
except json.JSONDecodeError:
return JSONResponse({"error": "invalid JSON"}, status_code=400)
event_type = str(payload.get("event") or payload.get("type") or "unknown")
if event_type.startswith("calendar."):
_dispatch_background(
asyncio.to_thread(
process_calendar_webhook,
event_type,
payload,
settings=settings,
)
)
return PlainTextResponse("ok", status_code=200)
_dispatch_background(
asyncio.to_thread(process_recall_webhook, event_type, payload, settings=settings)
)
return PlainTextResponse("ok", status_code=200)
async def api_recall_calendar_callback(request: Request) -> Response:
if not _calendar_regional_uri:
return JSONResponse(
{
"error": "Calendar callback not configured. Bootstrap setup via "
"POST /api/recall/calendar/setup/bootstrap with regional_callback_uri "
"from Recall MCP start_calendar_integration_setup."
},
status_code=503,
)
params = dict(request.query_params)
status, body = await asyncio.to_thread(
forward_calendar_callback,
regional_callback_uri=_calendar_regional_uri,
query_params=params,
)
return PlainTextResponse(body, status_code=status)
async def api_recall_calendar_bootstrap(request: Request) -> JSONResponse:
try:
body = await request.json()
except json.JSONDecodeError:
return JSONResponse({"error": "invalid JSON"}, status_code=400)
setup_id = str(body.get("setup_id") or "").strip()
platform = str(body.get("platform") or "google_calendar").strip()
regional_uri = str(body.get("regional_callback_uri") or "").strip()
state = body.get("state") if isinstance(body.get("state"), dict) else {}
if not setup_id or not regional_uri:
return JSONResponse(
{"error": "setup_id and regional_callback_uri are required"},
status_code=400,
)
set_calendar_regional_callback_uri(regional_uri)
RecallStore.save_calendar_setup(
setup_id,
platform,
{
**state,
"regional_callback_uri": regional_uri,
"platform": platform,
},
)
callback_url = "(set PUBLIC_API_BASE_URL)"
if os.getenv("PUBLIC_API_BASE_URL", "").strip():
try:
callback_url = load_recall_settings().calendar_callback_url
except RecallConfigError:
pass
return JSONResponse(
{
"setup_id": setup_id,
"callback_url": callback_url,
"status": "bootstrapped",
}
)
async def api_recall_calendar_status(_request: Request) -> JSONResponse:
auto_record = os.getenv("CALENDAR_AUTO_RECORD", "true").strip().lower() not in {
"0",
"false",
"no",
"off",
}
public_url = os.getenv("PUBLIC_API_BASE_URL", "").strip()
calendars: list[dict[str, Any]] = []
try:
from meeting_integrations.recall.client import RecallClient
settings = load_recall_settings()
calendars = RecallClient(settings).list_calendars(status="connected")
except Exception as exc: # noqa: BLE001
logger.debug("Could not list calendars: %s", exc)
return JSONResponse(
{
"calendar_v2": {
"auto_record_eligible_events": auto_record,
"connected_calendars": len(calendars),
"calendars": [
{
"id": c.get("id"),
"platform": c.get("platform"),
"email": c.get("platform_email") or c.get("email"),
"status": c.get("status"),
}
for c in calendars
if isinstance(c, dict)
],
"callback_configured": bool(_calendar_regional_uri),
"public_api_base_url_set": bool(public_url),
"webhook_events": ["calendar.sync_events", "calendar.update"],
"next_step": (
"Register calendar webhooks on the same /webhooks/recall URL, "
"then complete Google Calendar V2 OAuth via Recall MCP."
),
}
}
)
async def api_recall_calendar_upcoming(request: Request) -> JSONResponse:
hours = int(request.query_params.get("hours", "24"))
try:
rows = await asyncio.to_thread(list_upcoming_calendar_meetings, hours=hours)
except RecallConfigError as exc:
return JSONResponse({"error": str(exc)}, status_code=503)
except Exception as exc: # noqa: BLE001
return JSONResponse({"error": str(exc)}, status_code=502)
return JSONResponse({"events": rows, "hours": hours})
async def api_recall_calendar_sync(request: Request) -> JSONResponse:
try:
body = await request.json()
except json.JSONDecodeError:
body = {}
calendar_id = str(body.get("calendar_id") or request.query_params.get("calendar_id") or "").strip()
if not calendar_id:
return JSONResponse({"error": "calendar_id is required"}, status_code=400)
try:
settings = load_recall_settings()
result = await asyncio.to_thread(
sync_calendar_events,
calendar_id,
updated_since=body.get("updated_at__gte"),
settings=settings,
)
except RecallConfigError as exc:
return JSONResponse({"error": str(exc)}, status_code=503)
except Exception as exc: # noqa: BLE001
return JSONResponse({"error": str(exc)}, status_code=502)
return JSONResponse(result)
async def api_meeting_live_transcript(request: Request) -> Response:
meeting_id = request.path_params["meeting_id"]
accept = request.headers.get("accept", "")
try:
from praisonai_tools.tools.meeting_tools import get_meeting
record = get_meeting.__wrapped__(meeting_id)
except ImportError:
return JSONResponse({"error": "meeting tools unavailable"}, status_code=503)
if "error" in record:
return JSONResponse(record, status_code=404)
if "text/event-stream" not in accept:
return JSONResponse(snapshot_for_api(meeting_id, record))
async def event_stream():
last_text = ""
while True:
try:
from praisonai_tools.tools.meeting_tools import get_meeting
record = get_meeting.__wrapped__(meeting_id)
except ImportError:
break
snap = snapshot_for_api(meeting_id, record)
text = snap.get("transcript") or ""
live = (snap.get("live_status") or "").lower()
if text != last_text:
yield f"data: {json.dumps(snap)}\n\n"
last_text = text
if live not in ("live", "joining", "waiting_room", "scheduled", ""):
break
await asyncio.to_thread(wait_for_update, meeting_id, 25.0)
return StreamingResponse(event_stream(), media_type="text/event-stream")
async def api_recall_config_status(_request: Request) -> JSONResponse:
try:
settings = load_recall_settings()
configured = True
except RecallConfigError as exc:
settings = None
configured = False
error = str(exc)
else:
error = None
return JSONResponse(
{
"configured": configured,
"error": error,
"region": settings.region if settings else os.getenv("RECALL_REGION"),
"workspace_id": settings.workspace_id if settings else None,
"webhook_url": settings.webhook_url if settings and settings.public_api_base_url else None,
"bot_name": settings.bot_name if settings else None,
}
)
@@ -0,0 +1,133 @@
"""Recover a Recall meeting when webhooks missed (stale tunnel URL).
Usage:
py -3.13 recover_recall_meeting.py <meeting_id> [bot_id]
"""
from __future__ import annotations
import sys
import time
from datetime import datetime
from pathlib import Path
_EXAMPLE_DIR = Path(__file__).resolve().parent
if str(_EXAMPLE_DIR) not in sys.path:
sys.path.insert(0, str(_EXAMPLE_DIR))
def _load_env() -> None:
import os
env_path = _EXAMPLE_DIR / ".env"
if not env_path.is_file():
return
for raw in env_path.read_text(encoding="utf-8").splitlines():
line = raw.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
key, value = key.strip(), value.strip()
if key and key not in os.environ:
os.environ[key] = value
def _duration_seconds(started_at: str | None, completed_at: str | None) -> int | None:
if not started_at or not completed_at:
return None
try:
fmt = "%Y-%m-%dT%H:%M:%S.%f%z"
start = datetime.strptime(started_at.replace("Z", "+0000"), fmt)
end = datetime.strptime(completed_at.replace("Z", "+0000"), fmt)
return int((end - start).total_seconds())
except (TypeError, ValueError):
return None
def recover(meeting_id: str, bot_id: str | None = None) -> dict:
from meeting_integrations.recall.client import RecallClient
from meeting_integrations.recall.config import load_recall_settings
from meeting_integrations.recall.transcript import transcript_download_to_text
from pipeline import merge_meeting_metadata, run_post_transcript_pipeline
from praisonai_tools.tools.meeting_tools import get_meeting
_load_env()
settings = load_recall_settings()
client = RecallClient(settings)
record = get_meeting.__wrapped__(meeting_id)
if "error" in record:
raise SystemExit(record["error"])
meta = record.get("metadata") or {}
bot_id = bot_id or meta.get("recall_bot_id")
if not bot_id:
raise SystemExit("No recall_bot_id — pass bot_id as second argument")
bot = client.get_bot(str(bot_id))
recordings = bot.get("recordings") or []
if not recordings:
raise SystemExit("Bot has no recordings yet")
rec_wrap = recordings[0].get("recording") or recordings[0]
recording_id = rec_wrap.get("id")
if not recording_id:
raise SystemExit("Missing recording id on bot")
duration = _duration_seconds(rec_wrap.get("started_at"), rec_wrap.get("completed_at"))
merge_meeting_metadata(
meeting_id,
{
"recall_bot_id": bot_id,
"recall_recording_id": recording_id,
"live_status": "processing",
"status": "transcribing",
**({"duration_seconds": duration} if duration else {}),
},
)
transcript_id = None
for _ in range(30):
bot = client.get_bot(str(bot_id))
recs = bot.get("recordings") or []
if recs:
rec = recs[0].get("recording") or recs[0]
shortcuts = rec.get("media_shortcuts") or {}
tr = shortcuts.get("transcript")
if isinstance(tr, dict) and tr.get("id"):
transcript_id = str(tr["id"])
break
artifacts = recs[0].get("transcript_artifacts") or rec.get("transcript_artifacts") or []
if artifacts:
transcript_id = str(artifacts[0].get("id") or "")
if transcript_id:
break
if not transcript_id:
print("Starting async transcript…")
client.create_async_transcript(str(recording_id))
time.sleep(10)
else:
break
if not transcript_id:
raise SystemExit("Timed out waiting for transcript — webhook may still deliver it")
artifact = client.get_transcript(transcript_id)
download_url = (artifact.get("data") or {}).get("download_url")
if not download_url:
raise SystemExit("Transcript ready but no download_url")
raw = client.download_json(download_url)
text = transcript_download_to_text(raw)
if not text.strip():
raise SystemExit("Transcript download was empty")
result = run_post_transcript_pipeline(meeting_id, text)
print("Recovered:", result)
return result
if __name__ == "__main__":
if len(sys.argv) < 2:
raise SystemExit("Usage: recover_recall_meeting.py <meeting_id> [bot_id]")
recover(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else None)
@@ -0,0 +1,87 @@
"""Non-binding smoke checks for Recall integration boundaries."""
from __future__ import annotations
import base64
import hashlib
import hmac
import json
import os
import sys
from pathlib import Path
from unittest.mock import patch
from starlette.testclient import TestClient
_EXAMPLE_DIR = Path(__file__).resolve().parent
_REPO_SRC = _EXAMPLE_DIR.parents[2] / "src"
for _p in (_EXAMPLE_DIR, str(_REPO_SRC)):
if str(_p) not in sys.path:
sys.path.insert(0, str(_p))
def _sign_payload(secret: str, body: str) -> dict[str, str]:
key = base64.b64decode(secret.removeprefix("whsec_"))
msg_id = "msg_smoke_test"
timestamp = "1700000000"
to_sign = f"{msg_id}.{timestamp}.{body}"
signature = base64.b64encode(
hmac.new(key, to_sign.encode("utf-8"), hashlib.sha256).digest()
).decode("ascii")
return {
"webhook-id": msg_id,
"webhook-timestamp": timestamp,
"webhook-signature": f"v1,{signature}",
}
def run_smoke() -> None:
os.environ.setdefault("RECALL_REGION", "eu-central-1")
os.environ.setdefault("RECALL_API_KEY", "smoke-test-key")
os.environ.setdefault("RECALL_WEBHOOK_VERIFICATION_SECRET", "whsec_" + base64.b64encode(b"smoke-secret-key-32-bytes-long!!").decode())
os.environ.setdefault("RECALL_WORKSPACE_ID", "smoke-workspace")
os.environ.setdefault("PUBLIC_API_BASE_URL", "https://smoke.example.test")
import app as meeting_app # noqa: WPS433
from meeting_integrations.recall.config import load_recall_settings
from meeting_integrations.recall.verify import verify_request_from_recall
settings = load_recall_settings()
assert settings.api_key == "smoke-test-key"
body = json.dumps({"event": "bot.joining_call", "data": {"bot": {"id": "b1", "metadata": {"meeting_id": "m1"}}}})
headers = _sign_payload(settings.webhook_verification_secret, body)
verify_request_from_recall(
secret=settings.webhook_verification_secret,
headers=headers,
payload=body,
)
client = TestClient(meeting_app.app)
assert client.get("/health").status_code == 200
bad = client.post("/webhooks/recall", content=body, headers={"webhook-id": "x"})
assert bad.status_code == 401
ok = client.post("/webhooks/recall", content=body, headers=headers)
assert ok.status_code == 200
with patch(
"recall_routes.schedule_recall_bot",
return_value={
"meeting_id": "m-smoke",
"recall_bot_id": "bot-smoke",
"status": "scheduled",
},
):
schedule = client.post(
"/api/recall/bots",
json={"meeting_url": "https://zoom.us/j/123", "title": "Smoke"},
)
assert schedule.status_code == 201
print("Recall smoke checks passed")
if __name__ == "__main__":
run_smoke()
+112
View File
@@ -0,0 +1,112 @@
# Start meeting-agent (Part B) + Cloudflare tunnel for Recall webhooks.
# Usage: .\start_dev.ps1
# Optional: $env:TEST_MEETING_URL = "https://meet.google.com/..." ; .\start_dev.ps1 -ScheduleBot
param(
[switch]$ScheduleBot,
[string]$MeetingUrl = $env:TEST_MEETING_URL,
[int]$Port = 8000
)
$ErrorActionPreference = "Stop"
$Root = Split-Path -Parent $MyInvocation.MyCommand.Path
Set-Location $Root
function Load-DotEnv {
$envFile = Join-Path $Root ".env"
if (-not (Test-Path $envFile)) { return }
Get-Content $envFile | ForEach-Object {
$line = $_.Trim()
if (-not $line -or $line.StartsWith("#") -or $line -notmatch "=") { return }
$key, $value = $line -split "=", 2
if ($key -and -not [string]::IsNullOrWhiteSpace($key) -and -not (Get-Item "Env:$key" -ErrorAction SilentlyContinue)) {
Set-Item -Path "Env:$key" -Value $value.Trim()
}
}
}
function Test-PortListening([int]$p) {
try {
$r = Invoke-WebRequest -Uri "http://127.0.0.1:$p/api/recall/config" -UseBasicParsing -TimeoutSec 2
return $r.StatusCode -eq 200
} catch { return $false }
}
function Update-PublicUrl([string]$baseUrl) {
$envFile = Join-Path $Root ".env"
$content = Get-Content $envFile -Raw
if ($content -match "PUBLIC_API_BASE_URL=.*") {
$newContent = $content -replace "PUBLIC_API_BASE_URL=.*", "PUBLIC_API_BASE_URL=$baseUrl"
} else {
$newContent = $content.TrimEnd() + "`nPUBLIC_API_BASE_URL=$baseUrl`n"
}
Set-Content -Path $envFile -Value $newContent -NoNewline
$env:PUBLIC_API_BASE_URL = $baseUrl
Write-Host "Updated .env PUBLIC_API_BASE_URL=$baseUrl"
}
Load-DotEnv
$env:MEETING_AGENT_PORT = "$Port"
if (-not (Test-PortListening $Port)) {
Write-Host "Starting meeting-agent on port $Port..."
Start-Process -FilePath "py" -ArgumentList "-3.13", "app.py" -WorkingDirectory $Root -WindowStyle Minimized
$deadline = (Get-Date).AddSeconds(45)
while ((Get-Date) -lt $deadline) {
if (Test-PortListening $Port) { break }
Start-Sleep -Seconds 2
}
if (-not (Test-PortListening $Port)) {
throw "App did not become ready on http://127.0.0.1:$Port"
}
Write-Host "App ready: http://127.0.0.1:$Port"
} else {
Write-Host "App already running on port $Port"
}
$public = $env:PUBLIC_API_BASE_URL
try {
$null = Invoke-WebRequest -Uri "$public/api/recall/config" -UseBasicParsing -TimeoutSec 5
Write-Host "Tunnel OK: $public"
} catch {
Write-Host "Starting Cloudflare tunnel (quick tunnel - URL may change)..."
$log = Join-Path $Root ".tunnel.log"
$logErr = "${log}.err"
if (Test-Path $log) { Remove-Item $log -Force }
if (Test-Path $logErr) { Remove-Item $logErr -Force }
Start-Process -FilePath "cloudflared" -ArgumentList "tunnel", "--url", "http://127.0.0.1:$Port" `
-RedirectStandardOutput $log -RedirectStandardError $logErr -WindowStyle Minimized
$deadline = (Get-Date).AddSeconds(90)
$newUrl = $null
while ((Get-Date) -lt $deadline -and -not $newUrl) {
Start-Sleep -Seconds 2
foreach ($path in @($logErr, $log)) {
if (-not (Test-Path $path)) { continue }
$match = Select-String -Path $path -Pattern "https://[a-z0-9-]+\.trycloudflare\.com" | Select-Object -First 1
if ($match) {
$newUrl = $match.Matches[0].Value
break
}
}
}
if (-not $newUrl) { throw "Could not read tunnel URL from $logErr (or $log). Check cloudflared is installed." }
Update-PublicUrl $newUrl
Write-Host "Tunnel ready: $newUrl"
Write-Host "If Recall webhook URL changed, update it in Recall dashboard Webhooks"
}
$config = Invoke-RestMethod -Uri "http://127.0.0.1:$Port/api/recall/config"
Write-Host "Recall configured: $($config.configured)"
if ($ScheduleBot) {
if (-not $MeetingUrl) {
Write-Host "No TEST_MEETING_URL - skip bot schedule. Set env TEST_MEETING_URL or pass -MeetingUrl"
} else {
$body = @{ meeting_url = $MeetingUrl; title = "Part B live transcript test" } | ConvertTo-Json
$result = Invoke-RestMethod -Method Post -Uri "http://127.0.0.1:$Port/api/recall/bots" `
-ContentType "application/json" -Body $body
Write-Host "Bot scheduled:" ($result | ConvertTo-Json -Compress)
}
}
Write-Host "Done. Dashboard: http://127.0.0.1:$Port"
+16
View File
@@ -0,0 +1,16 @@
# Voice Agent — local secrets. Never commit.
OPENAI_API_KEY=
VOICE_AGENT_PORT=8001
# Voice telephony provider
VOICE_API_BASE=
VOICE_PRIVATE_API_KEY=
VOICE_SERVER_URL_SECRET=
VOICE_ASSISTANT_ID=
VOICE_PHONE_NUMBER_ID=
# Public HTTPS URL for provider webhook (Cloudflare tunnel)
# cloudflared tunnel --url http://127.0.0.1:8001
PUBLIC_API_BASE_URL=
+54
View File
@@ -0,0 +1,54 @@
# Voice Agent example — PraisonAIUI
Phone and web voice agents using an external telephony/voice platform for STT/TTS/calls and PraisonAI for tools + dashboard.
## Quick start
```powershell
cd examples/python/voice-agent
copy .env.example .env
# Fill VOICE_API_BASE, VOICE_PRIVATE_API_KEY, VOICE_ASSISTANT_ID, etc.
# Fully automatic (app + tunnel + provider config):
.\setup_all.ps1
# Or step by step:
.\start_dev.ps1
```
Dashboard: http://127.0.0.1:8001
## Provider setup
1. Create an **assistant** with tools `get_current_time` and `echo_message`
2. Set **webhook URL** to `{PUBLIC_API_BASE_URL}/webhooks/voice`
3. Set **webhook secret header** `X-Voice-Webhook-Secret` → same as `VOICE_SERVER_URL_SECRET` in `.env`
4. Add a phone number and copy `VOICE_PHONE_NUMBER_ID`
Or run `py -3.13 setup_voice_provider.py` after `start_dev.ps1`.
## API
| Route | Method | Purpose |
|-------|--------|---------|
| `/api/voice/calls` | POST | Outbound call `{ "customer_number": "+1..." }` |
| `/api/voice/calls` | GET | List stored calls |
| `/api/voice/calls/{id}` | GET | Call detail + transcript |
| `/webhooks/voice` | POST | Provider webhooks (tool-calls, transcript, end-of-call) |
| `/api/voice/config` | GET | Config status |
## Architecture
External platform handles voice pipeline (phone, STT, TTS, turn-taking). This app handles:
- **tool-calls** → PraisonAI-registered tools (`integrations/voice/tools.py`)
- **transcript** / **end-of-call-report** → SQLite store + dashboard pages
- **assistant-request** → returns `VOICE_ASSISTANT_ID`
Same pattern as `examples/python/meeting-agent` (Recall webhooks).
## Tests
```bash
pytest tests/unit/test_voice_agent.py -v
```
+208
View File
@@ -0,0 +1,208 @@
"""Voice Agent — PraisonAIUI example with telephony webhooks.
- Outbound calls → POST /api/voice/calls
- Provider webhooks → POST /webhooks/voice (tool-calls, transcript, end-of-call)
- Dashboard → Calls list + call detail + chat
Run:
cd examples/python/voice-agent
pip install httpx
copy .env.example .env
.\\start_dev.ps1
python app.py
"""
from __future__ import annotations
import asyncio
import os
import sys
from pathlib import Path
from typing import Any
from starlette.routing import Route
from voice_routes import (
api_voice_call_detail,
api_voice_config_status,
api_voice_create_call,
api_voice_list_calls,
webhook_voice,
)
import praisonaiui as aiui
from praisonaiui.server import create_app
_EXAMPLE_DIR = Path(__file__).resolve().parent
if str(_EXAMPLE_DIR) not in sys.path:
sys.path.insert(0, str(_EXAMPLE_DIR))
def _load_local_env() -> None:
env_path = _EXAMPLE_DIR / ".env"
if not env_path.is_file():
return
for raw in env_path.read_text(encoding="utf-8").splitlines():
line = raw.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
key, value = key.strip(), value.strip()
if key and key not in os.environ:
os.environ[key] = value
_load_local_env()
aiui.set_pages(["chat", "voice-calls", "call-detail", "config"])
aiui.set_style("dashboard")
_agent: Any | None = None
def get_agent():
global _agent
if _agent is not None:
return _agent
from integrations.voice.tools import echo_message, get_current_time, register_tool
from praisonaiagents import Agent
register_tool("get_current_time", get_current_time)
register_tool("echo_message", echo_message)
_agent = Agent(
name="Voice Assistant",
instructions=(
"You help with voice and phone agent demos. "
"Use tools when the caller asks for the time or wants a message echoed."
),
model="gpt-4o-mini",
tools=[get_current_time, echo_message],
)
from praisonaiagents.escalation.loop_guard import LoopGuard, LoopGuardConfig
max_turn_sec = float(os.getenv("VOICE_AGENT_LOOP_GUARD_MAX_SEC", "600"))
_agent._loop_guard = LoopGuard(
LoopGuardConfig(enabled=True, max_time_per_turn=max_turn_sec)
)
return _agent
def _demo_tool_reply(text: str) -> str | None:
"""Fast path for demo tool queries — no LLM required."""
from integrations.voice.tools import echo_message, get_current_time
lower = text.lower().strip()
if any(p in lower for p in ("what time", "current time", "tell me the time", "time is it")):
return str(get_current_time()["spoken"])
if lower.startswith("echo "):
return str(echo_message(text[5:])["spoken"])
if lower.startswith("repeat "):
return str(echo_message(text[7:])["spoken"])
return None
@aiui.reply
async def on_message(message: str):
"""Chat with the voice demo agent."""
text = str(message).strip()
if not text:
return
demo = _demo_tool_reply(text)
if demo is not None:
await aiui.say(demo)
return
if not os.getenv("OPENAI_API_KEY"):
await aiui.say(
"Set `OPENAI_API_KEY` for open-ended chat. "
'Demo without API key: "What time is it?" or "Echo hello world".'
)
return
await aiui.think("Running voice assistant...")
agent = get_agent()
response = await asyncio.to_thread(agent.chat, text)
await aiui.say(str(response))
def _list_calls() -> list[dict[str, Any]]:
from integrations.voice.store import VoiceCallStore
rows = VoiceCallStore.list_calls()
return [
{
"id": r["call_id"],
"status": r["status"],
"customer_number": r.get("customer_number") or "—",
"transcript_preview": (r.get("transcript") or "")[:120],
"updated_at": r.get("updated_at") or "",
}
for r in rows
]
def _pick_call(calls: list[dict[str, Any]]) -> dict[str, Any]:
live = next((c for c in calls if "progress" in (c.get("status") or "").lower()), None)
return live or (calls[0] if calls else {"id": "—", "status": "none", "customer_number": "—", "transcript_preview": ""})
@aiui.page("voice-calls", title="Voice calls", icon="📞", group="Voice", order=1)
async def voice_calls_page():
calls = _list_calls()
rows = [[c["id"], c["customer_number"], c["status"], c["updated_at"]] for c in calls]
return aiui.layout(
[
aiui.text("Outbound and inbound voice calls via telephony provider"),
aiui.table(
headers=["Call ID", "Customer", "Status", "Updated"],
rows=rows or [["—", "—", "—", "—"]],
),
aiui.alert(
"Start a call: POST /api/voice/calls with {\"customer_number\": \"+1...\"}. "
"Set provider webhook URL to {PUBLIC_API_BASE_URL}/webhooks/voice",
variant="info",
title="Setup",
),
]
)
@aiui.page("call-detail", title="Call detail", icon="🎙️", group="Voice", order=2)
async def call_detail_page():
from integrations.voice.store import VoiceCallStore
calls = _list_calls()
picked = _pick_call(calls)
record = VoiceCallStore.get_call(picked["id"]) if picked.get("id") and picked["id"] != "—" else None
transcript = (record or {}).get("transcript") or "(empty)"
summary = (record or {}).get("summary") or "(not available yet)"
status = (record or {}).get("status") or picked.get("status") or "unknown"
return aiui.layout(
[
aiui.text(f"Call {picked.get('id', '—')}"),
aiui.badge(status, variant="default"),
aiui.tabs(
[
{"label": "Transcript", "children": [aiui.code_block(transcript, language="text")]},
{"label": "Summary", "children": [aiui.text(summary)]},
]
),
]
)
app = create_app()
app.routes[0:0] = [
Route("/api/voice/calls", api_voice_create_call, methods=["POST"]),
Route("/api/voice/calls", api_voice_list_calls, methods=["GET"]),
Route("/api/voice/calls/{call_id}", api_voice_call_detail, methods=["GET"]),
Route("/api/voice/config", api_voice_config_status, methods=["GET"]),
Route("/webhooks/voice", webhook_voice, methods=["POST"]),
]
if __name__ == "__main__":
import uvicorn
host = os.environ.get("HOST", "127.0.0.1")
port = int(os.environ.get("VOICE_AGENT_PORT", "8001"))
uvicorn.run(app, host=host, port=port)
@@ -0,0 +1 @@
"""Voice agent integrations."""
@@ -0,0 +1 @@
"""Voice telephony provider integration."""
@@ -0,0 +1,76 @@
"""Minimal voice provider REST client."""
from __future__ import annotations
import logging
from typing import Any
from integrations.voice.config import VoiceSettings
logger = logging.getLogger(__name__)
class VoiceAPIError(RuntimeError):
def __init__(self, status_code: int, detail: str):
super().__init__(detail)
self.status_code = status_code
self.detail = detail
class VoiceClient:
"""HTTP client for voice provider calls and assistants."""
def __init__(self, settings: VoiceSettings):
self._settings = settings
def _headers(self) -> dict[str, str]:
return {
"Authorization": f"Bearer {self._settings.api_key}",
"Accept": "application/json",
"Content-Type": "application/json",
}
def _request(
self,
method: str,
path: str,
*,
json_body: dict[str, Any] | None = None,
) -> dict[str, Any]:
import httpx # lazy: optional dependency for the example (pip install httpx)
url = f"{self._settings.api_base.rstrip('/')}{path}"
with httpx.Client(timeout=30.0) as client:
response = client.request(method, url, headers=self._headers(), json=json_body)
if response.status_code >= 400:
raise VoiceAPIError(response.status_code, response.text[:500] or response.reason_phrase)
if not response.content:
return {}
data = response.json()
return data if isinstance(data, dict) else {"data": data}
def create_call(
self,
*,
customer_number: str,
assistant_id: str | None = None,
phone_number_id: str | None = None,
metadata: dict[str, Any] | None = None,
) -> dict[str, Any]:
assistant_id = assistant_id or self._settings.default_assistant_id
phone_number_id = phone_number_id or self._settings.default_phone_number_id
if not assistant_id:
raise VoiceAPIError(0, "assistant_id is required (set VOICE_ASSISTANT_ID or pass explicitly)")
if not phone_number_id:
raise VoiceAPIError(0, "phone_number_id is required (set VOICE_PHONE_NUMBER_ID or pass explicitly)")
body: dict[str, Any] = {
"assistantId": assistant_id,
"phoneNumberId": phone_number_id,
"customer": {"number": customer_number},
}
if metadata:
body["metadata"] = metadata
return self._request("POST", "/call", json_body=body)
def get_call(self, call_id: str) -> dict[str, Any]:
return self._request("GET", f"/call/{call_id}")
@@ -0,0 +1,65 @@
"""Voice provider configuration loaded from environment."""
from __future__ import annotations
import os
from dataclasses import dataclass
from pathlib import Path
class VoiceConfigError(ValueError):
"""Missing or invalid voice provider configuration."""
@dataclass(frozen=True)
class VoiceSettings:
api_key: str
server_url_secret: str | None
public_api_base_url: str | None
default_assistant_id: str | None
default_phone_number_id: str | None
api_base: str
@property
def webhook_url(self) -> str | None:
if not self.public_api_base_url:
return None
return f"{self.public_api_base_url.rstrip('/')}/webhooks/voice"
@property
def configured(self) -> bool:
return bool(self.api_key and self.api_base)
def _read_dotenv(name: str) -> str | None:
root = Path(__file__).resolve().parents[2]
env_path = root / ".env"
if not env_path.is_file():
return None
prefix = f"{name}="
for raw in env_path.read_text(encoding="utf-8").splitlines():
line = raw.strip()
if line.startswith(prefix):
return line[len(prefix) :].strip() or None
return None
def _env(name: str) -> str:
return os.getenv(name, "").strip() or (_read_dotenv(name) or "")
def load_voice_settings(*, require_key: bool = True) -> VoiceSettings:
api_key = _env("VOICE_PRIVATE_API_KEY") or _env("VOICE_API_KEY")
api_base = _env("VOICE_API_BASE")
if require_key and not api_key:
raise VoiceConfigError("VOICE_PRIVATE_API_KEY (or VOICE_API_KEY) is required")
if require_key and not api_base:
raise VoiceConfigError("VOICE_API_BASE is required")
return VoiceSettings(
api_key=api_key,
server_url_secret=_env("VOICE_SERVER_URL_SECRET") or None,
public_api_base_url=_env("PUBLIC_API_BASE_URL") or None,
default_assistant_id=_env("VOICE_ASSISTANT_ID") or None,
default_phone_number_id=_env("VOICE_PHONE_NUMBER_ID") or None,
api_base=api_base.rstrip("/"),
)
@@ -0,0 +1,115 @@
"""Process voice provider webhook events."""
from __future__ import annotations
import json
import logging
from typing import Any
from integrations.voice.config import VoiceSettings
from integrations.voice.store import VoiceCallStore, webhook_event_key
from integrations.voice.tools import execute_tool
logger = logging.getLogger(__name__)
def _message(payload: dict[str, Any]) -> dict[str, Any]:
msg = payload.get("message")
return msg if isinstance(msg, dict) else payload
def _call_id(payload: dict[str, Any]) -> str | None:
msg = _message(payload)
call = msg.get("call")
if isinstance(call, dict) and call.get("id"):
return str(call["id"])
return None
def _customer_number(payload: dict[str, Any]) -> str | None:
msg = _message(payload)
call = msg.get("call") if isinstance(msg.get("call"), dict) else {}
customer = call.get("customer") if isinstance(call, dict) else {}
if isinstance(customer, dict) and customer.get("number"):
return str(customer["number"])
return None
def process_voice_webhook(
event_type: str, payload: dict[str, Any], *, settings: VoiceSettings
) -> dict[str, Any] | None:
"""Handle a verified provider webhook POST. Returns response body when required."""
event_key = webhook_event_key(event_type, payload)
if event_type not in ("tool-calls", "assistant-request", "transfer-destination-request", "knowledge-base-request"):
if not VoiceCallStore.mark_event_processed(event_key, event_type):
logger.info("Skipping duplicate voice event %s", event_type)
return None
msg = _message(payload)
call_id = _call_id(payload)
if event_type == "assistant-request":
assistant_id = settings.default_assistant_id
if not assistant_id:
return {"error": "No assistant configured on server"}
return {"assistantId": assistant_id}
if event_type == "tool-calls" and call_id:
tool_calls = msg.get("toolCallList") or []
results = []
for item in tool_calls:
if not isinstance(item, dict):
continue
tool_call_id = str(item.get("id") or "")
name = str(item.get("name") or "")
params = item.get("parameters") if isinstance(item.get("parameters"), dict) else {}
result = execute_tool(name, params)
results.append({"toolCallId": tool_call_id, "name": name, "result": result})
VoiceCallStore.upsert_call(call_id, status="in-progress", customer_number=_customer_number(payload))
return {"results": results}
if event_type == "status-update" and call_id:
status = str(msg.get("status") or "unknown")
VoiceCallStore.upsert_call(call_id, status=status, customer_number=_customer_number(payload))
return None
if event_type == "transcript" and call_id:
role = str(msg.get("role") or "unknown")
text = str(msg.get("transcript") or "").strip()
if text and str(msg.get("transcriptType") or "final") == "final":
VoiceCallStore.append_transcript_line(call_id, f"{role}: {text}")
return None
if event_type == "end-of-call-report" and call_id:
artifact = msg.get("artifact") if isinstance(msg.get("artifact"), dict) else {}
transcript = artifact.get("transcript") if isinstance(artifact, dict) else None
summary = str(msg.get("summary") or "")
ended = str(msg.get("endedReason") or "ended")
VoiceCallStore.upsert_call(
call_id,
status=f"ended ({ended})",
customer_number=_customer_number(payload),
transcript=str(transcript) if transcript else None,
summary=summary or None,
metadata={"artifact": artifact},
)
return None
if event_type == "conversation-update" and call_id:
messages = msg.get("messages") or []
lines = []
for item in messages:
if not isinstance(item, dict):
continue
role = item.get("role") or "unknown"
text = item.get("message") or item.get("content") or ""
if text:
lines.append(f"{role}: {text}")
if lines:
VoiceCallStore.upsert_call(
call_id, transcript="\n".join(lines), customer_number=_customer_number(payload)
)
return None
logger.debug("Unhandled voice event %s: %s", event_type, json.dumps(payload)[:300])
return None
@@ -0,0 +1,204 @@
"""Persist voice call state and webhook idempotency."""
from __future__ import annotations
import json
import os
import sqlite3
from contextlib import closing
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
def _state_db_path() -> str:
env = os.getenv("PRAISONAI_VOICE_DIR")
base = Path(env) if env else Path.home() / ".praisonai" / "voice"
base.mkdir(parents=True, exist_ok=True)
return str(base / "voice_state.db")
def _connect() -> sqlite3.Connection:
conn = sqlite3.connect(_state_db_path())
conn.execute(
"""
CREATE TABLE IF NOT EXISTS voice_calls (
call_id TEXT PRIMARY KEY,
status TEXT NOT NULL,
customer_number TEXT,
transcript TEXT NOT NULL DEFAULT '',
summary TEXT NOT NULL DEFAULT '',
metadata_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS voice_webhook_events (
event_key TEXT PRIMARY KEY,
event_type TEXT NOT NULL,
processed_at TEXT NOT NULL
)
"""
)
return conn
def webhook_event_key(event_type: str, payload: dict[str, Any]) -> str:
message = payload.get("message") if isinstance(payload.get("message"), dict) else payload
call = (message or {}).get("call") if isinstance(message, dict) else {}
call_id = call.get("id") if isinstance(call, dict) else None
if isinstance(message, dict):
# Derive the key from the fields that actually distinguish one event from
# the next. A truncated JSON dump is unsafe here: when a large static field
# (e.g. ``call``) sorts before the discriminating field, successive events
# collapse to the same prefix and later ones get dropped as "duplicates"
# (e.g. a status-update stuck on "ringing").
if event_type == "transcript":
text = str(message.get("transcript") or "")
role = str(message.get("role") or "")
kind = str(message.get("transcriptType") or "")
return f"{event_type}:{call_id}:{role}:{kind}:{text}"
if event_type == "status-update":
return f"{event_type}:{call_id}:{message.get('status')}"
if event_type == "conversation-update":
messages = message.get("messages")
count = len(messages) if isinstance(messages, list) else 0
return f"{event_type}:{call_id}:{count}"
if event_type == "end-of-call-report":
return f"{event_type}:{call_id}:{message.get('endedReason')}"
if call_id:
return f"{event_type}:{call_id}:{json.dumps(message, sort_keys=True)[:200]}"
return f"{event_type}:{json.dumps(payload, sort_keys=True)[:300]}"
class VoiceCallStore:
@staticmethod
def mark_event_processed(event_key: str, event_type: str) -> bool:
now = datetime.now(timezone.utc).isoformat()
with closing(_connect()) as conn, conn:
row = conn.execute(
"SELECT 1 FROM voice_webhook_events WHERE event_key = ?",
(event_key,),
).fetchone()
if row:
return False
conn.execute(
"""
INSERT INTO voice_webhook_events (event_key, event_type, processed_at)
VALUES (?, ?, ?)
""",
(event_key, event_type, now),
)
return True
@staticmethod
def upsert_call(
call_id: str,
*,
status: str | None = None,
customer_number: str | None = None,
transcript: str | None = None,
summary: str | None = None,
metadata: dict[str, Any] | None = None,
) -> None:
now = datetime.now(timezone.utc).isoformat()
with closing(_connect()) as conn, conn:
row = conn.execute(
"SELECT transcript, summary, metadata_json, customer_number, status FROM voice_calls WHERE call_id = ?",
(call_id,),
).fetchone()
existing_transcript = row[0] if row else ""
existing_summary = row[1] if row else ""
existing_meta = json.loads(row[2]) if row and row[2] else {}
existing_number = row[3] if row else None
existing_status = row[4] if row else None
# Preserve prior status when the caller doesn't supply one (e.g. transcript
# / conversation-update events) so partial updates don't clobber live state.
final_status = status if status is not None else (existing_status or "unknown")
merged_meta = {**existing_meta, **(metadata or {})}
conn.execute(
"""
INSERT INTO voice_calls (
call_id, status, customer_number, transcript, summary, metadata_json, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(call_id) DO UPDATE SET
status = excluded.status,
customer_number = COALESCE(excluded.customer_number, voice_calls.customer_number),
transcript = excluded.transcript,
summary = COALESCE(excluded.summary, voice_calls.summary),
metadata_json = excluded.metadata_json,
updated_at = excluded.updated_at
""",
(
call_id,
final_status,
customer_number or existing_number,
transcript if transcript is not None else existing_transcript,
summary if summary is not None else existing_summary,
json.dumps(merged_meta, sort_keys=True),
now,
now,
),
)
@staticmethod
def append_transcript_line(call_id: str, line: str) -> str:
with closing(_connect()) as conn:
row = conn.execute(
"SELECT transcript FROM voice_calls WHERE call_id = ?",
(call_id,),
).fetchone()
existing = row[0] if row else ""
full = f"{existing}\n{line}".strip() if existing else line
VoiceCallStore.upsert_call(call_id, transcript=full)
return full
@staticmethod
def list_calls(limit: int = 50) -> list[dict[str, Any]]:
with closing(_connect()) as conn:
rows = conn.execute(
"""
SELECT call_id, status, customer_number, transcript, summary, metadata_json, updated_at
FROM voice_calls ORDER BY updated_at DESC LIMIT ?
""",
(max(1, limit),),
).fetchall()
out: list[dict[str, Any]] = []
for row in rows:
out.append(
{
"call_id": row[0],
"status": row[1],
"customer_number": row[2],
"transcript": row[3],
"summary": row[4],
"metadata": json.loads(row[5] or "{}"),
"updated_at": row[6],
}
)
return out
@staticmethod
def get_call(call_id: str) -> dict[str, Any] | None:
with closing(_connect()) as conn:
row = conn.execute(
"""
SELECT call_id, status, customer_number, transcript, summary, metadata_json, updated_at
FROM voice_calls WHERE call_id = ?
""",
(call_id,),
).fetchone()
if not row:
return None
return {
"call_id": row[0],
"status": row[1],
"customer_number": row[2],
"transcript": row[3],
"summary": row[4],
"metadata": json.loads(row[5] or "{}"),
"updated_at": row[6],
}
@@ -0,0 +1,49 @@
"""Tools callable from voice provider ``tool-calls`` webhook events."""
from __future__ import annotations
import json
from datetime import datetime, timezone
from typing import Any, Callable
def get_current_time() -> dict[str, Any]:
"""Return current UTC time for voice agent demos."""
now = datetime.now(timezone.utc).isoformat()
return {"utc": now, "spoken": f"The current UTC time is {now}"}
def echo_message(message: str = "") -> dict[str, Any]:
"""Echo a message back to the caller."""
text = message.strip() or "nothing"
return {"message": text, "spoken": f"You said {text}"}
_TOOL_REGISTRY: dict[str, Callable[..., Any]] = {
"get_current_time": get_current_time,
"echo_message": echo_message,
}
def register_tool(name: str, fn: Callable[..., Any]) -> None:
_TOOL_REGISTRY[name] = fn
def execute_tool(name: str, parameters: dict[str, Any] | None) -> str:
"""Run a registered tool and return a JSON string for the provider."""
fn = _TOOL_REGISTRY.get(name)
if fn is None:
return json.dumps({"error": f"Unknown tool: {name}"})
try:
result = fn(**(parameters or {}))
if isinstance(result, str):
return result
return json.dumps(result)
except TypeError:
try:
result = fn(parameters or {})
return json.dumps(result)
except Exception as exc: # noqa: BLE001
return json.dumps({"error": str(exc)})
except Exception as exc: # noqa: BLE001
return json.dumps({"error": str(exc)})
@@ -0,0 +1,21 @@
"""Verify incoming voice provider webhook requests."""
from __future__ import annotations
import hmac
_SECRET_HEADERS = ("x-voice-webhook-secret", "x-webhook-secret")
class VerificationError(ValueError):
"""Webhook secret verification failed."""
def verify_webhook_request(*, secret: str | None, headers: dict[str, str]) -> None:
"""Validate webhook secret header when ``VOICE_SERVER_URL_SECRET`` is configured."""
if not secret:
return
lowered = {k.lower(): v for k, v in headers.items()}
incoming = next((lowered[name] for name in _SECRET_HEADERS if lowered.get(name)), None)
if not incoming or not hmac.compare_digest(incoming, secret):
raise VerificationError("Invalid or missing webhook secret header")
+11
View File
@@ -0,0 +1,11 @@
# One command: start app + tunnel + configure voice provider automatically.
Set-Location $PSScriptRoot
if (-not (Test-Path .env)) { Copy-Item .env.example .env; Write-Host "Created .env — add VOICE_* keys" ; exit 1 }
.\start_dev.ps1
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Write-Host ""
Write-Host "Configuring voice provider via API..." -ForegroundColor Cyan
py -3.13 setup_voice_provider.py
exit $LASTEXITCODE
@@ -0,0 +1,246 @@
"""Configure voice provider assistant: tools + webhook URL + auth header.
Usage:
cd examples/python/voice-agent
py -3.13 setup_voice_provider.py
"""
from __future__ import annotations
import os
import uuid
from pathlib import Path
from typing import Any
import httpx
_ROOT = Path(__file__).resolve().parent
ENV_PATH = _ROOT / ".env"
WEBHOOK_SECRET_HEADER = "X-Voice-Webhook-Secret"
def _load_env() -> None:
if not ENV_PATH.is_file():
return
for raw in ENV_PATH.read_text(encoding="utf-8").splitlines():
line = raw.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
key, value = key.strip(), value.strip()
if key and key not in os.environ:
os.environ[key] = value
def _write_env_value(key: str, value: str) -> None:
lines = ENV_PATH.read_text(encoding="utf-8").splitlines() if ENV_PATH.is_file() else []
updated = False
new_lines: list[str] = []
for line in lines:
if line.strip().startswith(f"{key}="):
new_lines.append(f"{key}={value}")
updated = True
else:
new_lines.append(line)
if not updated:
new_lines.append(f"{key}={value}")
ENV_PATH.write_text("\n".join(new_lines) + "\n", encoding="utf-8")
os.environ[key] = value
def _api_key() -> str:
return os.getenv("VOICE_PRIVATE_API_KEY", "").strip() or os.getenv("VOICE_API_KEY", "").strip()
def _api_base() -> str:
return os.getenv("VOICE_API_BASE", "").strip().rstrip("/")
def _request(
method: str,
path: str,
*,
api_key: str,
api_base: str,
body: dict | None = None,
) -> dict | list | None:
url = f"{api_base}{path}"
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
response = httpx.request(method, url, headers=headers, json=body, timeout=30.0)
if response.status_code >= 400:
print(f"ERROR {method} {path} -> {response.status_code}")
print(response.text[:1500])
return None
if not response.content:
return {}
return response.json()
def _tool_specs() -> list[dict[str, Any]]:
return [
{
"type": "function",
"function": {
"name": "get_current_time",
"description": "Returns the current UTC time when the caller asks for the time.",
"parameters": {"type": "object", "properties": {}, "required": []},
},
"async": False,
},
{
"type": "function",
"function": {
"name": "echo_message",
"description": "Repeats back a message the caller provides.",
"parameters": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "The message to repeat back to the caller.",
}
},
"required": ["message"],
},
},
"async": False,
},
]
def ensure_server_secret() -> str:
secret = os.getenv("VOICE_SERVER_URL_SECRET", "").strip()
if not secret:
secret = str(uuid.uuid4())
_write_env_value("VOICE_SERVER_URL_SECRET", secret)
print("Generated VOICE_SERVER_URL_SECRET")
return secret
def ensure_tools(api_key: str, api_base: str, assistant_id: str) -> list[str]:
existing = _request("GET", "/tool", api_key=api_key, api_base=api_base)
by_name: dict[str, str] = {}
if isinstance(existing, list):
for item in existing:
fn = item.get("function") if isinstance(item, dict) else None
if isinstance(fn, dict) and fn.get("name") and item.get("id"):
by_name[str(fn["name"])] = str(item["id"])
tool_ids: list[str] = []
for spec in _tool_specs():
name = spec["function"]["name"]
if name in by_name:
tool_id = by_name[name]
print(f"Reusing tool {name}: {tool_id}")
else:
created = _request("POST", "/tool", api_key=api_key, api_base=api_base, body=spec)
if not isinstance(created, dict) or not created.get("id"):
raise RuntimeError(f"Failed to create tool {name}")
tool_id = str(created["id"])
print(f"Created tool {name}: {tool_id}")
tool_ids.append(tool_id)
assistant = _request("GET", f"/assistant/{assistant_id}", api_key=api_key, api_base=api_base)
if not isinstance(assistant, dict):
raise RuntimeError("Could not load assistant")
model = dict(assistant.get("model") or {})
if not model.get("provider"):
raise RuntimeError("Assistant has no model.provider — configure a model in the provider dashboard first")
existing_ids = [str(x) for x in (model.get("toolIds") or [])]
model["toolIds"] = list(dict.fromkeys(existing_ids + tool_ids))
messages = list(model.get("messages") or [])
system_hint = (
"You are a helpful voice assistant. When the caller asks for the time, "
"call get_current_time. When they ask you to repeat something, call echo_message."
)
if not messages:
model["messages"] = [{"role": "system", "content": system_hint}]
else:
for msg in messages:
if isinstance(msg, dict) and msg.get("role") == "system":
content = str(msg.get("content") or "")
if "get_current_time" not in content:
msg["content"] = f"{content.rstrip()}\n\n{system_hint}"
break
else:
messages.insert(0, {"role": "system", "content": system_hint})
model["messages"] = messages
patched = _request(
"PATCH",
f"/assistant/{assistant_id}",
api_key=api_key,
api_base=api_base,
body={"model": model},
)
if patched is None:
raise RuntimeError("Failed to attach tools to assistant")
print("Attached tool IDs:", model["toolIds"])
return tool_ids
def ensure_webhook(api_key: str, api_base: str, assistant_id: str, webhook_url: str, secret: str) -> None:
assistant = _request("GET", f"/assistant/{assistant_id}", api_key=api_key, api_base=api_base)
if not isinstance(assistant, dict):
raise RuntimeError("Could not load assistant for webhook setup")
server = dict(assistant.get("server") or {})
server["url"] = webhook_url.rstrip("/")
server["headers"] = {WEBHOOK_SECRET_HEADER: secret}
server.pop("credentialId", None)
server.pop("secret", None)
patched = _request(
"PATCH",
f"/assistant/{assistant_id}",
api_key=api_key,
api_base=api_base,
body={"server": server},
)
if patched is None:
raise RuntimeError("Failed to configure assistant server URL")
print(f"Server URL: {webhook_url}")
print(f"Webhook auth header: {WEBHOOK_SECRET_HEADER}")
def main() -> int:
_load_env()
api_key = _api_key()
api_base = _api_base()
assistant_id = os.getenv("VOICE_ASSISTANT_ID", "").strip()
public_base = os.getenv("PUBLIC_API_BASE_URL", "").strip().rstrip("/")
if not api_key:
print("Set VOICE_PRIVATE_API_KEY in .env")
return 1
if not api_base:
print("Set VOICE_API_BASE in .env")
return 1
if not assistant_id:
print("Set VOICE_ASSISTANT_ID in .env")
return 1
if not public_base:
print("Set PUBLIC_API_BASE_URL in .env (run .\\start_dev.ps1 first)")
return 1
secret = ensure_server_secret()
webhook_url = f"{public_base}/webhooks/voice"
try:
ensure_tools(api_key, api_base, assistant_id)
ensure_webhook(api_key, api_base, assistant_id, webhook_url, secret)
except RuntimeError as exc:
print(f"Setup failed: {exc}")
return 1
print("Voice provider auto-setup complete.")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+221
View File
@@ -0,0 +1,221 @@
# Start voice-agent + Cloudflare tunnel for provider webhook URL.
# Usage: .\start_dev.ps1
# .\start_dev.ps1 -Restart # kill old app and reload code
param(
[int]$Port = 8001,
[switch]$Restart
)
$ErrorActionPreference = "Stop"
$Root = Split-Path -Parent $MyInvocation.MyCommand.Path
Set-Location $Root
function Load-DotEnv {
$envFile = Join-Path $Root ".env"
if (-not (Test-Path $envFile)) { return }
Get-Content $envFile | ForEach-Object {
$line = $_.Trim()
if (-not $line -or $line.StartsWith("#") -or $line -notmatch "=") { return }
$key, $value = $line -split "=", 2
if ($key -and -not [string]::IsNullOrWhiteSpace($key) -and -not (Get-Item "Env:$key" -ErrorAction SilentlyContinue)) {
Set-Item -Path "Env:$key" -Value $value.Trim()
}
}
}
function Test-PortListening([int]$p) {
foreach ($path in @("/health/live", "/health", "/api/voice/config")) {
try {
$r = Invoke-WebRequest -Uri "http://127.0.0.1:$p$path" -UseBasicParsing -TimeoutSec 15
if ($r.StatusCode -eq 200) { return $true }
} catch { continue }
}
return $false
}
function Stop-PortProcess([int]$p) {
try {
$conns = Get-NetTCPConnection -LocalPort $p -State Listen -ErrorAction SilentlyContinue
foreach ($conn in $conns) {
if ($conn.OwningProcess) {
Stop-Process -Id $conn.OwningProcess -Force -ErrorAction SilentlyContinue
}
}
if ($conns) {
Start-Sleep -Seconds 2
Write-Host "Stopped previous process on port $p"
}
} catch { }
}
function Clear-LogFile([string]$path) {
if (-not (Test-Path $path)) { return }
try {
Remove-Item $path -Force -ErrorAction Stop
} catch {
try { Clear-Content $path -ErrorAction SilentlyContinue } catch { }
}
}
function Start-VoiceApp([int]$p) {
Write-Host "Starting voice-agent on port $p..."
Stop-PortProcess $p
$appLog = Join-Path $Root ".app.log"
$appErr = Join-Path $Root ".app.err"
Clear-LogFile $appLog
Clear-LogFile $appErr
Start-Process -FilePath "py" -ArgumentList "-3.13", "app.py" -WorkingDirectory $Root -WindowStyle Minimized `
-RedirectStandardOutput $appLog -RedirectStandardError $appErr
$deadline = (Get-Date).AddSeconds(120)
while ((Get-Date) -lt $deadline) {
if (Test-PortListening $p) { break }
Start-Sleep -Seconds 3
}
if (-not (Test-PortListening $p)) {
$errText = ""
if (Test-Path $appErr) { $errText = Get-Content $appErr -Raw -ErrorAction SilentlyContinue }
if ($errText -match "Uvicorn running") {
Write-Host "Server process up — waiting for HTTP endpoints..."
Start-Sleep -Seconds 10
}
}
if (-not (Test-PortListening $p)) {
if (Test-Path $appErr) {
Write-Host "App startup log:" -ForegroundColor Red
Get-Content $appErr -Tail 20 | ForEach-Object { Write-Host $_ }
}
throw "App did not become ready on http://127.0.0.1:$p"
}
Write-Host "App ready: http://127.0.0.1:$p"
}
function Update-PublicUrl([string]$baseUrl) {
$envFile = Join-Path $Root ".env"
$content = Get-Content $envFile -Raw
if ($content -match "PUBLIC_API_BASE_URL=.*") {
$newContent = $content -replace "PUBLIC_API_BASE_URL=.*", "PUBLIC_API_BASE_URL=$baseUrl"
} else {
$newContent = $content.TrimEnd() + "`nPUBLIC_API_BASE_URL=$baseUrl`n"
}
Set-Content -Path $envFile -Value $newContent -NoNewline
$env:PUBLIC_API_BASE_URL = $baseUrl
Write-Host "Updated .env PUBLIC_API_BASE_URL=$baseUrl"
}
function Ensure-ServerSecret {
$envFile = Join-Path $Root ".env"
if (-not (Test-Path $envFile)) { return }
$lines = Get-Content $envFile
$secret = $env:VOICE_SERVER_URL_SECRET
foreach ($line in $lines) {
if ($line -match '^\s*VOICE_SERVER_URL_SECRET=(.+)$') {
$secret = $Matches[1].Trim()
break
}
}
if (-not $secret) {
$secret = [guid]::NewGuid().ToString()
$updated = $false
$newLines = foreach ($line in $lines) {
if ($line -match '^\s*VOICE_SERVER_URL_SECRET=') {
$updated = $true
"VOICE_SERVER_URL_SECRET=$secret"
} else { $line }
}
if (-not $updated) { $newLines += "VOICE_SERVER_URL_SECRET=$secret" }
Set-Content -Path $envFile -Value ($newLines -join "`n")
Write-Host "Generated VOICE_SERVER_URL_SECRET in .env"
}
$env:VOICE_SERVER_URL_SECRET = $secret
}
function Show-ProviderSetupSteps([string]$webhookUrl, [string]$secret) {
Write-Host ""
Write-Host "=== Provider webhook setup (Assistant -> Advanced -> Webhook Server) ===" -ForegroundColor Cyan
Write-Host "Webhook URL:"
Write-Host " $webhookUrl"
Write-Host ""
Write-Host "Secret header:"
Write-Host " Name: X-Voice-Webhook-Secret"
Write-Host " Value: $secret"
Write-Host "================================================================" -ForegroundColor Cyan
}
Load-DotEnv
Ensure-ServerSecret
$env:VOICE_AGENT_PORT = "$Port"
$needsRestart = $Restart.IsPresent
if (Test-PortListening $Port) {
if ($needsRestart) {
Stop-PortProcess $Port
} else {
try {
$cfg = Invoke-RestMethod -Uri "http://127.0.0.1:$Port/api/voice/config" -TimeoutSec 3
if (-not $cfg.webhook_url -and $env:PUBLIC_API_BASE_URL) {
Write-Host "App running but missing webhook URL — restarting to reload .env"
$needsRestart = $true
Stop-PortProcess $Port
} else {
Write-Host "App already running on port $Port"
}
} catch {
Write-Host "App already running on port $Port"
}
}
}
if ($needsRestart -or -not (Test-PortListening $Port)) {
Start-VoiceApp $Port
}
$public = $env:PUBLIC_API_BASE_URL
try {
$null = Invoke-WebRequest -Uri "$public/api/voice/config" -UseBasicParsing -TimeoutSec 5
Write-Host "Tunnel OK: $public"
} catch {
Write-Host "Starting Cloudflare tunnel..."
$log = Join-Path $Root ".tunnel.log"
$logErr = "${log}.err"
if (Test-Path $log) { Remove-Item $log -Force }
if (Test-Path $logErr) { Remove-Item $logErr -Force }
Start-Process -FilePath "cloudflared" -ArgumentList "tunnel", "--url", "http://127.0.0.1:$Port" `
-RedirectStandardOutput $log -RedirectStandardError $logErr -WindowStyle Minimized
$deadline = (Get-Date).AddSeconds(90)
$newUrl = $null
while ((Get-Date) -lt $deadline -and -not $newUrl) {
Start-Sleep -Seconds 2
foreach ($path in @($logErr, $log)) {
if (-not (Test-Path $path)) { continue }
$match = Select-String -Path $path -Pattern "https://[a-z0-9-]+\.trycloudflare\.com" | Select-Object -First 1
if ($match) {
$newUrl = $match.Matches[0].Value
break
}
}
}
if (-not $newUrl) { throw "Could not read tunnel URL from $logErr" }
Update-PublicUrl $newUrl
Write-Host "Tunnel ready: $newUrl"
if ($needsRestart -or -not (Test-PortListening $Port)) {
Stop-PortProcess $Port
Start-VoiceApp $Port
}
}
$config = Invoke-RestMethod -Uri "http://127.0.0.1:$Port/api/voice/config"
Write-Host "Voice configured: $($config.configured)"
Write-Host "Webhook URL: $($config.webhook_url)"
$webhook = if ($config.webhook_url) { $config.webhook_url } else { "$($env:PUBLIC_API_BASE_URL)/webhooks/voice" }
Write-Host ""
Write-Host "Configuring voice provider via API..." -ForegroundColor Cyan
py -3.13 setup_voice_provider.py
if ($LASTEXITCODE -eq 0) {
Write-Host "Voice provider auto-configured (tools + webhook URL + auth)." -ForegroundColor Green
} else {
Show-ProviderSetupSteps -webhookUrl $webhook -secret $env:VOICE_SERVER_URL_SECRET
}
Write-Host "Done. Dashboard: http://127.0.0.1:$Port"
+113
View File
@@ -0,0 +1,113 @@
"""Voice agent HTTP routes."""
from __future__ import annotations
import asyncio
import json
import logging
from integrations.voice.client import VoiceClient
from integrations.voice.config import VoiceConfigError, load_voice_settings
from integrations.voice.processor import process_voice_webhook
from integrations.voice.store import VoiceCallStore
from integrations.voice.verify import VerificationError, verify_webhook_request
from starlette.requests import Request
from starlette.responses import JSONResponse, PlainTextResponse, Response
logger = logging.getLogger(__name__)
# Hold strong references to background tasks so the event loop (which only keeps
# weak references) cannot garbage-collect them mid-flight and drop a webhook event.
_background_tasks: set[asyncio.Task] = set()
async def api_voice_config_status(_request: Request) -> JSONResponse:
try:
settings = load_voice_settings()
configured = True
error = None
except VoiceConfigError as exc:
settings = None
configured = False
error = str(exc)
return JSONResponse(
{
"configured": configured,
"error": error,
"webhook_url": settings.webhook_url if settings else None,
"default_assistant_id": settings.default_assistant_id if settings else None,
}
)
async def api_voice_create_call(request: Request) -> JSONResponse:
try:
body = await request.json()
except json.JSONDecodeError:
return JSONResponse({"error": "invalid JSON body"}, status_code=400)
number = str(body.get("customer_number") or body.get("phone_number") or "").strip()
if not number:
return JSONResponse({"error": "customer_number is required"}, status_code=400)
try:
settings = load_voice_settings()
client = VoiceClient(settings)
result = await asyncio.to_thread(
client.create_call,
customer_number=number,
assistant_id=body.get("assistant_id"),
phone_number_id=body.get("phone_number_id"),
metadata=body.get("metadata") if isinstance(body.get("metadata"), dict) else None,
)
except VoiceConfigError as exc:
return JSONResponse({"error": str(exc)}, status_code=503)
except Exception as exc: # noqa: BLE001
return JSONResponse({"error": str(exc)}, status_code=502)
call_id = result.get("id")
if call_id:
VoiceCallStore.upsert_call(str(call_id), status="scheduled", customer_number=number)
return JSONResponse(result, status_code=201)
async def api_voice_list_calls(_request: Request) -> JSONResponse:
return JSONResponse({"calls": VoiceCallStore.list_calls()})
async def api_voice_call_detail(request: Request) -> JSONResponse:
call_id = request.path_params["call_id"]
record = VoiceCallStore.get_call(call_id)
if not record:
return JSONResponse({"error": "not found"}, status_code=404)
return JSONResponse(record)
async def webhook_voice(request: Request) -> Response:
raw = (await request.body()).decode("utf-8")
try:
settings = load_voice_settings(require_key=False)
except VoiceConfigError as exc:
return JSONResponse({"error": str(exc)}, status_code=503)
headers = {k: v for k, v in request.headers.items()}
try:
verify_webhook_request(secret=settings.server_url_secret, headers=headers)
except VerificationError:
return JSONResponse({"error": "invalid secret"}, status_code=401)
try:
payload = json.loads(raw) if raw else {}
except json.JSONDecodeError:
return JSONResponse({"error": "invalid JSON"}, status_code=400)
msg = payload.get("message") if isinstance(payload.get("message"), dict) else payload
event_type = str((msg or {}).get("type") or payload.get("type") or "unknown")
if event_type in ("tool-calls", "assistant-request", "transfer-destination-request", "knowledge-base-request"):
result = await asyncio.to_thread(process_voice_webhook, event_type, payload, settings=settings)
return JSONResponse(result or {})
task = asyncio.create_task(
asyncio.to_thread(process_voice_webhook, event_type, payload, settings=settings)
)
_background_tasks.add(task)
task.add_done_callback(_background_tasks.discard)
return PlainTextResponse("ok", status_code=200)
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "aiui"
version = "0.3.142"
version = "0.3.143"
description = "YAML-driven website generator - CLI and compiler"
readme = "README.md"
license = {text = "MIT"}
+1 -3
View File
@@ -11,9 +11,7 @@
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<title>Documentation</title>
<script>
// Anti-flicker: apply dark mode class before first paint
// Default to dark to prevent flash; React will correct if needed
document.documentElement.classList.add('dark');
(function(){try{var t=localStorage.getItem('aiui-theme-preference');var d=__AIUI_DARK_DEFAULT__;if(t==='dark'||(t==='system'&&matchMedia('(prefers-color-scheme: dark)').matches)||(!t&&d))document.documentElement.classList.add('dark');else if(t==='light')document.documentElement.classList.remove('dark');}catch(e){}})();
</script>
<link rel="stylesheet" href="/assets/theme.css">
</head>
+903 -2
View File
File diff suppressed because it is too large. Load diff
+1
View File
@@ -89,6 +89,7 @@
"date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0",
"lucide-react": "^0.563.0",
"mermaid": "^11.17.2",
"react-day-picker": "^9.13.0",
"react-markdown": "^10.1.0",
"react-resizable-panels": "^4.5.4",
+206 -82
View File
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'
import { applyTheme } from './themes'
import { useEffect, useMemo, useState } from 'react'
import { applyTheme, applyStoredThemePreference } from './themes'
import './index.css'
import type { UIConfig, DocsNav, RouteManifest, NavItem } from './types'
import { Header } from './Header'
@@ -11,6 +11,8 @@ import { Footer } from './Footer'
import { ChatLayout, AgentUILayout, CopilotWidget, PlaygroundLayout } from './layouts'
import { LocaleProvider } from './i18n'
import { resolveTemplate, shouldShowToc } from './resolver'
import { normalizeDocPath } from './pathUtils'
import { detectActiveTabIndex, filterNavByTab } from './docs/navTabs'
function SkipLink({ enabled }: { enabled?: boolean }) {
if (!enabled) return null
@@ -32,17 +34,30 @@ export default function App() {
const [error, setError] = useState<string | null>(null)
const [selectedItem, setSelectedItem] = useState<NavItem | null>(null)
const [activeItemPath, setActiveItemPath] = useState<string>('')
const [currentPath, setCurrentPath] = useState(() => window.location.pathname.replace(/\/$/, '') || '/')
const [currentPath, setCurrentPath] = useState(() => normalizeDocPath(window.location.pathname))
const [mobileNavOpen, setMobileNavOpen] = useState(false)
const [activeTabIndex, setActiveTabIndex] = useState(0)
const navTabs = config.navigation?.tabs ?? []
const filteredNav = useMemo<DocsNav>(() => {
if (!navTabs.length) return nav
return { items: filterNavByTab(nav.items ?? [], navTabs[activeTabIndex]) }
}, [nav, navTabs, activeTabIndex])
useEffect(() => {
if (navTabs.length) {
setActiveTabIndex(detectActiveTabIndex(navTabs, currentPath))
}
}, [currentPath, navTabs])
// Helper function to find nav item by path (searches top-level items and nested children)
const findNavItemByPath = (navData: DocsNav, path: string): NavItem | null => {
const normalizedPath = path.replace(/\/$/, '') || '/'
const normalizedPath = normalizeDocPath(path)
const findItem = (items: NavItem[]): NavItem | null => {
for (const item of items) {
const itemPath = (item.path ?? '').replace(/\/$/, '')
if (itemPath === normalizedPath || itemPath === normalizedPath.replace(/^\//, '')) {
const itemPath = normalizeDocPath(item.path ?? '')
if (itemPath === normalizedPath) {
return item
}
if (item.children) {
@@ -56,88 +71,187 @@ export default function App() {
return navData.items ? findItem(navData.items) : null
}
const updateSEO = (title: string, path: string, description?: string, configOverride?: UIConfig) => {
const cfg = configOverride ?? config
// Update title using SEO titleTemplate if available
const seoOrigin = (cfg: UIConfig) => (cfg.seo?.siteUrl || window.location.origin).replace(/\/+$/, '')
const formatSeoTitle = (title: string, cfg: UIConfig) => {
const titleTemplate = cfg.seo?.titleTemplate || '%s | %s'
const siteName = cfg.site?.title || 'Documentation'
if (titleTemplate.includes('%s')) {
// Replace first %s with page title, second %s (if exists) with site name
let formattedTitle = titleTemplate.replace('%s', title)
if (formattedTitle.includes('%s')) {
formattedTitle = formattedTitle.replace('%s', siteName)
}
document.title = formattedTitle
} else {
document.title = `${title} | ${siteName}`
return formattedTitle
}
return `${title} | ${siteName}`
}
const updateSEO = (
title: string,
path: string,
description?: string,
configOverride?: UIConfig,
noindex?: boolean,
) => {
const cfg = configOverride ?? config
const canonicalPath = normalizeDocPath(path)
const formattedTitle = formatSeoTitle(title, cfg)
const metaDescription = description || `${title} - ${cfg.site?.description || ''}`
const absoluteUrl = `${seoOrigin(cfg)}${canonicalPath}`
document.title = formattedTitle
// Update canonical URL
let canonical = document.querySelector('link[rel="canonical"]') as HTMLLinkElement
if (!canonical) {
canonical = document.createElement('link')
canonical.rel = 'canonical'
document.head.appendChild(canonical)
}
canonical.href = window.location.origin + path
canonical.href = absoluteUrl
// Update meta description
let metaDesc = document.querySelector('meta[name="description"]') as HTMLMetaElement
if (!metaDesc) {
metaDesc = document.createElement('meta')
metaDesc.name = 'description'
document.head.appendChild(metaDesc)
}
metaDesc.content = description || `${title} - ${cfg.site?.description || ''}`
metaDesc.content = metaDescription
// Update Open Graph tags
let ogTitle = document.querySelector('meta[property="og:title"]') as HTMLMetaElement
if (!ogTitle) {
ogTitle = document.createElement('meta')
ogTitle.setAttribute('property', 'og:title')
document.head.appendChild(ogTitle)
}
ogTitle.content = title
let ogUrl = document.querySelector('meta[property="og:url"]') as HTMLMetaElement
if (!ogUrl) {
ogUrl = document.createElement('meta')
ogUrl.setAttribute('property', 'og:url')
document.head.appendChild(ogUrl)
}
ogUrl.content = window.location.origin + path
// Update Twitter meta tags if configured
if (cfg.seo?.twitter) {
Object.entries(cfg.seo.twitter).forEach(([key, value]) => {
let twitterMeta = document.querySelector(`meta[name="twitter:${key}"]`) as HTMLMetaElement
if (!twitterMeta) {
twitterMeta = document.createElement('meta')
twitterMeta.name = `twitter:${key}`
document.head.appendChild(twitterMeta)
}
twitterMeta.content = value
})
}
// Set default image if available
if (cfg.seo?.defaultImage) {
let ogImage = document.querySelector('meta[property="og:image"]') as HTMLMetaElement
if (!ogImage) {
ogImage = document.createElement('meta')
ogImage.setAttribute('property', 'og:image')
document.head.appendChild(ogImage)
const ensureMeta = (selector: string, create: () => HTMLMetaElement) => {
let el = document.querySelector(selector) as HTMLMetaElement | null
if (!el) {
el = create()
document.head.appendChild(el)
}
ogImage.content = cfg.seo.defaultImage
return el
}
ensureMeta('meta[name="robots"]', () => {
const el = document.createElement('meta')
el.name = 'robots'
return el
}).content = noindex ? 'noindex, nofollow' : 'index, follow'
ensureMeta('meta[property="og:title"]', () => {
const el = document.createElement('meta')
el.setAttribute('property', 'og:title')
return el
}).content = formattedTitle
ensureMeta('meta[property="og:description"]', () => {
const el = document.createElement('meta')
el.setAttribute('property', 'og:description')
return el
}).content = metaDescription
ensureMeta('meta[property="og:url"]', () => {
const el = document.createElement('meta')
el.setAttribute('property', 'og:url')
return el
}).content = absoluteUrl
ensureMeta('meta[property="og:type"]', () => {
const el = document.createElement('meta')
el.setAttribute('property', 'og:type')
return el
}).content = 'article'
ensureMeta('meta[name="twitter:card"]', () => {
const el = document.createElement('meta')
el.name = 'twitter:card'
return el
}).content = 'summary'
ensureMeta('meta[name="twitter:title"]', () => {
const el = document.createElement('meta')
el.name = 'twitter:title'
return el
}).content = formattedTitle
ensureMeta('meta[name="twitter:description"]', () => {
const el = document.createElement('meta')
el.name = 'twitter:description'
return el
}).content = metaDescription
if (cfg.seo?.twitter?.handle) {
ensureMeta('meta[name="twitter:site"]', () => {
const el = document.createElement('meta')
el.name = 'twitter:site'
return el
}).content = cfg.seo.twitter.handle
}
if (cfg.seo?.defaultImage) {
const image = cfg.seo.defaultImage.startsWith('http')
? cfg.seo.defaultImage
: `${seoOrigin(cfg)}${cfg.seo.defaultImage}`
ensureMeta('meta[property="og:image"]', () => {
const el = document.createElement('meta')
el.setAttribute('property', 'og:image')
return el
}).content = image
ensureMeta('meta[name="twitter:image"]', () => {
const el = document.createElement('meta')
el.name = 'twitter:image'
return el
}).content = image
}
let jsonLd = document.querySelector('script#aiui-jsonld') as HTMLScriptElement | null
if (!jsonLd) {
jsonLd = document.createElement('script')
jsonLd.type = 'application/ld+json'
jsonLd.id = 'aiui-jsonld'
document.head.appendChild(jsonLd)
}
jsonLd.textContent = JSON.stringify({
'@context': 'https://schema.org',
'@type': 'TechArticle',
headline: title,
description: metaDescription,
url: absoluteUrl,
isPartOf: {
'@type': 'WebSite',
name: cfg.site?.title || 'Documentation',
url: `${seoOrigin(cfg)}/`,
},
})
}
const selectNavItem = (item: NavItem) => {
const path = normalizeDocPath(item.path || '/')
setSelectedItem(item)
setActiveItemPath(item.path || item.title)
updateSEO(item.title, item.path || '/')
updateSEO(item.title, path, item.description, undefined, item.noindex)
}
// Keep React nav state in sync when plugins perform SPA navigation
useEffect(() => {
const onPluginNavigate = (event: Event) => {
const detail = (event as CustomEvent<{ path?: string }>).detail
const path = normalizeDocPath(detail?.path ?? window.location.pathname)
setCurrentPath(path)
setMobileNavOpen(false)
const found = findNavItemByPath(nav, path)
if (found) {
selectNavItem(found)
return
}
if (path === '/') {
const docsHome = findNavItemByPath(nav, '/docs/index')
if (docsHome) {
selectNavItem(docsHome)
}
}
}
window.addEventListener('aiui:navigate', onPluginNavigate)
return () => window.removeEventListener('aiui:navigate', onPluginNavigate)
}, [nav, config])
useEffect(() => {
async function loadManifests() {
try {
@@ -159,28 +273,33 @@ export default function App() {
// Apply theme from YAML config
const theme = configData.site?.theme
const yamlDark = theme?.darkMode !== false
applyTheme(
theme?.preset || 'zinc',
theme?.darkMode !== false,
yamlDark,
theme?.radius || 'md'
)
applyStoredThemePreference(yamlDark)
// Handle initial URL path for SPA routing
const currentPath = window.location.pathname.replace(/\/$/, '') || '/'
setCurrentPath(currentPath)
if (currentPath !== '/') {
const found = findNavItemByPath(navData, currentPath)
const initialPath = normalizeDocPath(window.location.pathname)
setCurrentPath(initialPath)
if (initialPath !== window.location.pathname) {
window.history.replaceState(null, '', initialPath + window.location.search + window.location.hash)
}
if (initialPath !== '/') {
const found = findNavItemByPath(navData, initialPath)
if (found) {
setSelectedItem(found)
setActiveItemPath(found.path || found.title)
updateSEO(found.title, found.path || currentPath, undefined, configData)
updateSEO(found.title, initialPath, found.description, configData, found.noindex)
}
} else {
const docsHome = findNavItemByPath(navData, '/docs/index')
if (docsHome) {
setSelectedItem(docsHome)
setActiveItemPath(docsHome.path || docsHome.title)
updateSEO(docsHome.title, docsHome.path || '/docs/index', undefined, configData)
updateSEO(docsHome.title, normalizeDocPath(docsHome.path || '/docs/index'), docsHome.description, configData, docsHome.noindex)
}
}
} catch (err) {
@@ -196,7 +315,7 @@ export default function App() {
// Handle browser back/forward navigation in a separate effect
useEffect(() => {
const handlePopState = () => {
const path = window.location.pathname.replace(/\/$/, '') || '/'
const path = normalizeDocPath(window.location.pathname)
setCurrentPath(path)
const found = findNavItemByPath(nav, path)
@@ -219,12 +338,9 @@ export default function App() {
}, [nav, config])
const handleItemClick = (item: NavItem) => {
setSelectedItem(item)
setActiveItemPath(item.path || item.title)
const path = item.path || `/${item.title.toLowerCase().replace(/\s+/g, '-')}`
setCurrentPath(path.replace(/\/$/, '') || '/')
const path = normalizeDocPath(item.path || `/${item.title.toLowerCase().replace(/\s+/g, '-')}`)
window.history.pushState({ path }, item.title, path)
updateSEO(item.title, path)
window.dispatchEvent(new CustomEvent('aiui:navigate', { detail: { path } }))
}
const templateMatch = resolveTemplate(
@@ -237,14 +353,14 @@ export default function App() {
const layout = templateMatch?.layout ?? activeTemplate?.layout ?? 'ThreeColumnLayout'
const zones = activeTemplate?.zones
const showToc = shouldShowToc(templateMatch)
const showGlobalFooter = !(layout === 'FlexibleLayout' && (zones?.footer?.length ?? 0) > 0)
const showGlobalFooter = true
const docsChrome = (
<>
<MobileNavSheet
open={mobileNavOpen}
onOpenChange={setMobileNavOpen}
nav={nav}
nav={filteredNav}
activeItem={activeItemPath}
onItemClick={handleItemClick}
title={config.site?.title || 'Navigation'}
@@ -253,13 +369,16 @@ export default function App() {
config={config}
templateKey={activeTemplateKey}
onMenuClick={() => setMobileNavOpen(true)}
navTabs={navTabs}
activeTabIndex={activeTabIndex}
onTabChange={setActiveTabIndex}
/>
</>
)
if (loading) {
return (
<div className="dark flex items-center justify-center min-h-screen bg-background">
<div className="flex items-center justify-center min-h-screen bg-background">
<span className="text-muted-foreground">Loading...</span>
</div>
)
@@ -267,7 +386,7 @@ export default function App() {
if (error) {
return (
<div className="dark flex flex-col items-center justify-center min-h-screen gap-4 bg-background">
<div className="flex flex-col items-center justify-center min-h-screen gap-4 bg-background">
<h2 className="text-xl font-semibold text-destructive">Failed to load</h2>
<p className="text-muted-foreground">{error}</p>
</div>
@@ -281,7 +400,7 @@ export default function App() {
return (
<div className="flex">
<Sidebar nav={nav} activeItem={activeItemPath} onItemClick={handleItemClick} />
<Content config={config} routes={routes} selectedItem={selectedItem} />
<Content config={config} routes={routes} selectedItem={selectedItem} currentPath={currentPath} />
</div>
)
case 'CenteredLayout':
@@ -289,15 +408,20 @@ export default function App() {
<div className="flex justify-center">
<div className="w-full max-w-4xl px-6">
{zones?.hero && zones.hero.length > 0 && <ZoneWidgets widgets={zones.hero} />}
<Content config={config} routes={routes} selectedItem={selectedItem} />
<Content config={config} routes={routes} selectedItem={selectedItem} currentPath={currentPath} />
</div>
</div>
)
case 'FullWidthLayout':
return (
<div className="px-6">
{zones?.hero && zones.hero.length > 0 && <ZoneWidgets widgets={zones.hero} />}
<Content config={config} routes={routes} selectedItem={selectedItem} />
<div className="flex">
{(nav?.items?.length ?? 0) > 0 && (
<Sidebar nav={nav} activeItem={activeItemPath} onItemClick={handleItemClick} />
)}
<div className="flex-1 px-6">
{zones?.hero && zones.hero.length > 0 && <ZoneWidgets widgets={zones.hero} />}
<Content config={config} routes={routes} selectedItem={selectedItem} currentPath={currentPath} />
</div>
</div>
)
case 'FlexibleLayout': {
@@ -319,7 +443,7 @@ export default function App() {
</aside>
)}
<div className="flex-1">
<Content config={config} routes={routes} selectedItem={selectedItem} />
<Content config={config} routes={routes} selectedItem={selectedItem} currentPath={currentPath} />
</div>
<Toc selectedItem={selectedItem} zones={zones} showToc={showToc} />
</div>
@@ -339,9 +463,9 @@ export default function App() {
case 'ThreeColumnLayout':
default:
return (
<div className="flex">
<Sidebar nav={nav} activeItem={activeItemPath} onItemClick={handleItemClick} />
<Content config={config} routes={routes} selectedItem={selectedItem} />
<div className="docs-shell flex w-full min-h-[calc(100vh-3.5rem)]">
<Sidebar nav={filteredNav} activeItem={activeItemPath} onItemClick={handleItemClick} />
<Content config={config} routes={routes} selectedItem={selectedItem} currentPath={currentPath} />
{showToc && <Toc selectedItem={selectedItem} zones={zones} showToc={showToc} />}
</div>
)
+114 -44
View File
@@ -1,122 +1,191 @@
// Content component — markdown rendering and landing page
import { useEffect, useRef, useState } from 'react'
import { Children, isValidElement, useEffect, useRef, useState } from 'react'
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { Separator } from '@/components/ui/separator'
import { SHADCN_THEMES } from './themes'
import { enhanceMkdocsDom } from './markdown/mkdocsEnhance'
import { preprocessMkdocsMarkdown } from './markdown/mkdocsPreprocess'
import { MermaidDiagram } from './markdown/MermaidDiagram'
import { slugify } from './markdown/slug'
import { MobileToc } from './Toc'
import { docPathToMarkdown, isInternalDocHref, normalizeDocHref, normalizeDocPath } from './pathUtils'
import type { UIConfig, NavItem, RouteManifest } from './types'
interface ContentProps {
config: UIConfig
routes: RouteManifest
selectedItem: NavItem | null
currentPath: string
}
export function Content({ config, routes, selectedItem }: ContentProps) {
export function Content({ config, routes, selectedItem, currentPath }: ContentProps) {
const [markdown, setMarkdown] = useState<string>('')
const [loadedPath, setLoadedPath] = useState('')
const [loadingContent, setLoadingContent] = useState(false)
const articleRef = useRef<HTMLElement>(null)
const theme = config.site?.theme
// Load markdown content when selected item changes
// Load markdown from URL path (source of truth) to avoid stale selectedItem races
useEffect(() => {
if (!selectedItem?.path) {
setMarkdown('')
const urlPath = normalizeDocPath(currentPath)
if (urlPath === '/') {
setLoadingContent(false)
setLoadedPath('')
if (!selectedItem?.path) {
setMarkdown('')
}
return
}
const mdUrl = docPathToMarkdown(urlPath)
const pageTitle = urlPath.split('/').pop()?.replace(/-/g, ' ') ?? 'page'
const controller = new AbortController()
setMarkdown('')
setLoadedPath('')
setLoadingContent(true)
const loadContent = async () => {
setLoadingContent(true)
try {
const docPath = (selectedItem.path ?? '').replace(/^\//, '') + '.md'
const response = await fetch(`/${docPath}`)
const response = await fetch(mdUrl, { signal: controller.signal })
if (controller.signal.aborted) return
if (response.ok) {
const content = await response.text()
setMarkdown(preprocessMkdocsMarkdown(content))
setLoadedPath(urlPath)
window.dispatchEvent(new CustomEvent('aiui:content-loaded', {
detail: { path: urlPath },
}))
} else {
setMarkdown(`*Content for **${selectedItem.title}** not found.*`)
setMarkdown(`*Content for **${pageTitle}** not found.*`)
setLoadedPath(urlPath)
}
} catch {
setMarkdown(`*Failed to load content for **${selectedItem.title}**.*`)
} catch (err) {
if (controller.signal.aborted) return
setMarkdown(`*Failed to load content for **${pageTitle}**.*`)
setLoadedPath(urlPath)
} finally {
setLoadingContent(false)
if (!controller.signal.aborted) {
setLoadingContent(false)
}
}
}
loadContent()
}, [selectedItem])
return () => {
controller.abort()
setLoadingContent(false)
}
}, [currentPath, selectedItem?.path])
useEffect(() => {
if (!markdown || !articleRef.current) return
enhanceMkdocsDom(articleRef.current)
window.dispatchEvent(new CustomEvent('aiui:content-loaded', { detail: { root: articleRef.current } }))
const article = articleRef.current
const frame = window.requestAnimationFrame(() => {
enhanceMkdocsDom(article)
})
return () => window.cancelAnimationFrame(frame)
}, [markdown])
const headingId = (children?: React.ReactNode) => slugify(String(children ?? ''))
const mermaidChartFromPre = (children?: React.ReactNode): string | null => {
try {
const child = Children.only(children)
if (!isValidElement<{ className?: string; children?: React.ReactNode }>(child)) return null
const className = child.props.className ?? ''
if (!className.includes('language-mermaid')) return null
return String(child.props.children ?? '').replace(/\n$/, '').trim()
} catch {
return null
}
}
const markdownComponents = {
h1: ({ children }: { children?: React.ReactNode }) => <h1 id={headingId(children)} className="text-3xl font-bold mt-8 mb-4 scroll-mt-20">{children}</h1>,
h2: ({ children }: { children?: React.ReactNode }) => <h2 id={headingId(children)} className="text-2xl font-semibold mt-8 mb-4 text-primary scroll-mt-20">{children}</h2>,
h3: ({ children }: { children?: React.ReactNode }) => <h3 id={headingId(children)} className="text-xl font-semibold mt-6 mb-3 scroll-mt-20">{children}</h3>,
h4: ({ children }: { children?: React.ReactNode }) => <h4 id={headingId(children)} className="text-lg font-semibold mt-4 mb-2 scroll-mt-20">{children}</h4>,
p: ({ children }: { children?: React.ReactNode }) => <p className="my-3 text-muted-foreground leading-relaxed">{children}</p>,
a: ({ href, children }: { href?: string; children?: React.ReactNode }) => <a href={href} className="text-primary hover:underline">{children}</a>,
p: ({ children }: { children?: React.ReactNode }) => <p className="my-3 text-foreground leading-relaxed">{children}</p>,
a: ({ href, children }: { href?: string; children?: React.ReactNode }) => {
const seoHref = href && isInternalDocHref(href) ? normalizeDocHref(href) : href
return (
<a href={seoHref} className="text-primary hover:underline">
{children}
</a>
)
},
ul: ({ children }: { children?: React.ReactNode }) => <ul className="list-disc pl-6 my-4 space-y-1">{children}</ul>,
ol: ({ children }: { children?: React.ReactNode }) => <ol className="list-decimal pl-6 my-4 space-y-1">{children}</ol>,
li: ({ children }: { children?: React.ReactNode }) => <li className="text-muted-foreground">{children}</li>,
blockquote: ({ children }: { children?: React.ReactNode }) => <blockquote className="border-l-4 border-primary pl-4 my-4 italic text-muted-foreground">{children}</blockquote>,
li: ({ children }: { children?: React.ReactNode }) => <li className="text-foreground">{children}</li>,
blockquote: ({ children }: { children?: React.ReactNode }) => <blockquote className="border-l-4 border-primary pl-4 my-4 italic text-foreground/80">{children}</blockquote>,
code: ({ className, children }: { className?: string; children?: React.ReactNode }) => {
if (className?.includes('language-mermaid')) {
return <MermaidDiagram chart={String(children ?? '').replace(/\n$/, '').trim()} />
}
const isInline = !className && String(children).indexOf('\n') === -1
if (isInline) {
return <code className="bg-primary/10 text-primary px-1.5 py-0.5 rounded text-sm font-mono">{children}</code>
}
return <code className={`block font-mono text-foreground ${className ?? ''}`}>{children}</code>
},
pre: ({ children }: { children?: React.ReactNode }) => (
<pre className="bg-muted text-foreground border border-border p-4 rounded-lg text-sm overflow-auto my-4 font-mono whitespace-pre">{children}</pre>
),
pre: ({ children }: { children?: React.ReactNode }) => {
if (isValidElement(children) && children.type === MermaidDiagram) {
return children
}
const chart = mermaidChartFromPre(children)
if (chart) return <MermaidDiagram chart={chart} />
return (
<pre className="bg-muted text-foreground border border-border p-4 rounded-lg text-sm overflow-auto my-4 font-mono whitespace-pre">{children}</pre>
)
},
table: ({ children }: { children?: React.ReactNode }) => <div className="overflow-auto my-4"><table className="w-full border-collapse text-sm">{children}</table></div>,
thead: ({ children }: { children?: React.ReactNode }) => <thead className="bg-muted/50">{children}</thead>,
tr: ({ children }: { children?: React.ReactNode }) => <tr className="border-b">{children}</tr>,
th: ({ children }: { children?: React.ReactNode }) => <th className="px-4 py-2 text-left font-medium">{children}</th>,
td: ({ children }: { children?: React.ReactNode }) => <td className="px-4 py-2 text-muted-foreground">{children}</td>,
td: ({ children }: { children?: React.ReactNode }) => <td className="px-4 py-2 text-foreground">{children}</td>,
hr: () => <hr className="my-6 border-border" />,
strong: ({ children }: { children?: React.ReactNode }) => <strong className="font-semibold text-foreground">{children}</strong>,
em: ({ children }: { children?: React.ReactNode }) => <em>{children}</em>,
}
const contentReady = loadedPath === normalizeDocPath(currentPath) && markdown.length > 0
if (selectedItem) {
return (
<main id="main-content" className="flex-1 p-8 max-w-3xl">
<MobileToc selectedItem={selectedItem} />
{loadingContent ? (
<div className="text-muted-foreground">Loading content...</div>
) : markdown ? (
<article
ref={articleRef}
className="prose prose-neutral dark:prose-invert max-w-none prose-pre:bg-muted prose-pre:text-foreground prose-code:text-foreground"
>
<Markdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
{markdown}
</Markdown>
</article>
) : (
<div className="bg-muted/50 border rounded-lg p-6">
<p className="text-muted-foreground">
Content for <strong className="text-primary">{selectedItem.title}</strong> would be displayed here.
</p>
</div>
)}
<main id="main-content" className="docs-main flex-1 min-w-0">
<div className="docs-main-inner mx-auto w-full max-w-[720px] px-6 py-8 lg:px-8">
<MobileToc selectedItem={selectedItem} />
{loadingContent && (
<div className="text-muted-foreground animate-pulse">Loading content...</div>
)}
{contentReady && (
<article
key={currentPath}
ref={articleRef}
className="prose prose-neutral dark:prose-invert max-w-none prose-pre:bg-muted prose-pre:text-foreground prose-code:text-foreground"
>
<Markdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
{markdown}
</Markdown>
</article>
)}
{!loadingContent && !contentReady && loadedPath === normalizeDocPath(currentPath) && (
<div className="bg-muted/50 border rounded-lg p-6">
<p className="text-muted-foreground">
Content for <strong className="text-primary">{selectedItem.title}</strong> would be displayed here.
</p>
</div>
)}
</div>
</main>
)
}
return (
<main className="flex-1 p-8 max-w-3xl">
<main id="main-content" className="docs-main flex-1 min-w-0">
<div className="docs-main-inner mx-auto w-full max-w-[720px] px-6 py-8 lg:px-8">
<h1 className="text-4xl font-bold tracking-tight mb-4">
{config.site?.title || 'Documentation'}
</h1>
@@ -166,6 +235,7 @@ export function Content({ config, routes, selectedItem }: ContentProps) {
<pre className="bg-muted text-foreground border border-border p-4 rounded-lg text-sm overflow-auto">
{JSON.stringify(routes.routes?.slice(0, 3), null, 2)}
</pre>
</div>
</main>
)
}
+109 -30
View File
@@ -1,18 +1,27 @@
// Header component
import { Menu } from 'lucide-react'
import type { UIConfig } from './types'
// Header component — Mintlify-style docs chrome
import { Github, Menu, Search } from 'lucide-react'
import type { NavTabConfig, UIConfig } from './types'
import { LocaleSwitcher } from './i18n'
import { Button } from '@/components/ui/button'
import { ThemeToggle } from '@/components/theme-toggle'
interface HeaderProps {
config: UIConfig
templateKey?: string
onMenuClick?: () => void
navTabs?: NavTabConfig[]
activeTabIndex?: number
onTabChange?: (index: number) => void
}
export function Header({
config,
templateKey = 'docs',
onMenuClick,
}: {
config: UIConfig
templateKey?: string
onMenuClick?: () => void
}) {
navTabs,
activeTabIndex = 0,
onTabChange,
}: HeaderProps) {
const headerSlot = config.templates?.[templateKey]?.slots?.header
const headerRef = headerSlot?.ref
const headerComponent = headerRef
@@ -29,16 +38,36 @@ export function Header({
const isDarkMode = document.documentElement.classList.contains('dark')
const logoSrc = header?.logoImage || (logoConfig ? (isDarkMode ? (logoConfig.dark || logoConfig.light) : (logoConfig.light || logoConfig.dark)) : null)
const logoHref = logoConfig?.href || '/'
const logoText = header?.logoText || config.site?.title || 'PraisonAIUI'
const tabs = navTabs ?? config.navigation?.tabs ?? []
const navbar = config.navbar
const navbarLinks = navbar?.links ?? header?.links ?? []
const primary = navbar?.primary ?? (header?.cta ? { type: 'button' as const, label: header.cta.label, href: header.cta.href } : undefined)
const showMintlifyNav = tabs.length > 0
const onLogoClick = (event: React.MouseEvent) => {
event.preventDefault()
window.history.pushState({}, '', logoHref)
window.dispatchEvent(new CustomEvent('aiui:navigate', { detail: { path: logoHref } }))
}
const onSearchClick = () => {
window.dispatchEvent(new CustomEvent('aiui:search-open'))
}
return (
<header className="sticky top-0 z-50 border-b border-border/50 bg-background/80 backdrop-blur-xl supports-[backdrop-filter]:bg-background/60">
<div className="max-w-screen-2xl mx-auto flex h-16 items-center px-6 gap-4">
<header
className="sticky top-0 z-50 border-b border-border/40 bg-background/90 backdrop-blur-xl supports-[backdrop-filter]:bg-background/75"
data-aiui-header-tabs={showMintlifyNav ? 'true' : undefined}
>
<div className="flex h-14 items-center gap-4 px-4 lg:px-6">
{onMenuClick && (
<Button
type="button"
variant="ghost"
size="icon"
className="md:hidden shrink-0"
className="md:hidden shrink-0 -ml-1"
onClick={onMenuClick}
aria-label="Open navigation menu"
>
@@ -46,47 +75,97 @@ export function Header({
</Button>
)}
<a href={logoHref} className="flex items-center gap-3 group cursor-pointer min-w-0" onClick={(e) => {
e.preventDefault()
window.history.pushState({}, '', logoHref)
window.location.reload()
}}>
<a
href={logoHref}
className="flex items-center gap-2.5 group cursor-pointer shrink-0 min-w-0"
onClick={onLogoClick}
>
{logoSrc ? (
<img
src={logoSrc}
alt="Logo"
className="w-8 h-8 rounded-lg shadow-lg shadow-primary/20 group-hover:shadow-primary/40 transition-shadow object-contain"
className="h-7 w-7 rounded-md object-contain"
/>
) : (
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-primary/80 to-primary flex items-center justify-center shadow-lg shadow-primary/20 group-hover:shadow-primary/40 transition-shadow">
<span className="text-primary-foreground text-xs font-bold">AI</span>
<div className="h-7 w-7 rounded-md bg-gradient-to-br from-primary/80 to-primary flex items-center justify-center">
<span className="text-primary-foreground text-[10px] font-bold">AI</span>
</div>
)}
<span className="font-semibold text-lg tracking-tight truncate">{header?.logoText || config.site?.title || 'PraisonAIUI'}</span>
<span className="font-semibold text-[15px] tracking-tight truncate hidden sm:inline">{logoText}</span>
</a>
<nav className="hidden md:flex items-center gap-1 flex-1">
{header?.links?.map((link) => (
{showMintlifyNav && (
<nav className="hidden md:flex items-center gap-1 ml-2" aria-label="Primary docs sections">
{tabs.map((tab, index) => (
<button
key={tab.tab}
type="button"
onClick={() => onTabChange?.(index)}
className={`px-3 py-1.5 text-sm rounded-md transition-colors ${index === activeTabIndex
? 'text-foreground font-medium'
: 'text-muted-foreground hover:text-foreground'
}`}
>
{tab.tab}
</button>
))}
</nav>
)}
{!showMintlifyNav && (
<nav className="hidden md:flex items-center gap-1 flex-1 ml-2">
{navbarLinks.map((link) => (
<a
key={link.href}
href={link.href}
className="px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors rounded-md hover:bg-muted/50"
>
{link.label}
</a>
))}
</nav>
)}
<div className="flex-1 min-w-0" />
{config.search?.enabled !== false && showMintlifyNav && (
<button
type="button"
onClick={onSearchClick}
className="hidden sm:flex items-center gap-2 h-9 w-full max-w-[220px] px-3 text-sm text-muted-foreground border border-border/60 rounded-lg bg-muted/30 hover:bg-muted/50 transition-colors"
aria-label="Search documentation"
>
<Search className="h-4 w-4 shrink-0" />
<span className="flex-1 text-left truncate">Search...</span>
<kbd className="hidden lg:inline text-[10px] font-medium text-muted-foreground/80 border border-border/60 rounded px-1.5 py-0.5">⌘K</kbd>
</button>
)}
<div className="flex items-center gap-1.5 shrink-0">
{showMintlifyNav && navbarLinks.map((link) => (
<a
key={link.href}
href={link.href}
className="px-3 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors rounded-md hover:bg-muted/50"
className="hidden lg:inline-flex px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
{link.label}
</a>
))}
</nav>
<div className="flex items-center gap-2 ml-auto">
<LocaleSwitcher />
{header?.cta && (
{primary && (
<a
href={header.cta.href}
className="inline-flex items-center justify-center rounded-md text-sm font-medium bg-primary text-primary-foreground shadow h-9 px-4 hover:bg-primary/90 transition-colors"
href={primary.href}
target={primary.href.startsWith('http') ? '_blank' : undefined}
rel={primary.href.startsWith('http') ? 'noopener noreferrer' : undefined}
className="inline-flex items-center justify-center gap-1.5 rounded-lg text-sm font-medium bg-primary text-primary-foreground h-9 px-3.5 hover:bg-primary/90 transition-colors"
>
{header.cta.label}
{primary.type === 'github' && <Github className="h-4 w-4" />}
{primary.label ?? (primary.type === 'github' ? 'GitHub' : 'Get started')}
</a>
)}
<ThemeToggle />
<LocaleSwitcher />
</div>
</div>
</header>
+30 -19
View File
@@ -1,12 +1,13 @@
// Sidebar component — navigation tree
// Sidebar component — Mintlify-style navigation tree
import { ChevronRight } from 'lucide-react'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Separator } from '@/components/ui/separator'
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet'
import { normalizeDocPath } from './pathUtils'
import type { DocsNav, NavItem } from './types'
interface NavTreeProps {
@@ -22,35 +23,44 @@ export function NavTree({ nav, activeItem, onItemClick }: NavTreeProps) {
if (hasChildren) {
return (
<div key={item.title + (item.path || '')}>
<div className="flex items-center gap-2 px-3 py-1.5 text-xs font-semibold text-muted-foreground/70 uppercase tracking-widest mt-4 first:mt-0">
<div key={item.title + (item.path || '')} className="mb-4">
<div className="flex items-center gap-1 px-3 py-1.5 text-[11px] font-semibold text-muted-foreground/80 uppercase tracking-wider">
{item.title}
</div>
<div className="space-y-0.5">
<div className="space-y-0.5 mt-1">
{item.children!.map((child) => renderItem(child, depth + 1))}
</div>
</div>
)
}
const href = normalizeDocPath(item.path || '/')
return (
<button
<a
key={item.title + (item.path || '')}
type="button"
onClick={() => onItemClick(item)}
className={`w-full text-left px-3 py-1.5 text-sm rounded-md transition-all duration-150 ${isActive
? 'bg-primary/10 text-primary font-medium border-l-2 border-primary'
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
href={href}
data-nav-path={item.path || ''}
onClick={(event) => {
event.preventDefault()
onItemClick(item)
}}
className={`group flex items-center gap-1.5 w-full text-left px-3 py-1.5 text-[13px] rounded-md transition-colors ${isActive
? 'text-primary font-medium bg-primary/10'
: 'text-muted-foreground hover:text-foreground hover:bg-muted/40'
}`}
style={{ paddingLeft: `${12 + depth * 12}px` }}
style={{ paddingLeft: `${12 + depth * 10}px` }}
>
{item.title}
</button>
{depth > 0 && (
<ChevronRight className={`h-3 w-3 shrink-0 opacity-0 group-hover:opacity-40 ${isActive ? 'opacity-60 text-primary' : ''}`} />
)}
<span className="truncate">{item.title}</span>
</a>
)
}
return (
<nav className="space-y-0.5">
<nav className="space-y-0.5 pb-6">
{nav.items?.map((group) => renderItem(group))}
</nav>
)
@@ -62,10 +72,11 @@ interface SidebarProps extends NavTreeProps {
export function Sidebar({ nav, activeItem, onItemClick }: SidebarProps) {
return (
<aside className="w-64 min-w-[16rem] border-r border-border/50 hidden md:block">
<ScrollArea className="h-[calc(100vh-4rem)] py-4 px-2">
<NavTree nav={nav} activeItem={activeItem} onItemClick={onItemClick} />
<Separator className="my-4" />
<aside className="docs-sidebar w-[17rem] min-w-[17rem] shrink-0 hidden md:block border-r border-border/40">
<ScrollArea className="h-[calc(100vh-3.5rem)] sticky top-14">
<div className="py-5 px-2">
<NavTree nav={nav} activeItem={activeItem} onItemClick={onItemClick} />
</div>
</ScrollArea>
</aside>
)
+60 -28
View File
@@ -22,6 +22,7 @@ function collectHeadings(): TocHeading[] {
function usePageHeadings(selectedItem: NavItem | null) {
const [headings, setHeadings] = useState<TocHeading[]>([])
const [activeId, setActiveId] = useState('')
const refreshHeadings = useCallback(() => {
setHeadings(collectHeadings())
@@ -30,48 +31,79 @@ function usePageHeadings(selectedItem: NavItem | null) {
useEffect(() => {
refreshHeadings()
const timer = window.setTimeout(refreshHeadings, 100)
return () => window.clearTimeout(timer)
const timer2 = window.setTimeout(refreshHeadings, 500)
return () => {
window.clearTimeout(timer)
window.clearTimeout(timer2)
}
}, [selectedItem, refreshHeadings])
useEffect(() => {
const onContentLoaded = () => refreshHeadings()
window.addEventListener('aiui:content-loaded', onContentLoaded)
return () => window.removeEventListener('aiui:content-loaded', onContentLoaded)
}, [refreshHeadings])
if (!headings.length) return
return headings
const observer = new IntersectionObserver(
(entries) => {
const visible = entries
.filter((entry) => entry.isIntersecting)
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top)
if (visible[0]?.target.id) {
setActiveId(visible[0].target.id)
}
},
{ rootMargin: '-20% 0px -70% 0px', threshold: 0 },
)
for (const heading of headings) {
const el = document.getElementById(heading.id)
if (el) observer.observe(el)
}
return () => observer.disconnect()
}, [headings])
return { headings, activeId }
}
function TocLinks({ headings }: { headings: TocHeading[] }) {
function TocLinks({ headings, activeId }: { headings: TocHeading[]; activeId: string }) {
if (headings.length === 0) {
return <p className="text-sm text-muted-foreground">No sections on this page.</p>
}
return (
<div className="space-y-2">
{headings.map((heading) => (
<a
key={heading.id}
href={`#${heading.id}`}
className={`flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors ${heading.level === 3 ? 'pl-3 text-sm' : heading.level === 4 ? 'pl-6 text-sm' : 'font-medium'}`}
>
<span className="w-1 h-1 rounded-full bg-muted-foreground/30 shrink-0" />
{heading.text}
</a>
))}
<div className="space-y-2.5">
{headings.map((heading) => {
const isActive = heading.id === activeId
return (
<a
key={heading.id}
href={`#${heading.id}`}
className={`flex items-start gap-2 text-[13px] leading-snug transition-colors ${heading.level === 3 ? 'pl-3' : heading.level === 4 ? 'pl-5' : ''
} ${isActive
? 'text-primary font-medium'
: 'text-muted-foreground hover:text-foreground'
}`}
>
<span
className={`mt-[0.45rem] h-1.5 w-1.5 shrink-0 rounded-full ${isActive ? 'bg-primary' : 'bg-muted-foreground/30'
}`}
/>
<span>{heading.text}</span>
</a>
)
})}
</div>
)
}
export function MobileToc({ selectedItem }: { selectedItem: NavItem | null }) {
const headings = usePageHeadings(selectedItem)
const { headings, activeId } = usePageHeadings(selectedItem)
if (!selectedItem || headings.length === 0) return null
return (
<details className="lg:hidden border border-border rounded-lg mb-6 px-4 py-3 bg-muted/20">
<details className="lg:hidden border border-border/60 rounded-lg mb-6 px-4 py-3 bg-muted/20">
<summary className="text-sm font-medium cursor-pointer">On this page</summary>
<nav className="mt-3">
<TocLinks headings={headings} />
<TocLinks headings={headings} activeId={activeId} />
</nav>
</details>
)
@@ -86,7 +118,7 @@ export function Toc({
zones?: ZonesConfig
showToc?: boolean
}) {
const headings = usePageHeadings(selectedItem)
const { headings, activeId } = usePageHeadings(selectedItem)
const rightSidebarWidgets = zones?.rightSidebar || []
if (!showToc && rightSidebarWidgets.filter((w) => w.type !== 'Toc').length === 0) {
@@ -94,21 +126,21 @@ export function Toc({
}
return (
<aside className="w-64 hidden lg:block border-l border-border/50">
<div className="sticky top-20 px-4 py-6 space-y-4">
<aside className="docs-toc w-[16rem] min-w-[16rem] shrink-0 hidden lg:block">
<div className="sticky top-14 h-[calc(100vh-3.5rem)] overflow-y-auto px-4 py-6">
{showToc && (
<div>
<h4 className="text-xs font-semibold text-muted-foreground/70 uppercase tracking-widest mb-4">
<h4 className="text-[11px] font-semibold text-muted-foreground/80 uppercase tracking-wider mb-4">
On this page
</h4>
<nav className="text-sm">
<TocLinks headings={headings} />
<nav>
<TocLinks headings={headings} activeId={activeId} />
</nav>
</div>
)}
{rightSidebarWidgets.filter((w) => w.type !== 'Toc').length > 0 && (
<div className="pt-4 border-t border-border/50">
<div className="pt-4 mt-4 border-t border-border/40">
<ZoneWidgets widgets={rightSidebarWidgets.filter((w) => w.type !== 'Toc')} />
</div>
)}
+5 -11
View File
@@ -6,22 +6,16 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
type Theme = "light" | "dark" | "system"
import { setThemePreference, type ThemePreference } from "../themes"
interface ThemeToggleProps {
theme?: Theme
onThemeChange?: (theme: Theme) => void
theme?: ThemePreference
onThemeChange?: (theme: ThemePreference) => void
}
export function ThemeToggle({ onThemeChange }: ThemeToggleProps) {
const setTheme = (newTheme: Theme) => {
if (newTheme === "system") {
const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches
document.documentElement.classList.toggle("dark", systemDark)
} else {
document.documentElement.classList.toggle("dark", newTheme === "dark")
}
const setTheme = (newTheme: ThemePreference) => {
setThemePreference(newTheme)
onThemeChange?.(newTheme)
}
+89
View File
@@ -0,0 +1,89 @@
import type { NavItem, NavTabConfig } from '../types'
function sectionTitle(item: NavItem): string {
return item.title.trim().toLowerCase()
}
function collectPaths(item: NavItem): string[] {
const paths: string[] = []
if (item.path) paths.push(item.path.toLowerCase())
for (const child of item.children ?? []) {
paths.push(...collectPaths(child))
}
return paths
}
function pathMatchesPage(docPath: string, page: string): boolean {
const normalizedPage = page.replace(/^\/+/, '').toLowerCase()
const normalizedPath = docPath.replace(/\/+$/, '').toLowerCase()
if (normalizedPage === 'index') {
return normalizedPath.endsWith('/index') || normalizedPath === '/docs'
}
const suffix = `/${normalizedPage}`
return normalizedPath.endsWith(suffix) || normalizedPath.endsWith(`${suffix}/index`)
}
/** Whether a sidebar group belongs to the given navigation tab (from topnav plugin logic). */
export function navItemBelongsToTab(item: NavItem, tab: NavTabConfig): boolean {
if (!tab.groups?.length) return false
const header = sectionTitle(item)
const paths = collectPaths(item)
for (const grp of tab.groups) {
const groupName = grp.group.toLowerCase()
if (header === groupName) return true
if (grp.prefix) {
const prefix = grp.prefix.toLowerCase()
if (header === prefix || header === prefix.replace(/-/g, ' ')) return true
const prefixPath = `/docs/${prefix}`
if (paths.some((path) => path === prefixPath || path.startsWith(`${prefixPath}/`))) {
return true
}
}
if (grp.pages?.some((page) => paths.some((path) => pathMatchesPage(path, page)))) {
return true
}
if (!item.children?.length && grp.pages?.some((page) => {
const slug = page.split('/').pop()?.replace(/-/g, ' ') ?? ''
return header === slug || header === page.toLowerCase()
})) {
return true
}
}
return false
}
/** Pick the active tab index from the current URL path. */
export function detectActiveTabIndex(tabs: NavTabConfig[], path: string): number {
const normalized = path.toLowerCase()
let bestMatch = -1
let bestMatchLen = 0
for (let i = 0; i < tabs.length; i++) {
const tab = tabs[i]
for (const grp of tab.groups ?? []) {
if (!grp.prefix) continue
const needle = `/${grp.prefix.toLowerCase()}/`
if (normalized.includes(needle) && needle.length > bestMatchLen) {
bestMatch = i
bestMatchLen = needle.length
}
}
}
if (bestMatch >= 0) return bestMatch
return 0
}
export function filterNavByTab(items: NavItem[], tab: NavTabConfig | undefined): NavItem[] {
if (!tab) return items
return items.filter((item) => navItemBelongsToTab(item, tab))
}
+152 -37
View File
@@ -12,37 +12,37 @@
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-destructive: hsl(var(--destructive));
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
--color-chart-1: hsl(var(--chart-1));
--color-chart-2: hsl(var(--chart-2));
--color-chart-3: hsl(var(--chart-3));
--color-chart-4: hsl(var(--chart-4));
--color-chart-5: hsl(var(--chart-5));
--color-sidebar: hsl(var(--sidebar));
--color-sidebar-foreground: hsl(var(--sidebar-foreground));
--color-sidebar-primary: hsl(var(--sidebar-primary));
--color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
--color-sidebar-accent: hsl(var(--sidebar-accent));
--color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
--color-sidebar-border: hsl(var(--sidebar-border));
--color-sidebar-ring: hsl(var(--sidebar-ring));
}
/* Color variables are injected via theme.css at build time.
@@ -52,20 +52,37 @@
* {
@apply border-border outline-ring/50;
}
html,
body {
@apply bg-background text-foreground;
background-color: hsl(var(--background));
color: hsl(var(--foreground));
}
}
@layer components {
/* Readable code blocks in light and dark prose */
.prose pre {
@apply bg-muted text-foreground border border-border;
/* Readable prose in light and dark mode */
.prose-neutral {
--tw-prose-body: hsl(var(--foreground));
--tw-prose-headings: hsl(var(--foreground));
--tw-prose-bold: hsl(var(--foreground));
--tw-prose-counters: hsl(var(--muted-foreground));
--tw-prose-bullets: hsl(var(--muted-foreground));
--tw-prose-pre-code: hsl(var(--foreground));
--tw-prose-pre-bg: hsl(var(--muted));
--tw-prose-code: hsl(var(--foreground));
}
.prose pre code {
.prose pre,
.prose-neutral :where(pre) {
@apply bg-muted text-foreground border border-border;
color: hsl(var(--foreground)) !important;
background-color: hsl(var(--muted)) !important;
}
.prose pre code,
.prose-neutral :where(pre code) {
@apply text-foreground bg-transparent;
color: inherit;
color: inherit !important;
}
.prose :not(pre) > code {
@@ -79,4 +96,102 @@
.prose .aiui-tabs pre {
@apply my-2;
}
.mermaid-diagram {
display: block;
width: 100%;
margin: 1.5rem 0;
padding: 1rem 1.25rem;
border-radius: 12px;
overflow-x: auto;
transition: border-color 0.3s ease, box-shadow 0.3s ease;
}
.mermaid-diagram-svg {
width: 100%;
min-width: 100%;
display: block;
}
:root:not(.dark) .mermaid-diagram:not(.mermaid-error) {
background: hsl(var(--muted));
border: 1px solid hsl(var(--border));
}
.dark .mermaid-diagram:not(.mermaid-error) {
background: rgba(15, 23, 42, 0.5);
border: 1px solid rgba(148, 163, 184, 0.15);
}
.mermaid-diagram:not(.mermaid-error):hover {
border-color: rgba(20, 184, 166, 0.4);
box-shadow: 0 0 24px rgba(13, 148, 136, 0.12);
}
.mermaid-diagram svg {
width: 100% !important;
max-width: 100%;
height: auto !important;
display: block;
}
.mermaid-diagram .nodeLabel,
.mermaid-diagram .label div,
.mermaid-diagram .cluster-label .nodeLabel {
color: #ffffff !important;
fill: #ffffff !important;
font-family: "Inter", system-ui, -apple-system, sans-serif !important;
font-weight: 500 !important;
}
.mermaid-diagram .edgeLabel {
color: #e2e8f0 !important;
fill: #e2e8f0 !important;
background-color: rgba(15, 23, 42, 0.85) !important;
padding: 2px 6px;
border-radius: 4px;
}
.mermaid-diagram .cluster-label .nodeLabel {
font-weight: 600 !important;
font-size: 0.95em;
}
.mermaid-diagram .node rect,
.mermaid-diagram .node circle,
.mermaid-diagram .node polygon {
rx: 8;
ry: 8;
}
/* Mintlify-style docs shell */
.docs-shell {
width: 100%;
}
.docs-main-inner {
min-height: calc(100vh - 3.5rem);
}
}
/* Prose readability — must beat @tailwindcss/typography defaults (unlayered) */
article.prose :where(p, li, td, th, blockquote, strong, em, h1, h3, h4, h5, h6) {
color: hsl(var(--foreground)) !important;
-webkit-text-fill-color: unset !important;
opacity: 1 !important;
}
article.prose :where(h2) {
color: hsl(var(--primary)) !important;
-webkit-text-fill-color: unset !important;
opacity: 1 !important;
}
article.prose .aiui-feature-grid > li {
color: hsl(var(--foreground)) !important;
}
.dark article.prose.dark\:prose-invert :where(p, li, td, th, blockquote, strong, h1, h2, h3, h4) {
color: hsl(var(--foreground)) !important;
-webkit-text-fill-color: unset !important;
}
@@ -0,0 +1,98 @@
import { useEffect, useId, useRef, useState } from 'react'
import { fitMermaidSvg, getMermaidConfig, isDarkMode } from './mermaidThemes'
let renderCounter = 0
function nextRenderId(baseId: string): string {
renderCounter += 1
return `mermaid-${baseId.replace(/:/g, '')}-${renderCounter}`
}
export function MermaidDiagram({ chart }: { chart: string }) {
const baseId = useId()
const containerRef = useRef<HTMLDivElement>(null)
const svgRef = useRef<HTMLDivElement>(null)
const bindFunctionsRef = useRef<((element: Element) => void) | undefined>(undefined)
const [status, setStatus] = useState<'loading' | 'ready' | 'error'>('loading')
const [errorMessage, setErrorMessage] = useState('')
const [svgMarkup, setSvgMarkup] = useState('')
useEffect(() => {
const container = containerRef.current
if (!container) return
let cancelled = false
const render = async () => {
setStatus('loading')
setErrorMessage('')
setSvgMarkup('')
const mermaid = (await import('mermaid')).default
mermaid.initialize(getMermaidConfig(isDarkMode()))
const styles = getComputedStyle(container)
const paddingX = parseFloat(styles.paddingLeft) + parseFloat(styles.paddingRight)
const containerWidth = container.clientWidth - paddingX
try {
const { svg, bindFunctions } = await mermaid.render(nextRenderId(baseId), chart)
if (cancelled) return
bindFunctionsRef.current = bindFunctions
setSvgMarkup(fitMermaidSvg(svg, containerWidth))
setStatus('ready')
} catch (err) {
if (cancelled) return
setStatus('error')
setErrorMessage(err instanceof Error ? err.message : 'Failed to render diagram')
}
}
void render()
const observer = new ResizeObserver(() => {
void render()
})
observer.observe(container)
const themeObserver = new MutationObserver(() => {
void render()
})
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class'],
})
return () => {
cancelled = true
observer.disconnect()
themeObserver.disconnect()
}
}, [chart, baseId])
useEffect(() => {
if (status !== 'ready' || !svgRef.current) return
bindFunctionsRef.current?.(svgRef.current)
}, [status, svgMarkup])
if (status === 'error') {
return (
<pre className="mermaid-diagram mermaid-error text-destructive text-sm my-4 p-4 rounded-lg border border-destructive/30 bg-destructive/5 overflow-x-auto">
{errorMessage}
</pre>
)
}
return (
<div
ref={containerRef}
className={`mermaid-diagram w-full${status === 'loading' ? ' mermaid-loading text-muted-foreground text-sm' : ''}`}
aria-busy={status === 'loading'}
aria-label={status === 'loading' ? 'Loading diagram' : 'Mermaid diagram'}
>
{status === 'loading' ? 'Loading diagram…' : null}
{svgMarkup ? (
<div ref={svgRef} className="mermaid-diagram-svg w-full" dangerouslySetInnerHTML={{ __html: svgMarkup }} />
) : null}
</div>
)
}
+118
View File
@@ -0,0 +1,118 @@
/** Mermaid theme config — aligned with plugins/mermaid.js */
export const MERMAID_DARK_THEME = {
theme: 'base' as const,
themeVariables: {
background: 'transparent',
mainBkg: '#0d9488',
primaryColor: '#0d9488',
primaryBorderColor: '#14b8a6',
primaryTextColor: '#ffffff',
secondaryColor: '#6366f1',
secondaryBorderColor: '#818cf8',
secondaryTextColor: '#ffffff',
tertiaryColor: '#e11d48',
tertiaryBorderColor: '#fb7185',
tertiaryTextColor: '#ffffff',
textColor: '#f1f5f9',
labelTextColor: '#f1f5f9',
lineColor: '#94a3b8',
arrowheadColor: '#cbd5e1',
nodeBorder: '#14b8a6',
clusterBkg: 'rgba(13, 148, 136, 0.08)',
clusterBorder: 'rgba(20, 184, 166, 0.4)',
defaultLinkColor: '#94a3b8',
edgeLabelBackground: 'rgba(15, 23, 42, 0.85)',
nodeTextColor: '#ffffff',
actorBkg: '#0d9488',
actorBorder: '#14b8a6',
actorTextColor: '#ffffff',
actorLineColor: '#64748b',
signalColor: '#cbd5e1',
signalTextColor: '#f1f5f9',
activationBorderColor: '#14b8a6',
activationBkgColor: 'rgba(13, 148, 136, 0.2)',
sequenceNumberColor: '#ffffff',
noteBkgColor: 'rgba(99, 102, 241, 0.2)',
noteBorderColor: '#818cf8',
noteTextColor: '#f1f5f9',
titleColor: '#f8fafc',
fontFamily: '"Inter", system-ui, -apple-system, sans-serif',
fontSize: '16px',
},
}
export const MERMAID_LIGHT_THEME = {
theme: 'base' as const,
themeVariables: {
background: 'transparent',
mainBkg: '#0d9488',
primaryColor: '#0d9488',
primaryBorderColor: '#0f766e',
primaryTextColor: '#ffffff',
secondaryColor: '#6366f1',
secondaryBorderColor: '#4f46e5',
secondaryTextColor: '#ffffff',
tertiaryColor: '#e11d48',
tertiaryBorderColor: '#be123c',
tertiaryTextColor: '#ffffff',
textColor: '#1e293b',
labelTextColor: '#334155',
lineColor: '#94a3b8',
arrowheadColor: '#64748b',
nodeBorder: '#0f766e',
clusterBkg: 'rgba(13, 148, 136, 0.06)',
clusterBorder: 'rgba(15, 118, 110, 0.3)',
defaultLinkColor: '#94a3b8',
edgeLabelBackground: 'rgba(255, 255, 255, 0.9)',
nodeTextColor: '#ffffff',
actorBkg: '#0d9488',
actorBorder: '#0f766e',
actorTextColor: '#ffffff',
actorLineColor: '#cbd5e1',
signalColor: '#64748b',
signalTextColor: '#1e293b',
noteBkgColor: 'rgba(99, 102, 241, 0.1)',
noteBorderColor: '#6366f1',
noteTextColor: '#1e293b',
titleColor: '#0f172a',
fontFamily: '"Inter", system-ui, -apple-system, sans-serif',
fontSize: '16px',
},
}
export function isDarkMode(): boolean {
return document.documentElement.classList.contains('dark')
}
export function getMermaidConfig(dark: boolean) {
const theme = dark ? MERMAID_DARK_THEME : MERMAID_LIGHT_THEME
return {
startOnLoad: false,
securityLevel: 'strict' as const,
...theme,
flowchart: {
useMaxWidth: true,
htmlLabels: true,
curve: 'basis' as const,
},
sequence: {
useMaxWidth: true,
wrap: true,
},
er: { useMaxWidth: true },
gantt: { useMaxWidth: true },
}
}
/** Ensure rendered SVG stretches to the diagram container (Mintlify-style). */
export function fitMermaidSvg(svg: string, containerWidth: number): string {
const width = Math.max(Math.floor(containerWidth), 320)
return svg.replace(/<svg([^>]*)>/, (_match, attrs: string) => {
const cleaned = attrs
.replace(/\swidth="[^"]*"/g, '')
.replace(/\sheight="[^"]*"/g, '')
.replace(/\sstyle="[^"]*"/g, '')
return `<svg${cleaned} width="${width}" style="width:100%;max-width:100%;height:auto;display:block;">`
})
}
+9 -103
View File
@@ -1,25 +1,23 @@
import { isMkdocsTabHeading, mkdocsTabTitle } from './mkdocsPreprocess'
/** Post-render DOM fixes for MkDocs-style content (tabs, feature cards). */
/** Post-render class-only fixes for MkDocs-style content. No DOM moves — React owns the tree. */
export function enhanceMkdocsDom(root: HTMLElement): void {
const articles = new Set<HTMLElement>()
if (root.matches('article.prose, main .prose, .prose, main.flex-1, #main-content')) {
if (root.matches('article.prose, #main-content article')) {
articles.add(root)
}
root.querySelectorAll<HTMLElement>('article.prose, main .prose, .prose').forEach((el) => articles.add(el))
root.querySelectorAll<HTMLElement>('article.prose, #main-content article').forEach((el) => articles.add(el))
for (const article of articles) {
removeRawDivParagraphs(article)
hideRawDivParagraphs(article)
enhanceFeatureGrid(article)
enhanceTabs(article)
}
}
function removeRawDivParagraphs(article: HTMLElement): void {
/** Hide MkDocs artefact paragraphs — never remove nodes React may still own. */
function hideRawDivParagraphs(article: HTMLElement): void {
for (const p of article.querySelectorAll('p')) {
const text = p.textContent?.trim() ?? ''
if (/^<\/?div[\s>]/.test(text) || text === '</div>') {
p.remove()
p.classList.add('hidden')
}
}
}
@@ -53,97 +51,5 @@ function enhanceFeatureGrid(article: HTMLElement): void {
}
}
interface TabSection {
heading: HTMLHeadingElement
title: string
nodes: Node[]
}
function enhanceTabs(article: HTMLElement): void {
if (article.querySelector('[data-aiui-tabs]')) return
const tabHeadings = Array.from(article.querySelectorAll<HTMLHeadingElement>('h3')).filter((h3) =>
isMkdocsTabHeading(h3.textContent?.trim() ?? ''),
)
if (tabHeadings.length < 2) return
const sections: TabSection[] = tabHeadings.map((heading) => {
const title = mkdocsTabTitle(heading.textContent?.trim() ?? '')
const nodes: Node[] = []
let sibling = heading.nextSibling
while (sibling) {
if (sibling.nodeType === Node.ELEMENT_NODE) {
const el = sibling as HTMLElement
if (el.tagName === 'H3' && isMkdocsTabHeading(el.textContent?.trim() ?? '')) break
if (['H1', 'H2'].includes(el.tagName)) break
}
const next = sibling.nextSibling
nodes.push(sibling)
sibling = next
}
return { heading, title, nodes }
})
const wrapper = document.createElement('div')
wrapper.className = 'aiui-tabs my-6 border border-border rounded-lg overflow-hidden'
wrapper.dataset.aiuiTabs = 'true'
const buttons = document.createElement('div')
buttons.className = 'aiui-tab-buttons flex flex-wrap gap-0 border-b border-border bg-muted/30'
wrapper.appendChild(buttons)
const panels = document.createElement('div')
panels.className = 'aiui-tab-panels'
wrapper.appendChild(panels)
sections.forEach((section, index) => {
const btn = document.createElement('button')
btn.type = 'button'
btn.className = [
'aiui-tab-button px-4 py-2 text-sm font-medium transition-colors',
index === 0
? 'bg-background text-foreground border-b-2 border-primary'
: 'text-muted-foreground hover:text-foreground',
].join(' ')
btn.textContent = section.title
btn.dataset.tabIndex = String(index)
buttons.appendChild(btn)
const panel = document.createElement('div')
panel.className = index === 0 ? 'aiui-tab-panel p-4 block' : 'aiui-tab-panel p-4 hidden'
panel.dataset.tabPanel = String(index)
for (const node of section.nodes) {
panel.appendChild(node)
}
panels.appendChild(panel)
})
const insertPoint = sections[0].heading
article.insertBefore(wrapper, insertPoint)
for (const section of sections) {
section.heading.remove()
}
buttons.addEventListener('click', (event) => {
const target = (event.target as HTMLElement).closest<HTMLButtonElement>('.aiui-tab-button')
if (!target) return
const index = target.dataset.tabIndex ?? '0'
for (const btn of buttons.querySelectorAll<HTMLButtonElement>('.aiui-tab-button')) {
const active = btn.dataset.tabIndex === index
btn.classList.toggle('bg-background', active)
btn.classList.toggle('text-foreground', active)
btn.classList.toggle('border-b-2', active)
btn.classList.toggle('border-primary', active)
btn.classList.toggle('text-muted-foreground', !active)
}
for (const panel of panels.querySelectorAll<HTMLElement>('.aiui-tab-panel')) {
panel.classList.toggle('hidden', panel.dataset.tabPanel !== index)
panel.classList.toggle('block', panel.dataset.tabPanel === index)
}
})
}
/** No-op — React article must not receive foreign plugin nodes. */
export function teardownMkdocsEnhancements(_root: HTMLElement): void {}
+37
View File
@@ -0,0 +1,37 @@
/** Canonical docs path: no trailing slash except root. */
export function normalizeDocPath(path: string): string {
const cleaned = (path || '/').replace(/\/+$/, '')
return cleaned || '/'
}
/** Normalise an internal docs href for SEO-friendly crawlable links. */
export function normalizeDocHref(href: string): string {
if (!href || href.startsWith('#')) return href
try {
const url = new URL(href, window.location.origin)
if (url.origin !== window.location.origin) return href
return normalizeDocPath(url.pathname) + url.search + url.hash
} catch {
return href
}
}
export function isInternalDocHref(href?: string): boolean {
if (!href || href.startsWith('#')) return false
if (href.startsWith('/docs')) return true
try {
const url = new URL(href, window.location.origin)
return url.origin === window.location.origin && url.pathname.startsWith('/docs')
} catch {
return false
}
}
/** Map a docs URL path to a markdown file path (leading slash, .md suffix). */
export function docPathToMarkdown(path: string): string {
const normalized = normalizeDocPath(path)
if (normalized === '/') {
return '/docs/index.md'
}
return `${normalized}.md`
}
+42 -1
View File
@@ -136,7 +136,9 @@ export const NAMED_THEME_PRESETS = {
export type NamedThemePreset = keyof typeof NAMED_THEME_PRESETS
export type ThemePreset = typeof THEME_PRESETS[number]
export const THEME_STORAGE_KEY = 'aiui-theme-preference'
export type ThemePreference = 'light' | 'dark' | 'system'
export const RADIUS_PRESETS = {
none: '0',
@@ -146,6 +148,8 @@ export const RADIUS_PRESETS = {
xl: '1rem',
} as const
export type ThemePreset = typeof THEME_PRESETS[number]
export type RadiusPreset = keyof typeof RADIUS_PRESETS
// CSS variable names that shadcn/Tailwind expects
@@ -199,6 +203,43 @@ export function isDarkMode(): boolean {
return document.documentElement.classList.contains('dark')
}
/**
* Apply stored user theme preference over YAML default dark mode.
*/
export function applyStoredThemePreference(yamlDefaultDark: boolean): void {
let stored: string | null = null
try {
stored = localStorage.getItem(THEME_STORAGE_KEY)
} catch {
return
}
if (stored === 'light') {
applyDarkMode(false)
} else if (stored === 'dark') {
applyDarkMode(true)
} else if (stored === 'system') {
applyDarkMode(window.matchMedia('(prefers-color-scheme: dark)').matches)
} else {
applyDarkMode(yamlDefaultDark)
}
}
/**
* Persist and apply a theme preference.
*/
export function setThemePreference(preference: ThemePreference): void {
try {
localStorage.setItem(THEME_STORAGE_KEY, preference)
} catch {
// ignore storage failures
}
if (preference === 'system') {
applyDarkMode(window.matchMedia('(prefers-color-scheme: dark)').matches)
} else {
applyDarkMode(preference === 'dark')
}
}
/**
* Get list of available theme presets
*/
+49
View File
@@ -4,6 +4,8 @@ export interface NavItem {
title: string
href?: string
path?: string
description?: string
noindex?: boolean
children?: NavItem[]
}
@@ -24,6 +26,8 @@ export interface SEOConfig {
titleTemplate?: string
defaultImage?: string
twitter?: Record<string, string>
siteUrl?: string
robots?: { index?: boolean; follow?: boolean }
}
export interface I18nConfig {
@@ -47,6 +51,44 @@ export interface LogoConfig {
href?: string
}
export interface NavTabGroupConfig {
group: string
icon?: string
prefix?: string
pages?: string[]
}
export interface NavTabConfig {
tab: string
url?: string
groups?: NavTabGroupConfig[]
}
export interface NavigationConfig {
tabs?: NavTabConfig[]
}
export interface NavbarLinkConfig {
label: string
href: string
}
export interface NavbarPrimaryConfig {
type?: 'button' | 'github'
label?: string
href: string
}
export interface NavbarConfig {
primary?: NavbarPrimaryConfig
links?: NavbarLinkConfig[]
}
export interface SearchConfig {
enabled?: boolean
provider?: string
}
export interface WidgetConfig {
type: string
props?: Record<string, unknown>
@@ -89,6 +131,13 @@ export interface UIConfig {
i18n?: I18nConfig
a11y?: A11yConfig
logo?: LogoConfig
navigation?: NavigationConfig
navbar?: NavbarConfig
search?: SearchConfig
footer?: {
socials?: Record<string, string>
links?: { header: string; items: NavbarLinkConfig[] }[]
}
}
export interface DocsNav {
+1 -1
View File
@@ -1,3 +1,3 @@
"""Version information."""
__version__ = "0.3.142"
__version__ = "0.3.143"
+362 -41
View File
@@ -65,7 +65,8 @@ class Compiler:
# Ensure output directory exists
output_dir.mkdir(parents=True, exist_ok=True)
files: list[str] = []
(output_dir / ".nojekyll").touch()
files: list[str] = [".nojekyll"]
indent = None if minify else 2
@@ -111,6 +112,11 @@ class Compiler:
nav = self._generate_docs_nav()
route_files = self._generate_route_pages(output_dir, nav)
files.extend(route_files)
files.extend(self._generate_sitemap(output_dir, nav))
files.extend(self._generate_robots(output_dir))
files.extend(self._generate_llms_txt(output_dir, nav))
files.extend(self._generate_llms_full_txt(output_dir, nav))
self._patch_homepage_seo(output_dir, nav)
return CompileResult(success=True, files=files)
@@ -322,6 +328,8 @@ class Compiler:
"titleTemplate": self.config.seo.title_template,
"defaultImage": self.config.seo.default_image,
"twitter": self.config.seo.twitter,
"siteUrl": self.config.seo.site_url or self._site_url(),
"robots": self.config.seo.robots,
}
# A11y configuration - basic implementation for frontend
@@ -441,6 +449,7 @@ class Compiler:
output_dir / "index.html",
)
self._patch_antiflicker(output_dir / "index.html")
self._patch_static_shell(output_dir / "index.html")
# Create 404.html as a copy of patched index.html
# for SPA routing on static hosts like GitHub Pages
shutil.copy(
@@ -470,15 +479,39 @@ class Compiler:
self._copy_plugins(output_dir, frontend_dir)
def _patch_antiflicker(self, html_path: Path) -> None:
"""Patch anti-flicker script in index.html to respect darkMode."""
"""Patch theme bootstrap in index.html to respect darkMode."""
theme = self.config.site.theme
dark_mode = theme.dark_mode if theme else True
html = html_path.read_text(encoding="utf-8")
if not dark_mode:
# Remove the dark class addition for light-mode sites
html = html.replace("__AIUI_DARK_DEFAULT__", "true" if dark_mode else "false")
html_path.write_text(html, encoding="utf-8")
def _patch_static_shell(self, html_path: Path) -> None:
"""Patch built index.html for static hosts (GitHub Pages).
- Strip trailing slashes from the URL bar before React hydrates.
- Load plugin-loader.js (not included in the Vite bundle).
"""
html = html_path.read_text(encoding="utf-8")
canonical_script = (
'<script id="aiui-canonical-url">'
"(function(){var p=location.pathname.replace(/\\/+$/, '')||'/';"
"if(p!==location.pathname){history.replaceState(null,'',"
"p+location.search+location.hash);}})();"
"</script>"
)
if 'id="aiui-canonical-url"' not in html:
html = html.replace("<head>", f"<head>\n {canonical_script}", 1)
if 'rel="sitemap"' not in html:
html = html.replace(
"document.documentElement.classList.add('dark');",
"/* light mode — no dark class */",
"<head>",
'<head>\n <link rel="sitemap" type="application/xml" href="/sitemap.xml" />',
1,
)
if "plugin-loader.js" not in html:
html = html.replace(
"</body>",
' <script src="/plugins/plugin-loader.js"></script>\n</body>',
)
html_path.write_text(html, encoding="utf-8")
@@ -528,8 +561,10 @@ class Compiler:
def _generate_route_pages(self, output_dir: Path, nav: dict) -> list[str]:
"""Generate per-route HTML files for SPA fallback and SEO.
For each page in the docs navigation, creates an index.html at
the route path (e.g. _site/docs/concepts/configuration/index.html).
For each page in the docs navigation, creates a flat HTML file at
the route path (e.g. _site/docs/concepts/configuration.html).
GitHub Pages serves /docs/concepts/configuration from that file
without appending a trailing slash.
Each file is a copy of the SPA shell with page-specific:
- <title> tag
- <meta name="description">
@@ -554,15 +589,16 @@ class Compiler:
path = page["path"] # e.g. "/docs/concepts/configuration"
title = page["title"] # e.g. "YAML Configuration"
# Build file system path
# Build file system path — flat .html file so static hosts (GitHub Pages)
# serve /docs/testing without a trailing-slash redirect.
relative = path.lstrip("/")
page_dir = output_dir / relative
page_dir.mkdir(parents=True, exist_ok=True)
page_file = page_dir / "index.html"
page_file = output_dir / f"{relative}.html"
page_file.parent.mkdir(parents=True, exist_ok=True)
# Build page-specific HTML
page_title = f"{title} | {site_title}"
page_desc = f"{title} - {site_desc}"
page_desc = page.get("description") or f"{title} - {site_desc}"
page_noindex = bool(page.get("noindex"))
html = template_html
@@ -579,50 +615,283 @@ class Compiler:
)
# Add canonical URL, OG tags, and noscript content before </head>
seo_tags = self._build_seo_tags(path, page_title, page_desc)
seo_tags = self._build_seo_tags(path, page_title, page_desc, noindex=page_noindex)
json_ld = self._build_json_ld(path, title, page_desc)
# Try to load markdown content for noscript pre-rendering
noscript_content = self._get_noscript_content(output_dir, path)
crawl_nav = self._build_crawl_nav_html(nav)
noscript_block = crawl_nav
if noscript_content:
noscript_block += noscript_content
# Inject SEO tags before </head>
html = html.replace(
"</head>",
f"{seo_tags}\n</head>",
f"{seo_tags}\n{json_ld}\n</head>",
)
# Inject noscript block inside <div id="root">
if noscript_content:
html = html.replace(
'<div id="root"></div>',
f'<div id="root"><noscript>{noscript_content}</noscript></div>',
)
# Inject noscript crawl content inside <div id="root">
html = html.replace(
'<div id="root"></div>',
f'<div id="root"><noscript>{noscript_block}</noscript></div>',
)
page_file.write_text(html, encoding="utf-8")
files.append(f"{relative}/index.html")
files.append(f"{relative}.html")
return files
def _collect_nav_pages(self, nav: dict) -> list[dict]:
"""Recursively collect all pages from docs-nav.json."""
pages: list[dict] = []
for item in nav.get("items", []):
if item.get("path"):
pages.append({"path": item["path"], "title": item.get("title", "")})
for child in item.get("children", []):
if child.get("path"):
pages.append({"path": child["path"], "title": child.get("title", "")})
# Handle deeper nesting if present
for grandchild in child.get("children", []):
if grandchild.get("path"):
pages.append(
{"path": grandchild["path"], "title": grandchild.get("title", "")}
)
def walk(items: list[dict]) -> None:
for item in items:
if item.get("path"):
pages.append(
{
"path": item["path"],
"title": item.get("title", ""),
"description": item.get("description", ""),
"noindex": bool(item.get("noindex")),
}
)
if item.get("children"):
walk(item["children"])
walk(nav.get("items", []))
return pages
def _build_seo_tags(self, path: str, title: str, description: str) -> str:
def _site_url(self) -> str | None:
if self.config.seo and self.config.seo.site_url:
return self.config.seo.site_url.rstrip("/")
for candidate in (self.base_path / "CNAME", self.base_path / "docs" / "CNAME"):
if candidate.exists():
domain = candidate.read_text(encoding="utf-8").strip()
if domain and "." in domain:
return f"https://{domain.rstrip('/')}"
return None
def _absolute_url(self, path: str) -> str:
site = self._site_url()
if not site:
return path
if not path.startswith("/"):
path = f"/{path}"
return f"{site}{path}"
def _generate_sitemap(self, output_dir: Path, nav: dict) -> list[str]:
site = self._site_url()
if not site:
return []
urls = []
site_root = self._absolute_url("/")
root_lastmod = self._format_lastmod(self.base_path / "docs" / "index.md")
urls.append(self._sitemap_url_entry(site_root, root_lastmod))
for page in self._collect_nav_pages(nav):
if page.get("noindex"):
continue
path = page["path"].rstrip("/") or "/"
loc = self._absolute_url(path)
lastmod = self._format_lastmod(self._resolve_md_source(path))
urls.append(self._sitemap_url_entry(loc, lastmod))
xml = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
+ "\n".join(urls)
+ "\n</urlset>\n"
)
(output_dir / "sitemap.xml").write_text(xml, encoding="utf-8")
return ["sitemap.xml"]
@staticmethod
def _sitemap_url_entry(loc: str, lastmod: str | None) -> str:
safe_loc = Compiler._escape_html(loc)
if lastmod:
return f" <url><loc>{safe_loc}</loc><lastmod>{lastmod}</lastmod></url>"
return f" <url><loc>{safe_loc}</loc></url>"
@staticmethod
def _format_lastmod(path: Path | None) -> str | None:
if not path or not path.is_file():
return None
from datetime import datetime, timezone
mtime = path.stat().st_mtime
return datetime.fromtimestamp(mtime, tz=timezone.utc).strftime("%Y-%m-%d")
def _resolve_md_source(self, route_path: str) -> Path | None:
"""Map a docs route path to its source markdown file."""
if not self.config.content or not self.config.content.docs:
return None
base = self.config.site.route_base_docs.rstrip("/")
if not route_path.startswith(base):
return None
rel = route_path[len(base) :].lstrip("/")
docs_dir = self.base_path / self.config.content.docs.dir
candidates = [
docs_dir / f"{rel}.md",
docs_dir / rel / "index.md",
docs_dir / rel / "README.md",
]
if rel in ("", "index"):
candidates.insert(0, docs_dir / "index.md")
for candidate in candidates:
if candidate.is_file():
return candidate
return None
def _generate_robots(self, output_dir: Path) -> list[str]:
robots = self.config.seo.robots if self.config.seo else None
allow_index = robots.get("index", True) if robots else True
allow_follow = robots.get("follow", True) if robots else True
lines = ["User-agent: *"]
if allow_index:
lines.append("Allow: /")
else:
lines.append("Disallow: /")
if not allow_follow:
lines.append("Disallow: /docs/")
site = self._site_url()
if site:
lines.append(f"Sitemap: {site}/sitemap.xml")
lines.append(f"# LLM index: {site}/llms.txt")
(output_dir / "robots.txt").write_text("\n".join(lines) + "\n", encoding="utf-8")
return ["robots.txt"]
def _generate_llms_txt(self, output_dir: Path, nav: dict) -> list[str]:
"""Generate llms.txt index for AI crawlers (llmstxt.org convention)."""
site = self._site_url()
if not site:
return []
site_title = self.config.site.title
site_desc = self.config.site.description or f"Documentation for {site_title}"
lines = [
f"# {site_title}",
"",
f"> {site_desc}",
"",
"## Documentation",
"",
]
for page in self._collect_nav_pages(nav):
if page.get("noindex"):
continue
path = page["path"].rstrip("/") or "/"
title = page["title"]
desc = page.get("description") or title
url = self._absolute_url(path)
lines.append(f"- [{title}]({url}): {desc}")
lines.extend(["", "## Full export", "", f"- [Complete documentation]({site}/llms-full.txt): All pages in one file"])
content = "\n".join(lines).rstrip() + "\n"
(output_dir / "llms.txt").write_text(content, encoding="utf-8")
return ["llms.txt"]
def _generate_llms_full_txt(self, output_dir: Path, nav: dict) -> list[str]:
"""Generate llms-full.txt — concatenated markdown for AI crawlers."""
site = self._site_url()
if not site:
return []
sections: list[str] = []
for page in self._collect_nav_pages(nav):
if page.get("noindex"):
continue
md_path = self._resolve_md_source(page["path"])
if not md_path:
continue
body = self._strip_frontmatter(md_path.read_text(encoding="utf-8")).strip()
if not body:
continue
url = self._absolute_url(page["path"].rstrip("/") or "/")
title = page["title"]
sections.append(f"# {title}\n\nSource: {url}\n\n{body}")
if not sections:
return []
(output_dir / "llms-full.txt").write_text("\n\n---\n\n".join(sections) + "\n", encoding="utf-8")
return ["llms-full.txt"]
@staticmethod
def _strip_frontmatter(content: str) -> str:
if content.startswith("---"):
end_idx = content.find("---", 3)
if end_idx != -1:
return content[end_idx + 3 :].lstrip("\n")
return content
def _patch_homepage_seo(self, output_dir: Path, nav: dict) -> None:
"""Add canonical and crawl hints to root index.html."""
index_path = output_dir / "index.html"
if not index_path.exists():
return
docs_home = "/docs/index"
for page in self._collect_nav_pages(nav):
if page["path"].rstrip("/").endswith("/docs/index") or page["path"] == "/docs/index":
docs_home = page["path"].rstrip("/") or "/docs/index"
break
site_title = self.config.site.title
site_desc = self.config.site.description or f"Documentation for {site_title}"
page_title = f"{site_title}"
seo_tags = self._build_seo_tags(docs_home, page_title, site_desc)
crawl_nav = self._build_crawl_nav_html(nav)
html = index_path.read_text(encoding="utf-8")
html = html.replace(
"<title>Documentation</title>",
f"<title>{self._escape_html(page_title)}</title>",
)
html = html.replace(
'content="Documentation built with PraisonAIUI"',
f'content="{self._escape_html(site_desc)}"',
)
if "</head>" in html and 'rel="canonical"' not in html:
html = html.replace("</head>", f"{seo_tags}\n</head>", 1)
if '<div id="root"></div>' in html:
html = html.replace(
'<div id="root"></div>',
f'<div id="root"><noscript>{crawl_nav}</noscript></div>',
)
index_path.write_text(html, encoding="utf-8")
def _build_crawl_nav_html(self, nav: dict) -> str:
"""Static nav links for non-JS crawlers."""
links: list[str] = []
for page in self._collect_nav_pages(nav):
path = page["path"].rstrip("/") or "/"
title = self._escape_html(page["title"])
url = self._escape_html(self._absolute_url(path))
links.append(f'<li><a href="{url}">{title}</a></li>')
if not links:
return ""
return (
'<nav aria-label="Documentation">'
f'<ul>{"".join(links)}</ul>'
"</nav>"
)
def _build_seo_tags(self, path: str, title: str, description: str, *, noindex: bool = False) -> str:
"""Build canonical, OG, and Twitter meta tags."""
d = self._escape_html(description)
p = self._escape_html(path)
canonical = self._escape_html(self._absolute_url(path))
og_url = canonical
# Extract original page title by removing the site title suffix if present
site_title = self.config.site.title
@@ -646,15 +915,19 @@ class Compiler:
t = self._escape_html(title)
tags = [
f' <link rel="canonical" href="{p}" />',
f' <link rel="canonical" href="{canonical}" />',
f' <meta property="og:title" content="{t}" />',
f' <meta property="og:description" content="{d}" />',
f' <meta property="og:url" content="{p}" />',
f' <meta property="og:url" content="{og_url}" />',
' <meta property="og:type" content="article" />',
]
# Add default OG image if configured
if self.config.seo and self.config.seo.default_image:
og_image = self._escape_html(self.config.seo.default_image)
image = self.config.seo.default_image
if not image.startswith(("http://", "https://")):
image = self._absolute_url(image)
og_image = self._escape_html(image)
tags.append(f' <meta property="og:image" content="{og_image}" />')
# Add Twitter card tags
@@ -667,8 +940,34 @@ class Compiler:
handle = self._escape_html(self.config.seo.twitter["handle"])
tags.append(f' <meta name="twitter:site" content="{handle}" />')
robots = self.config.seo.robots if self.config.seo else None
if noindex:
tags.append(' <meta name="robots" content="noindex, nofollow" />')
elif robots:
index = "index" if robots.get("index", True) else "noindex"
follow = "follow" if robots.get("follow", True) else "nofollow"
tags.append(f' <meta name="robots" content="{index}, {follow}" />')
return "\n".join(tags)
def _build_json_ld(self, path: str, title: str, description: str) -> str:
url = self._absolute_url(path)
site_url = self._site_url()
payload: dict = {
"@context": "https://schema.org",
"@type": "TechArticle",
"headline": title,
"description": description,
"url": url,
}
if site_url:
payload["isPartOf"] = {
"@type": "WebSite",
"name": self.config.site.title,
"url": f"{site_url}/",
}
return f' <script type="application/ld+json">{json.dumps(payload, ensure_ascii=False)}</script>'
def _get_noscript_content(self, output_dir: Path, path: str) -> str:
"""Load markdown file and convert to simple HTML for noscript block."""
# Try to find the corresponding markdown file
@@ -736,14 +1035,36 @@ class Compiler:
# List items
if re.match(r"^[-*+]\s+", stripped):
text = re.sub(r"^[-*+]\s+", "", stripped)
result.append(f"<li>{text}</li>")
result.append(f"<li>{Compiler._inline_md_to_html(text)}</li>")
continue
# Paragraphs
result.append(f"<p>{stripped}</p>")
result.append(f"<p>{Compiler._inline_md_to_html(stripped)}</p>")
return "\n".join(result)
@staticmethod
def _inline_md_to_html(text: str) -> str:
"""Convert inline markdown (links, emphasis) for noscript HTML."""
escaped = (
text.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
)
def link_repl(match: re.Match[str]) -> str:
label = match.group(1)
href = match.group(2)
if href.startswith("/") or href.startswith("http://") or href.startswith("https://"):
safe_href = href.replace('"', "&quot;")
return f'<a href="{safe_href}">{label}</a>'
return label
escaped = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", link_repl, escaped)
escaped = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", escaped)
escaped = re.sub(r"`([^`]+)`", r"<code>\1</code>", escaped)
return escaped
@staticmethod
def _escape_html(text: str) -> str:
"""Escape HTML special characters for meta tags."""
+39
View File
@@ -15,6 +15,8 @@ class DocPage:
slug: str
title: str
order: int = 0
description: str = ""
noindex: bool = False
frontmatter: dict = field(default_factory=dict)
@@ -82,6 +84,10 @@ class DocsScanner:
# Determine title (from frontmatter or first heading)
title = frontmatter.get("title") or self._extract_title(content) or slug
description = str(frontmatter.get("description", "") or "").strip()
if not description:
description = self._extract_description(content)
noindex = bool(frontmatter.get("noindex", False))
# Extract order from frontmatter or filename
order = frontmatter.get("order", 0)
@@ -96,9 +102,42 @@ class DocsScanner:
slug=slug,
title=title,
order=order,
description=description,
noindex=noindex,
frontmatter=frontmatter,
)
def _extract_description(self, content: str) -> str:
"""First paragraph after frontmatter — used for meta description when omitted."""
if content.startswith("---"):
end_idx = content.find("---", 3)
if end_idx != -1:
content = content[end_idx + 3 :]
in_code = False
for line in content.splitlines():
stripped = line.strip()
if stripped.startswith("```"):
in_code = not in_code
continue
if in_code or not stripped:
continue
if stripped.startswith("#"):
continue
if stripped.startswith(("-", "*", "+", "|", ">", "!", "---")):
continue
text = stripped
text = re.sub(r"!\[([^\]]*)\]\([^)]+\)", r"\1", text)
text = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text)
text = re.sub(r"\*\*([^*]+)\*\*", r"\1", text)
text = re.sub(r"\*([^*]+)\*", r"\1", text)
text = re.sub(r"`([^`]+)`", r"\1", text)
text = re.sub(r"\s+", " ", text).strip()
if len(text) >= 20:
return text[:160].rstrip()
return ""
def _path_to_slug(self, file_path: Path) -> str:
"""Convert a file path to a URL slug."""
rel_path = file_path.relative_to(self.docs_dir)
+12 -1
View File
@@ -13,6 +13,8 @@ class NavItem:
title: str
path: str
description: str = ""
noindex: bool = False
children: list["NavItem"] = field(default_factory=list)
@@ -38,7 +40,12 @@ class NavBuilder:
full_path = f"{self.base_path}/{page.slug}".rstrip("/")
# Create NavItem for this page
item = NavItem(title=page.title, path=full_path)
item = NavItem(
title=page.title,
path=full_path,
description=page.description,
noindex=page.noindex,
)
# Determine parent path
parts = page.slug.split("/")
@@ -76,6 +83,10 @@ class NavBuilder:
def _item_to_dict(self, item: NavItem) -> dict:
"""Convert a NavItem to dictionary."""
result = {"title": item.title, "path": item.path}
if item.description:
result["description"] = item.description
if item.noindex:
result["noindex"] = True
if item.children:
result["children"] = [self._item_to_dict(child) for child in item.children]
return result
+1
View File
@@ -83,6 +83,7 @@ def auto_register_defaults() -> None:
(".workflows", "WorkflowsFeature"),
(".guardrails", "GuardrailsFeature"),
(".eval", "EvalFeature"),
(".training", "TrainingFeature"),
(".telemetry", "TelemetryFeature"),
(".tracing", "TracingFeature"),
(".security", "SecurityFeature"),
+21 -10
View File
@@ -168,7 +168,9 @@ async def run_startup_hooks() -> None:
"""Execute all registered startup hooks.
Called by the server during application startup.
Blocks until all hooks complete.
Has a timeout from AIUI_STARTUP_TIMEOUT (default 60s) so a hung hook
cannot block the server from reaching the lifespan ``yield`` — startup is
marked completed on timeout so ``/health/live`` still responds.
"""
if _lifecycle_state["startup_completed"]:
_log.warning("Startup hooks already executed")
@@ -177,15 +179,24 @@ async def run_startup_hooks() -> None:
_log.info(f"Running {len(_startup_hooks)} startup hooks")
start_time = time.perf_counter()
for hook in _startup_hooks:
try:
_log.debug(f"Executing startup hook: {getattr(hook, '__name__', str(hook))}")
result = hook()
if asyncio.iscoroutine(result):
await result
except Exception as e:
_log.error(f"Startup hook failed: {getattr(hook, '__name__', str(hook))}: {e}")
# Continue with other hooks - individual failures shouldn't break startup
# Get startup timeout from environment (mirrors AIUI_SHUTDOWN_TIMEOUT)
timeout = float(os.environ.get("AIUI_STARTUP_TIMEOUT", "60.0"))
async def _execute_hooks():
for hook in _startup_hooks:
try:
_log.debug(f"Executing startup hook: {getattr(hook, '__name__', str(hook))}")
result = hook()
if asyncio.iscoroutine(result):
await result
except Exception as e:
_log.error(f"Startup hook failed: {getattr(hook, '__name__', str(hook))}: {e}")
# Continue with other hooks - individual failures shouldn't break startup
try:
await asyncio.wait_for(_execute_hooks(), timeout=timeout)
except asyncio.TimeoutError:
_log.error(f"Startup hooks timed out after {timeout}s")
execution_time = time.perf_counter() - start_time
_lifecycle_state.update(
+380
View File
@@ -0,0 +1,380 @@
"""Training Lab feature — read-mostly proxy for praisonai-train sessions.
Architecture:
TrainingFeature (BaseFeatureProtocol)
├── reads JSON session files from ~/.praison/train/*.json
└── optionally delegates apply to praisonai_train.train.agents.apply_training
The praisonai-train package is an optional dependency. When it is not installed
the feature degrades gracefully: session listing/detail still work from the raw
JSON storage, while apply returns a 503 with an install hint.
"""
from __future__ import annotations
import json
import logging
import os
from pathlib import Path
from typing import Any
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.routing import Route
from ._base import BaseFeatureProtocol
logger = logging.getLogger(__name__)
_EARLY_STOP_THRESHOLD = 9.5
def _storage_dir() -> Path:
"""Resolve the praisonai-train JSON storage directory."""
override = os.environ.get("PRAISON_TRAIN_DIR")
if override:
return Path(override).expanduser()
return Path.home() / ".praison" / "train"
def _train_available() -> bool:
"""Return True when the praisonai-train package can be imported."""
import importlib.util
return importlib.util.find_spec("praisonai_train") is not None
def _train_version() -> str | None:
try:
from importlib.metadata import version
return version("praisonai-train")
except Exception:
return None
def _iter_scores(iterations: list[dict[str, Any]]) -> list[float]:
scores: list[float] = []
for it in iterations:
score = it.get("score")
if isinstance(score, (int, float)):
scores.append(float(score))
return scores
def _best_iteration_index(iterations: list[dict[str, Any]]) -> int | None:
best_idx: int | None = None
best_score = float("-inf")
for idx, it in enumerate(iterations):
score = it.get("score")
if isinstance(score, (int, float)) and float(score) > best_score:
best_score = float(score)
best_idx = idx
return best_idx
def _summarize_session(session_id: str, path: Path, data: dict[str, Any]) -> dict[str, Any]:
"""Build a session summary row (parity with praisonai-train list)."""
report = data.get("report") or {}
metadata = report.get("metadata") or data.get("metadata") or {}
iterations = data.get("iterations") or report.get("iterations") or []
completed = int(report.get("total_iterations", len(iterations)) or 0)
requested = metadata.get("target_iterations")
requested = int(requested) if isinstance(requested, (int, float)) else completed
mode = metadata.get("mode", "llm")
agent_id = metadata.get("agent_id") or data.get("agent_id")
scores = _iter_scores(iterations)
avg_score = report.get("avg_score")
if avg_score is None and scores:
avg_score = sum(scores) / len(scores)
improvement = report.get("improvement")
if improvement is None and len(scores) >= 2:
improvement = scores[-1] - scores[0]
passed = report.get("passed")
early_stopped = bool(requested > completed and mode == "llm")
try:
stat = path.stat()
size_bytes = stat.st_size
modified_at = _iso_from_mtime(stat.st_mtime)
except OSError:
size_bytes = 0
modified_at = None
return {
"session_id": session_id,
"agent_id": agent_id,
"completed_iterations": completed,
"requested_iterations": requested,
"early_stopped": early_stopped,
"avg_score": round(float(avg_score), 4) if avg_score is not None else None,
"improvement": round(float(improvement), 4) if improvement is not None else None,
"passed": passed,
"status_label": _status_label(passed, completed),
"mode": mode,
"size_bytes": size_bytes,
"modified_at": modified_at,
}
def _iso_from_mtime(mtime: float) -> str:
from datetime import datetime, timezone
return datetime.fromtimestamp(mtime, tz=timezone.utc).replace(microsecond=0).isoformat()
def _status_label(passed: Any, completed: int) -> str:
if completed == 0:
return "INCOMPLETE"
if passed is True:
return "PASSED"
if passed is False:
return "NEEDS WORK"
return "UNKNOWN"
def _load_session_file(path: Path) -> dict[str, Any]:
with path.open("r", encoding="utf-8") as fh:
return json.load(fh)
class TrainingFeature(BaseFeatureProtocol):
"""Training Lab — inspect praisonai-train agent sessions and apply profiles."""
feature_name = "training"
feature_description = "Inspect praisonai-train agent sessions and apply training profiles"
@property
def name(self) -> str:
return self.feature_name
@property
def description(self) -> str:
return self.feature_description
def routes(self) -> list[Route]:
return [
Route("/api/training/status", self._status, methods=["GET"]),
Route("/api/training/sessions", self._list_sessions, methods=["GET"]),
Route("/api/training/sessions/{session_id}", self._get_session, methods=["GET"]),
Route("/api/training/sessions/{session_id}/apply", self._apply, methods=["POST"]),
]
async def health(self) -> dict[str, Any]:
available = _train_available()
return {
"status": "ok" if available else "degraded",
"feature": self.name,
"train_package_available": available,
}
# ── storage helpers ──────────────────────────────────────────────
def _read_sessions(self, storage: Path) -> list[dict[str, Any]]:
if not storage.exists():
return []
summaries: list[dict[str, Any]] = []
for path in storage.glob("*.json"):
session_id = path.stem
try:
data = _load_session_file(path)
except (OSError, json.JSONDecodeError):
logger.warning("Skipping unreadable training session: %s", path)
continue
summaries.append(_summarize_session(session_id, path, data))
summaries.sort(key=lambda s: s.get("modified_at") or "", reverse=True)
return summaries
# ── route handlers ───────────────────────────────────────────────
async def _status(self, request: Request) -> JSONResponse:
storage = _storage_dir()
available = _train_available()
sessions = self._read_sessions(storage)
warnings: list[str] = []
if not available:
warnings.append(
"praisonai-train not installed. Install with: pip install praisonai-train"
)
return JSONResponse(
{
"status": "ok" if available else "degraded",
"feature": self.name,
"train_package_available": available,
"train_package_version": _train_version(),
"storage_dir": str(storage),
"storage_backend": "json",
"session_count": len(sessions),
"sqlite_supported": False,
"warnings": warnings,
}
)
async def _list_sessions(self, request: Request) -> JSONResponse:
storage = _storage_dir()
try:
limit = int(request.query_params.get("limit", "50"))
except ValueError:
limit = 50
agent_id = request.query_params.get("agent_id")
sessions = self._read_sessions(storage)
if agent_id:
sessions = [s for s in sessions if s.get("agent_id") == agent_id]
limited = sessions[: max(limit, 0)]
return JSONResponse({"sessions": limited, "count": len(limited)})
async def _get_session(self, request: Request) -> JSONResponse:
session_id = request.path_params["session_id"]
storage = _storage_dir()
path = storage / f"{session_id}.json"
if not path.exists():
return JSONResponse(
{"error": f"Session not found: {session_id}"}, status_code=404
)
try:
data = _load_session_file(path)
except (OSError, json.JSONDecodeError):
return JSONResponse(
{"error": f"Corrupt session JSON: {session_id}"}, status_code=422
)
report = data.get("report") or {}
iterations = list(data.get("iterations") or report.get("iterations") or [])
best_idx = _best_iteration_index(iterations)
for idx, it in enumerate(iterations):
it["is_best"] = idx == best_idx
summary = _summarize_session(session_id, path, data)
best_iteration = iterations[best_idx] if best_idx is not None else None
return JSONResponse(
{
"session_id": session_id,
"report": report,
"iterations": iterations,
"best_iteration": best_iteration,
"best_iteration_num": (
best_iteration.get("iteration_num", best_idx + 1)
if best_iteration is not None
else None
),
"early_stopped": summary["early_stopped"],
"early_stop_reason": (
"score_threshold"
if summary["early_stopped"]
and _iter_scores(iterations)
and _iter_scores(iterations)[-1] >= _EARLY_STOP_THRESHOLD
else None
),
"requested_iterations": summary["requested_iterations"],
"completed_iterations": summary["completed_iterations"],
"avg_score": summary["avg_score"],
"improvement": summary["improvement"],
"passed": summary["passed"],
"mode": summary["mode"],
"scenarios": data.get("scenarios") or [],
}
)
async def _apply(self, request: Request) -> JSONResponse:
session_id = request.path_params["session_id"]
storage = _storage_dir()
path = storage / f"{session_id}.json"
if not path.exists():
return JSONResponse(
{"error": f"Session not found: {session_id}"}, status_code=404
)
if not _train_available():
return JSONResponse(
{
"error": "praisonai-train is not installed",
"hint": "pip install praisonai-train",
},
status_code=503,
)
try:
body = await request.json()
except Exception:
body = {}
agent_id = body.get("agent_id")
if not agent_id:
return JSONResponse({"error": "agent_id is required"}, status_code=422)
iteration = body.get("iteration")
from .agents import get_agent_registry
registry = get_agent_registry()
agent_def = registry.get(agent_id)
if agent_def is None:
return JSONResponse(
{"error": f"Agent not found: {agent_id}"}, status_code=404
)
try:
result = self._apply_training(agent_def, session_id, iteration)
except Exception as exc:
logger.exception("apply_training failed for session %s", session_id)
return JSONResponse({"error": str(exc)}, status_code=500)
return JSONResponse(
{
"success": True,
"session_id": session_id,
"agent_id": agent_id,
"iteration_applied": result.get("iteration_applied"),
"quality_score": result.get("quality_score"),
"suggestions_count": result.get("suggestions_count", 0),
"hook_id": result.get("hook_id", f"training_hook_{session_id}"),
"message": f"Training profile applied to agent '{agent_id}'",
}
)
def _apply_training(
self, agent_def: dict[str, Any], session_id: str, iteration: int | None
) -> dict[str, Any]:
from praisonai_train.train.agents import apply_training # type: ignore
from praisonaiagents import Agent # type: ignore
agent = Agent(
name=agent_def.get("name", agent_def.get("id", "agent")),
instructions=agent_def.get("instructions", agent_def.get("description", "")),
llm=agent_def.get("model"),
)
apply_training(agent, session_id=session_id, iteration=iteration)
hook_id = getattr(agent, "_training_hook_id", f"training_hook_{session_id}")
storage = _storage_dir()
applied_iteration = iteration
quality_score = None
suggestions_count = 0
try:
data = _load_session_file(storage / f"{session_id}.json")
iterations = list(data.get("iterations") or [])
idx = _best_iteration_index(iterations) if iteration is None else None
chosen = None
if idx is not None:
chosen = iterations[idx]
applied_iteration = chosen.get("iteration_num", idx + 1)
elif iteration is not None:
for it in iterations:
if it.get("iteration_num") == iteration:
chosen = it
break
if chosen is not None:
quality_score = chosen.get("score")
suggestions_count = len(chosen.get("suggestions") or [])
except (OSError, json.JSONDecodeError):
pass
return {
"iteration_applied": applied_iteration,
"quality_score": quality_score,
"suggestions_count": suggestions_count,
"hook_id": hook_id,
}
+3 -1
View File
@@ -187,9 +187,11 @@ class A11yConfig(BaseModel):
class SEOConfig(BaseModel):
"""SEO configuration."""
title_template: str = Field(default="%s", alias="titleTemplate")
title_template: str = Field(default="%s | %s", alias="titleTemplate")
default_image: Optional[str] = Field(default=None, alias="defaultImage")
twitter: Optional[dict[str, str]] = None
site_url: Optional[str] = Field(default=None, alias="siteUrl")
robots: Optional[dict[str, bool]] = None
class DependenciesConfig(BaseModel):
+68 -7
View File
@@ -687,12 +687,21 @@ def _build_html(style: str) -> str:
title = f"{_site_title} Dashboard" if style == "dashboard" else _site_title
cache_bust = int(_server_start_time)
# Anti-flicker: dark background + hide React content until plugins render
# Anti-flicker: hide React content until plugins render (uses theme CSS vars)
anti_flicker = ""
theme_bootstrap = (
'<script id="aiui-theme-bootstrap">'
"(function(){try{var t=localStorage.getItem('aiui-theme-preference');"
"if(t==='dark'||(t==='system'&&matchMedia('(prefers-color-scheme: dark)').matches))"
"document.documentElement.classList.add('dark');"
"else if(t==='light')document.documentElement.classList.remove('dark');"
"}catch(e){}})();"
"</script>"
)
if style == "docs":
anti_flicker = (
'<style id="aiui-anti-flicker">'
"html,body{background:#0f172a!important;color:#e2e8f0}"
"html,body{background:hsl(var(--background))!important;color:hsl(var(--foreground))}"
"#root>*{opacity:0;transition:opacity .15s ease}"
"</style>"
)
@@ -751,16 +760,26 @@ def _build_html(style: str) -> str:
except Exception:
pass
canonical_script = (
'<script id="aiui-canonical-url">'
"(function(){var p=location.pathname.replace(/\\/+$/, '')||'/';"
"if(p!==location.pathname){history.replaceState(null,'',"
"p+location.search+location.hash);}})();"
"</script>"
)
return (
'<!doctype html><html lang="en" style="background:#0f172a;color:#e2e8f0"><head>'
'<!doctype html><html lang="en"><head>'
'<meta charset="UTF-8">'
'<meta name="viewport" content="width=device-width,initial-scale=1.0">'
f"{canonical_script}"
f"{theme_bootstrap}"
f'<link rel="stylesheet" href="/assets/index.css?v={cache_bust}">'
f"{anti_flicker}"
f"{theme_vars_tag}"
f"{custom_css_tag}"
f"<title>{title}</title>"
'</head><body style="background:#0f172a;margin:0">'
'</head><body style="margin:0">'
'<div id="root"></div>'
f"{react_script}"
f'<script src="/plugins/plugin-loader.js?v={cache_bust}"></script>'
@@ -813,10 +832,8 @@ async def _plugins_config(request: Request) -> JSONResponse:
"syntax-highlight",
"code-copy",
"content-loader",
"toc",
"mermaid",
"nav-intercept",
"mkdocs-compat",
"homepage",
]
else:
@@ -3320,6 +3337,14 @@ def create_app(
"description": "Manage conversation sessions",
"order": 20,
},
{
"id": "runs",
"title": "Runs",
"icon": "⏱️",
"group": "Control",
"description": "Run flight recorder — timeline debugger",
"order": 22,
},
{
"id": "usage",
"title": "Usage",
@@ -3496,6 +3521,14 @@ def create_app(
"description": "Agent evaluation & accuracy",
"order": 40,
},
{
"id": "training",
"title": "Training",
"icon": "🧪",
"group": "Control",
"description": "Inspect praisonai-train sessions & apply profiles",
"order": 42,
},
{
"id": "code-studio",
"title": "Code Studio",
@@ -3548,6 +3581,7 @@ def create_app(
]
_page_api_overrides = {
"sessions": "/sessions",
"runs": "/api/sessions",
"cron": "/api/schedules",
"channels": "/api/channels",
"mcp": "/api/mcp/servers",
@@ -3587,6 +3621,8 @@ def create_app(
# Always mount built-in frontend plugins and assets BEFORE catch-all /
_frontend_dir = Path(__file__).parent / "templates" / "frontend"
_plugins_dir = _frontend_dir / "plugins"
if static_dir and (Path(static_dir) / "plugins").is_dir():
_plugins_dir = Path(static_dir) / "plugins"
_assets_dir = _frontend_dir / "assets"
# Dynamic plugins.json route — MUST come before static /plugins mount
@@ -3613,6 +3649,13 @@ def create_app(
routes.append(Route("/", _serve_index, methods=["GET"]))
_static_app = StaticFiles(directory=str(static_dir), html=True)
_static_root = Path(static_dir)
async def _serve_static_file(scope, receive, send, file_path: Path, media_type: str | None = None):
from starlette.responses import FileResponse
response = FileResponse(str(file_path), media_type=media_type)
await response(scope, receive, send)
async def _http_only_static(scope, receive, send):
if scope["type"] != "http":
@@ -3621,7 +3664,25 @@ def create_app(
await send({"type": "websocket.close", "code": 1008})
return
# Try StaticFiles first; on 404, fall back to SPA index
request_path = scope.get("path", "")
rel = request_path.lstrip("/")
if rel:
exact = _static_root / rel
if exact.is_file():
await _serve_static_file(scope, receive, send, exact)
return
if not request_path.endswith("/"):
html_path = _static_root / f"{rel}.html"
if html_path.is_file():
await _serve_static_file(scope, receive, send, html_path, media_type="text/html")
return
if exact.is_dir():
index = exact / "index.html"
if index.is_file():
await _serve_static_file(scope, receive, send, index, media_type="text/html")
return
# Try StaticFiles; on 404, fall back to SPA index
# (paths like /chat, /memory don't have static files but need
# the SPA shell so JS routing can take over).
response_started = False
@@ -0,0 +1 @@
import{g as p,r as u,d as a}from"./chunk-SVP7TREG.js";import{p as f}from"./chunk-JWPE2WC7.js";import{_ as n,l as o}from"./mermaid.core.js";import{M as c,b as d}from"./cynefin-OW5HDTMX.js";import"./index.js";var v=d().RailroadAbnf.parser.LangiumParser,i=n(e=>{const r=e.alternatives.map(g);return r.length===1?r[0]:{type:"choice",alternatives:r}},"transformAlternation"),g=n(e=>{const r=e.elements.map(y);return r.length===1?r[0]:{type:"sequence",elements:r}},"transformConcatenation"),b=n(e=>{if(e.includes("*")){const[t,s]=e.split("*"),l=t?parseInt(t,10):0,m=s?parseInt(s,10):1/0;return{min:l,max:m}}const r=parseInt(e,10);return{min:r,max:r}},"parseRepeat"),y=n(e=>{const r=A(e.primary);if(!e.repeat)return r;const{min:t,max:s}=b(e.repeat);return t===0&&s===1?{type:"optional",element:r}:{type:"repetition",element:r,min:t,max:s}},"transformElement"),A=n(e=>{switch(e.$type){case"AbnfStringLiteral":return{type:"terminal",value:e.value};case"AbnfNumVal":return{type:"terminal",value:e.value};case"AbnfRuleName":return{type:"nonterminal",name:e.name};case"AbnfGroup":return i(e.element);case"AbnfOptionalGroup":return{type:"optional",element:i(e.element)};default:throw new Error(`Unsupported ABNF primary node: ${e.$type}`)}},"transformPrimary"),P=n(e=>({name:e.name,definition:i(e.definition)}),"transformRule"),h=n(e=>{f(e,a),e.title&&a.setTitle(e.title),e.rules.map(r=>a.addRule(P(r)))},"populateDb"),R={parse:n(e=>{a.clear(),o.debug("[ABNF Parser] Starting Langium parse");const r=v.parse(e);if(r.lexerErrors.length>0||r.parserErrors.length>0)throw new c(r);const t=r.value;o.debug("[ABNF Parser] Parsed rules:",t.rules.length),h(t),o.debug("[ABNF Parser] Parse complete")},"parse"),parser:{yy:a}},B={parser:R,db:a,renderer:u,styles:p};export{B as diagram};
@@ -0,0 +1 @@
import{a0 as ln,a1 as an,a2 as H,a3 as q,a4 as B,a5 as un,a6 as y,a7 as tn,a8 as L,a9 as _,aa as rn,ab as o,ac as on,ad as sn,ae as fn}from"./mermaid.core.js";function cn(l){return l.innerRadius}function yn(l){return l.outerRadius}function gn(l){return l.startAngle}function dn(l){return l.endAngle}function mn(l){return l&&l.padAngle}function pn(l,h,I,D,v,A,C,a){var O=I-l,i=D-h,n=C-v,d=a-A,u=d*O-n*i;if(!(u*u<y))return u=(n*(h-A)-d*(l-v))/u,[l+u*O,h+u*i]}function W(l,h,I,D,v,A,C){var a=l-I,O=h-D,i=(C?A:-A)/L(a*a+O*O),n=i*O,d=-i*a,u=l+n,s=h+d,f=I+n,c=D+d,F=(u+f)/2,t=(s+c)/2,m=f-u,g=c-s,R=m*m+g*g,T=v-A,P=u*c-f*s,S=(g<0?-1:1)*L(on(0,T*T*R-P*P)),j=(P*g-m*S)/R,z=(-P*m-g*S)/R,w=(P*g+m*S)/R,p=(-P*m+g*S)/R,x=j-F,e=z-t,r=w-F,G=p-t;return x*x+e*e>r*r+G*G&&(j=w,z=p),{cx:j,cy:z,x01:-n,y01:-d,x11:j*(v/T-1),y11:z*(v/T-1)}}function hn(){var l=cn,h=yn,I=B(0),D=null,v=gn,A=dn,C=mn,a=null,O=ln(i);function i(){var n,d,u=+l.apply(this,arguments),s=+h.apply(this,arguments),f=v.apply(this,arguments)-un,c=A.apply(this,arguments)-un,F=rn(c-f),t=c>f;if(a||(a=n=O()),s<u&&(d=s,s=u,u=d),!(s>y))a.moveTo(0,0);else if(F>tn-y)a.moveTo(s*H(f),s*q(f)),a.arc(0,0,s,f,c,!t),u>y&&(a.moveTo(u*H(c),u*q(c)),a.arc(0,0,u,c,f,t));else{var m=f,g=c,R=f,T=c,P=F,S=F,j=C.apply(this,arguments)/2,z=j>y&&(D?+D.apply(this,arguments):L(u*u+s*s)),w=_(rn(s-u)/2,+I.apply(this,arguments)),p=w,x=w,e,r;if(z>y){var G=sn(z/u*q(j)),M=sn(z/s*q(j));(P-=G*2)>y?(G*=t?1:-1,R+=G,T-=G):(P=0,R=T=(f+c)/2),(S-=M*2)>y?(M*=t?1:-1,m+=M,g-=M):(S=0,m=g=(f+c)/2)}var J=s*H(m),K=s*q(m),N=u*H(T),Q=u*q(T);if(w>y){var U=s*H(g),V=s*q(g),X=u*H(R),Y=u*q(R),E;if(F<an)if(E=pn(J,K,X,Y,U,V,N,Q)){var Z=J-E[0],$=K-E[1],b=U-E[0],k=V-E[1],nn=1/q(fn((Z*b+$*k)/(L(Z*Z+$*$)*L(b*b+k*k)))/2),en=L(E[0]*E[0]+E[1]*E[1]);p=_(w,(u-en)/(nn-1)),x=_(w,(s-en)/(nn+1))}else p=x=0}S>y?x>y?(e=W(X,Y,J,K,s,x,t),r=W(U,V,N,Q,s,x,t),a.moveTo(e.cx+e.x01,e.cy+e.y01),x<w?a.arc(e.cx,e.cy,x,o(e.y01,e.x01),o(r.y01,r.x01),!t):(a.arc(e.cx,e.cy,x,o(e.y01,e.x01),o(e.y11,e.x11),!t),a.arc(0,0,s,o(e.cy+e.y11,e.cx+e.x11),o(r.cy+r.y11,r.cx+r.x11),!t),a.arc(r.cx,r.cy,x,o(r.y11,r.x11),o(r.y01,r.x01),!t))):(a.moveTo(J,K),a.arc(0,0,s,m,g,!t)):a.moveTo(J,K),!(u>y)||!(P>y)?a.lineTo(N,Q):p>y?(e=W(N,Q,U,V,u,-p,t),r=W(J,K,X,Y,u,-p,t),a.lineTo(e.cx+e.x01,e.cy+e.y01),p<w?a.arc(e.cx,e.cy,p,o(e.y01,e.x01),o(r.y01,r.x01),!t):(a.arc(e.cx,e.cy,p,o(e.y01,e.x01),o(e.y11,e.x11),!t),a.arc(0,0,u,o(e.cy+e.y11,e.cx+e.x11),o(r.cy+r.y11,r.cx+r.x11),t),a.arc(r.cx,r.cy,p,o(r.y11,r.x11),o(r.y01,r.x01),!t))):a.arc(0,0,u,T,R,t)}if(a.closePath(),n)return a=null,n+""||null}return i.centroid=function(){var n=(+l.apply(this,arguments)+ +h.apply(this,arguments))/2,d=(+v.apply(this,arguments)+ +A.apply(this,arguments))/2-an/2;return[H(d)*n,q(d)*n]},i.innerRadius=function(n){return arguments.length?(l=typeof n=="function"?n:B(+n),i):l},i.outerRadius=function(n){return arguments.length?(h=typeof n=="function"?n:B(+n),i):h},i.cornerRadius=function(n){return arguments.length?(I=typeof n=="function"?n:B(+n),i):I},i.padRadius=function(n){return arguments.length?(D=n==null?null:typeof n=="function"?n:B(+n),i):D},i.startAngle=function(n){return arguments.length?(v=typeof n=="function"?n:B(+n),i):v},i.endAngle=function(n){return arguments.length?(A=typeof n=="function"?n:B(+n),i):A},i.padAngle=function(n){return arguments.length?(C=typeof n=="function"?n:B(+n),i):C},i.context=function(n){return arguments.length?(a=n??null,i):a},i}export{hn as d};
@@ -0,0 +1,36 @@
import{p as qe}from"./chunk-JWPE2WC7.js";import{_ as ct,D as Qe,Y as Je,l as Fe,b as Ke,a as je,n as _e,o as tr,g as er,s as rr,p as ir,A as ar,y as nr,B as or,c as Ee,b5 as Te,ak as pe,i as sr,j as hr,q as lr,al as fr,b6 as cr}from"./mermaid.core.js";import{p as gr}from"./cynefin-OW5HDTMX.js";import{c as be}from"./cytoscape.esm.js";import{g as ur}from"./index.js";var he={exports:{}},le={exports:{}},fe={exports:{}},dr=fe.exports,De;function vr(){return De||(De=1,(function(x,M){(function(R,T){x.exports=T()})(dr,function(){return(function(w){var R={};function T(v){if(R[v])return R[v].exports;var l=R[v]={i:v,l:!1,exports:{}};return w[v].call(l.exports,l,l.exports,T),l.l=!0,l.exports}return T.m=w,T.c=R,T.i=function(v){return v},T.d=function(v,l,i){T.o(v,l)||Object.defineProperty(v,l,{configurable:!1,enumerable:!0,get:i})},T.n=function(v){var l=v&&v.__esModule?function(){return v.default}:function(){return v};return T.d(l,"a",l),l},T.o=function(v,l){return Object.prototype.hasOwnProperty.call(v,l)},T.p="",T(T.s=28)})([(function(w,R,T){function v(){}v.QUALITY=1,v.DEFAULT_CREATE_BENDS_AS_NEEDED=!1,v.DEFAULT_INCREMENTAL=!1,v.DEFAULT_ANIMATION_ON_LAYOUT=!0,v.DEFAULT_ANIMATION_DURING_LAYOUT=!1,v.DEFAULT_ANIMATION_PERIOD=50,v.DEFAULT_UNIFORM_LEAF_NODE_SIZES=!1,v.DEFAULT_GRAPH_MARGIN=15,v.NODE_DIMENSIONS_INCLUDE_LABELS=!1,v.SIMPLE_NODE_SIZE=40,v.SIMPLE_NODE_HALF_SIZE=v.SIMPLE_NODE_SIZE/2,v.EMPTY_COMPOUND_NODE_SIZE=40,v.MIN_EDGE_LENGTH=1,v.WORLD_BOUNDARY=1e6,v.INITIAL_WORLD_BOUNDARY=v.WORLD_BOUNDARY/1e3,v.WORLD_CENTER_X=1200,v.WORLD_CENTER_Y=900,w.exports=v}),(function(w,R,T){var v=T(2),l=T(8),i=T(9);function r(f,e,d){v.call(this,d),this.isOverlapingSourceAndTarget=!1,this.vGraphObject=d,this.bendpoints=[],this.source=f,this.target=e}r.prototype=Object.create(v.prototype);for(var a in v)r[a]=v[a];r.prototype.getSource=function(){return this.source},r.prototype.getTarget=function(){return this.target},r.prototype.isInterGraph=function(){return this.isInterGraph},r.prototype.getLength=function(){return this.length},r.prototype.isOverlapingSourceAndTarget=function(){return this.isOverlapingSourceAndTarget},r.prototype.getBendpoints=function(){return this.bendpoints},r.prototype.getLca=function(){return this.lca},r.prototype.getSourceInLca=function(){return this.sourceInLca},r.prototype.getTargetInLca=function(){return this.targetInLca},r.prototype.getOtherEnd=function(f){if(this.source===f)return this.target;if(this.target===f)return this.source;throw"Node is not incident with this edge"},r.prototype.getOtherEndInGraph=function(f,e){for(var d=this.getOtherEnd(f),t=e.getGraphManager().getRoot();;){if(d.getOwner()==e)return d;if(d.getOwner()==t)break;d=d.getOwner().getParent()}return null},r.prototype.updateLength=function(){var f=new Array(4);this.isOverlapingSourceAndTarget=l.getIntersection(this.target.getRect(),this.source.getRect(),f),this.isOverlapingSourceAndTarget||(this.lengthX=f[0]-f[2],this.lengthY=f[1]-f[3],Math.abs(this.lengthX)<1&&(this.lengthX=i.sign(this.lengthX)),Math.abs(this.lengthY)<1&&(this.lengthY=i.sign(this.lengthY)),this.length=Math.sqrt(this.lengthX*this.lengthX+this.lengthY*this.lengthY))},r.prototype.updateLengthSimple=function(){this.lengthX=this.target.getCenterX()-this.source.getCenterX(),this.lengthY=this.target.getCenterY()-this.source.getCenterY(),Math.abs(this.lengthX)<1&&(this.lengthX=i.sign(this.lengthX)),Math.abs(this.lengthY)<1&&(this.lengthY=i.sign(this.lengthY)),this.length=Math.sqrt(this.lengthX*this.lengthX+this.lengthY*this.lengthY)},w.exports=r}),(function(w,R,T){function v(l){this.vGraphObject=l}w.exports=v}),(function(w,R,T){var v=T(2),l=T(10),i=T(13),r=T(0),a=T(16),f=T(5);function e(t,s,o,c){o==null&&c==null&&(c=s),v.call(this,c),t.graphManager!=null&&(t=t.graphManager),this.estimatedSize=l.MIN_VALUE,this.inclusionTreeDepth=l.MAX_VALUE,this.vGraphObject=c,this.edges=[],this.graphManager=t,o!=null&&s!=null?this.rect=new i(s.x,s.y,o.width,o.height):this.rect=new i}e.prototype=Object.create(v.prototype);for(var d in v)e[d]=v[d];e.prototype.getEdges=function(){return this.edges},e.prototype.getChild=function(){return this.child},e.prototype.getOwner=function(){return this.owner},e.prototype.getWidth=function(){return this.rect.width},e.prototype.setWidth=function(t){this.rect.width=t},e.prototype.getHeight=function(){return this.rect.height},e.prototype.setHeight=function(t){this.rect.height=t},e.prototype.getCenterX=function(){return this.rect.x+this.rect.width/2},e.prototype.getCenterY=function(){return this.rect.y+this.rect.height/2},e.prototype.getCenter=function(){return new f(this.rect.x+this.rect.width/2,this.rect.y+this.rect.height/2)},e.prototype.getLocation=function(){return new f(this.rect.x,this.rect.y)},e.prototype.getRect=function(){return this.rect},e.prototype.getDiagonal=function(){return Math.sqrt(this.rect.width*this.rect.width+this.rect.height*this.rect.height)},e.prototype.getHalfTheDiagonal=function(){return Math.sqrt(this.rect.height*this.rect.height+this.reLine truncated
.edge {
stroke-width: ${x.archEdgeWidth};
stroke: ${x.archEdgeColor};
fill: none;
}
.arrow {
fill: ${x.archEdgeArrowColor};
}
.node-bkg {
fill: none;
stroke: ${x.archGroupBorderColor};
stroke-width: ${x.archGroupBorderWidth};
stroke-dasharray: 8;
}
.node-icon-text {
display: flex;
align-items: center;
}
.node-icon-text > div {
color: #fff;
margin: 1px;
height: fit-content;
text-align: center;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
}
`,"getStyles"),Fr=Sr;function me(x,M){if(x===0)return M();const w=Math.random;let R=x>>>0;Math.random=function(){R=R+1831565813>>>0;let T=R;return T=Math.imul(T^T>>>15,T|1),T^=T+Math.imul(T^T>>>7,T|61),((T^T>>>14)>>>0)/4294967296};try{return M()}finally{Math.random=w}}ct(me,"withSeededRandom");var re=ct(x=>`<g><rect width="80" height="80" style="fill: #087ebf; stroke-width: 0px;"/>${x}</g>`,"wrapIcon"),ne={prefix:"mermaid-architecture",height:80,width:80,icons:{database:{body:re('<path id="b" data-name="4" d="m20,57.86c0,3.94,8.95,7.14,20,7.14s20-3.2,20-7.14" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/><path id="c" data-name="3" d="m20,45.95c0,3.94,8.95,7.14,20,7.14s20-3.2,20-7.14" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/><path id="d" data-name="2" d="m20,34.05c0,3.94,8.95,7.14,20,7.14s20-3.2,20-7.14" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/><ellipse id="e" data-name="1" cx="40" cy="22.14" rx="20" ry="7.14" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/><line x1="20" y1="57.86" x2="20" y2="22.14" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/><line x1="60" y1="57.86" x2="60" y2="22.14" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/>')},server:{body:re('<rect x="17.5" y="17.5" width="45" height="45" rx="2" ry="2" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/><line x1="17.5" y1="32.5" x2="62.5" y2="32.5" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/><line x1="17.5" y1="47.5" x2="62.5" y2="47.5" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/><g><path d="m56.25,25c0,.27-.45.5-1,.5h-10.5c-.55,0-1-.23-1-.5s.45-.5,1-.5h10.5c.55,0,1,.23,1,.5Z" style="fill: #fff; stroke-width: 0px;"/><path d="m56.25,25c0,.27-.45.5-1,.5h-10.5c-.55,0-1-.23-1-.5s.45-.5,1-.5h10.5c.55,0,1,.23,1,.5Z" style="fill: none; stroke: #fff; stroke-miterlimit: 10;"/></g><g><path d="m56.25,40c0,.27-.45.5-1,.5h-10.5c-.55,0-1-.23-1-.5s.45-.5,1-.5h10.5c.55,0,1,.23,1,.5Z" style="fill: #fff; stroke-width: 0px;"/><path d="m56.25,40c0,.27-.45.5-1,.5h-10.5c-.55,0-1-.23-1-.5s.45-.5,1-.5h10.5c.55,0,1,.23,1,.5Z" style="fill: none; stroke: #fff; stroke-miterlimit: 10;"/></g><g><path d="m56.25,55c0,.27-.45.5-1,.5h-10.5c-.55,0-1-.23-1-.5s.45-.5,1-.5h10.5c.55,0,1,.23,1,.5Z" style="fill: #fff; stroke-width: 0px;"/><path d="m56.25,55c0,.27-.45.5-1,.5h-10.5c-.55,0-1-.23-1-.5s.45-.5,1-.5h10.5c.55,0,1,.23,1,.5Z" style="fill: none; stroke: #fff; stroke-miterlimit: 10;"/></g><g><circle cx="32.5" cy="25" r=".75" style="fill: #fff; stroke: #fff; stroke-miterlimit: 10;"/><circle cx="27.5" cy="25" r=".75" style="fill: #fff; stroke: #fff; stroke-miterlimit: 10;"/><circle cx="22.5" cy="25" r=".75" style="fill: #fff; stroke: #fff; stroke-miterlimit: 10;"/></g><g><circle cx="32.5" cy="40" r=".75" style="fill: #fff; stroke: #fff; stroke-miterlimit: 10;"/><circle cx="27.5" cy="40" r=".75" style="fill: #fff; stroke: #fff; stroke-miterlimit: 10;"/><circle cx="22.5" cy="40" r=".75" style="fill: #fff; stroke: #fff; stroke-miterlimit: 10;"/></g><g><circle cx="32.5" cy="55" r=".75" style="fill: #fff; stroke: #fff; stroke-miterlimit: 10;"/><circle cx="27.5" cy="55" r=".75" style="fill: #fff; stroke: #fff; stroke-miterlimit: 10;"/><circle cx="22.5" cy="55" r=".75" style="fill: #fff; stroke: #fff; stroke-miterlimit: 10;"/></g>')},disk:{body:re('<rect x="20" y="15" width="40" height="50" rx="1" ry="1" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/><ellipse cx="24" cy="19.17" rx=".8" ry=".83" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/><ellipse cx="56" cy="19.17" rx=".8" ry=".83" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/><ellipse cx="24" cy="60.83" rx=".8" ry=".83" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/><ellipse cx="56" cy="60.83" rx=".8" ry=".83" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/><ellipse cx="40" cy="33.75" rx="14" ry="14.58" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/><ellipse cx="40" cy="33.75" rx="4" ry="4.17" style="fill: #fff; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/><path d="m37.51,42.52l-4.83,13.22c-.26.71-1.1,1.02-1.76.64l-4.18-2.42c-.66-.38-.81-1.26-.33-1.84l9.01-10.8c.88-1.05,2.56-.08,2.09,1.2Z" style="fill: #fff; stroke-width: 0px;"/>')},internet:{body:re('<circle cx="40" cy="40" r="22.5" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/><line x1="40" y1="17.5" x2="40" y2="62.5" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/><line x1="17.5" y1="40" x2="62.5" y2="40" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/><path d="m39.99,17.51c-15.28,11.1-15.28,33.88,0,44.98" style="fill: none; sLine truncated
translate(${L}, ${F-E.height/2})
translate(${p*y.width/2}, ${m*y.height/2})
rotate(${-1*p*m*45}, 0, ${E.height/2})
`)}}}}}))},"drawEdges"),Pr=ct(async function(x,M,w,R){const v=w.getConfigField("padding")*.75,l=w.getConfigField("fontSize"),r=w.getConfigField("iconSize")/2;await Promise.all(M.nodes().map(async a=>{const f=ie(a);if(f.type==="group"){const{h:e,w:d,x1:t,y1:s}=a.boundingBox(),o=x.append("rect");o.attr("id",`${R}-group-${f.id}`).attr("x",t+r).attr("y",s+r).attr("width",d).attr("height",e).attr("class","node-bkg");const c=x.append("g");let h=t,N=s;if(f.icon){const g=c.append("g");g.html(`<g>${await pe(f.icon,{height:v,width:v,fallbackPrefix:ne.prefix})}</g>`),g.attr("transform","translate("+(h+r+1)+", "+(N+r+1)+")"),h+=v,N+=l/2-1-2}if(f.label){const g=c.append("g");await Te(g,f.label,{useHtmlLabels:!1,width:d,classes:"architecture-service-label"},Ee()),g.attr("dy","1em").attr("alignment-baseline","middle").attr("dominant-baseline","start").attr("text-anchor","start"),g.attr("transform","translate("+(h+r+4)+", "+(N+r+2)+")")}w.setElementForId(f.id,o)}}))},"drawGroups"),Gr=ct(async function(x,M,w,R){const T=Ee();for(const v of w){const l=M.append("g"),i=x.getConfigField("iconSize");if(v.title){const e=l.append("g");await Te(e,v.title,{useHtmlLabels:!1,width:i*1.5,classes:"architecture-service-label"},T),e.attr("dy","1em").attr("alignment-baseline","middle").attr("dominant-baseline","middle").attr("text-anchor","middle"),e.attr("transform","translate("+i/2+", "+i+")")}const r=l.append("g");if(v.icon)r.html(`<g>${await pe(v.icon,{height:i,width:i,fallbackPrefix:ne.prefix})}</g>`);else if(v.iconText){r.html(`<g>${await pe("blank",{height:i,width:i,fallbackPrefix:ne.prefix})}</g>`);const t=r.append("g").append("foreignObject").attr("width",i).attr("height",i).append("div").attr("class","node-icon-text").attr("style",`height: ${i}px;`).append("div").html(sr(v.iconText,T)),s=parseInt(window.getComputedStyle(t.node(),null).getPropertyValue("font-size").replace(/\D/g,""))??16;t.attr("style",`-webkit-line-clamp: ${Math.floor((i-2)/s)};`)}else r.append("path").attr("class","node-bkg").attr("id",`${R}-node-${v.id}`).attr("d",`M0,${i} V5 Q0,0 5,0 H${i-5} Q${i},0 ${i},5 V${i} Z`);l.attr("id",`${R}-service-${v.id}`).attr("class","architecture-service");const{width:a,height:f}=l.node().getBBox();v.width=a,v.height=f,x.setElementForId(v.id,l)}return 0},"drawServices"),Ur=ct(function(x,M,w,R){w.forEach(T=>{const v=M.append("g"),l=x.getConfigField("iconSize");v.append("g").append("rect").attr("id",`${R}-node-${T.id}`).attr("fill-opacity","0").attr("width",l).attr("height",l),v.attr("class","architecture-junction");const{width:r,height:a}=v._groups[0][0].getBBox();v.width=r,v.height=a,x.setElementForId(T.id,v)})},"drawJunctions");fr([{name:ne.prefix,icons:ne}]);be.use(Nr);function Xe(x,M,w){x.forEach(R=>{M.add({group:"nodes",data:{type:"service",id:R.id,icon:R.icon,label:R.title,parent:R.in,width:w.getConfigField("iconSize"),height:w.getConfigField("iconSize")},classes:"node-service"})})}ct(Xe,"addServices");function He(x,M,w){x.forEach(R=>{M.add({group:"nodes",data:{type:"junction",id:R.id,parent:R.in,width:w.getConfigField("iconSize"),height:w.getConfigField("iconSize")},classes:"node-junction"})})}ct(He,"addJunctions");function We(x,M){M.nodes().map(w=>{const R=ie(w);if(R.type==="group")return;R.x=w.position().x,R.y=w.position().y,x.getElementById(R.id).attr("transform","translate("+(R.x||0)+","+(R.y||0)+")")})}ct(We,"positionNodes");function Ve(x,M){x.forEach(w=>{M.add({group:"nodes",data:{type:"group",id:w.id,icon:w.icon,label:w.title,parent:w.in},classes:"node-group"})})}ct(Ve,"addGroups");function $e(x,M){x.forEach(w=>{const{lhsId:R,rhsId:T,lhsInto:v,lhsGroup:l,rhsInto:i,lhsDir:r,rhsDir:a,rhsGroup:f,title:e}=w,d=Ne(w.lhsDir,w.rhsDir)?"segments":"straight",t={id:`${R}-${T}`,label:e,source:R,sourceDir:r,sourceArrow:v,sourceGroup:l,sourceEndpoint:r==="L"?"0 50%":r==="R"?"100% 50%":r==="T"?"50% 0":"50% 100%",target:T,targetDir:a,targetArrow:i,targetGroup:f,targetEndpoint:a==="L"?"0 50%":a==="R"?"100% 50%":a==="T"?"50% 0":"50% 100%"};M.add({group:"edges",data:t,classes:d})})}ct($e,"addEdges");function ze(x,M,w,R=[]){const T=ct((d,t)=>{const s=new Map;for(const[o,c]of d.entries()){const h=`${o}`;let N=0;const g=[...c.entries()];if(g.length===1){s.set(h,g[0][1]);continue}for(let u=0;u<g.length-1;u++)for(let L=u+1;L<g.length;L++){const[F,C]=g[u],[G,V]=g[L];if(w.get(Pe(F,G))===t)s.set(h,[...s.get(h)??[],...C,...V]);else if(F==="default"||G==="default")s.set(h,[...s.get(h)??[],...C,...V]);else{const $=`${h}-${N++}`;s.set($,C);const A=`${h}-${N++}`;s.set(A,V)}}}return s},"flattenAlignments"),v=M.map(d=>{const t=new Map,s=new Map;return d.forEach(([o,c],h)=>{const N=x.getNode(h)?.in??"default",g=t.get(c)??new Map;t.has(c)||t.set(c,g);const u=s.get(o)??new Map;s.has(o)||s.set(o,u);for(const L of[g,u]){const F=L.get(N)??[];L.has(N)||L.set(N,F),F.push(h)}}),{horiz:[...T(t,"horizontal").values()].filter(o=>o.length>1),vert:[...T(s,"vertical").values()].filter(o=>o.length>1)}}),[l,i]=v.reduce(([d,t],{horiz:s,vert:o})=>[[...d,Line truncated
@@ -0,0 +1,129 @@
import{g as Pe}from"./chunk-5VM5RSS4.js";import{aD as Fe,aE as xe,aF as Me,aG as Ke,aH as Ye,aI as We,aJ as Ve,aK as Ue,aL as He,aM as Xe,aN as je,aO as Ge,aP as qe,aQ as Je,aR as Ze,aS as Qe,aT as $e,aU as et,aV as tt,aW as st,aX as rt,aY as it,aZ as at,a_ as nt,a$ as ot,_ as u,y as Y,j as X,b0 as ct,d as lt,l as m,p as ut,r as gt,c as Le,Q as ht,az as dt,ay as pt,aA as ft,k as bt,b1 as St,ag as ye,ah as xt}from"./mermaid.core.js";import{c as Lt}from"./channel.js";import"./index.js";function yt(e){return Array.isArray(e)}function mt(e){if(Fe(e))return e;const a=xe(e);if(!wt(e))return{};if(yt(e)){const r=Array.from(e);return e.length>0&&typeof e[0]=="string"&&Object.hasOwn(e,"index")&&(r.index=e.index,r.input=e.input),r}if(Me(e)){const r=e,l=r.constructor;return new l(r.buffer,r.byteOffset,r.length)}if(a==="[object ArrayBuffer]")return new ArrayBuffer(e.byteLength);if(a==="[object DataView]"){const r=e,l=r.buffer,d=r.byteOffset,s=r.byteLength,h=new ArrayBuffer(s),x=new Uint8Array(l,d,s);return new Uint8Array(h).set(x),new DataView(h)}if(a==="[object Boolean]"||a==="[object Number]"||a==="[object String]"){const r=e.constructor,l=new r(e.valueOf());return a==="[object String]"?_t(l,e):se(l,e),l}if(a==="[object Date]")return new Date(Number(e));if(a==="[object RegExp]"){const r=e,l=new RegExp(r.source,r.flags);return l.lastIndex=r.lastIndex,l}if(a==="[object Symbol]")return Object(Symbol.prototype.valueOf.call(e));if(a==="[object Map]"){const r=e,l=new Map;return r.forEach((d,s)=>{l.set(s,d)}),l}if(a==="[object Set]"){const r=e,l=new Set;return r.forEach(d=>{l.add(d)}),l}if(a==="[object Arguments]"){const r=e,l={};return se(l,r),l.length=r.length,l[Symbol.iterator]=r[Symbol.iterator],l}const o={};return kt(o,e),se(o,e),Et(o,e),o}function wt(e){switch(xe(e)){case ot:case nt:case at:case it:case rt:case st:case tt:case et:case $e:case Qe:case Ze:case Je:case qe:case Ge:case je:case Xe:case He:case Ue:case Ve:case We:case Ye:case Ke:return!0;default:return!1}}function se(e,a){for(const o in a)Object.hasOwn(a,o)&&(e[o]=a[o])}function Et(e,a){const o=Object.getOwnPropertySymbols(a);for(let r=0;r<o.length;r++){const l=o[r];Object.prototype.propertyIsEnumerable.call(a,l)&&(e[l]=a[l])}}function _t(e,a){const o=a.valueOf().length;for(const r in a)Object.hasOwn(a,r)&&(Number.isNaN(Number(r))||Number(r)>=o)&&(e[r]=a[r])}function kt(e,a){const o=Object.getPrototypeOf(a);o!==null&&typeof a.constructor=="function"&&Object.setPrototypeOf(e,o)}var ie=(function(){var e=u(function(v,g,i,c){for(i=i||{},c=v.length;c--;i[v[c]]=g);return i},"o"),a=[1,15],o=[1,7],r=[1,13],l=[1,14],d=[1,19],s=[1,16],h=[1,17],x=[1,18],y=[8,30],S=[8,10,21,28,29,30,31,39,43,46],f=[1,23],L=[1,24],w=[8,10,15,16,21,28,29,30,31,39,43,46],b=[8,10,15,16,21,27,28,29,30,31,39,43,46],k=[1,49],_={trace:u(function(){},"trace"),yy:{},symbols_:{error:2,spaceLines:3,SPACELINE:4,NL:5,separator:6,SPACE:7,EOF:8,start:9,BLOCK_DIAGRAM_KEY:10,document:11,stop:12,statement:13,link:14,LINK:15,START_LINK:16,LINK_LABEL:17,STR:18,nodeStatement:19,columnsStatement:20,SPACE_BLOCK:21,blockStatement:22,classDefStatement:23,cssClassStatement:24,styleStatement:25,node:26,SIZE:27,COLUMNS:28,"id-block":29,end:30,NODE_ID:31,nodeShapeNLabel:32,dirList:33,DIR:34,NODE_DSTART:35,NODE_DEND:36,BLOCK_ARROW_START:37,BLOCK_ARROW_END:38,classDef:39,CLASSDEF_ID:40,CLASSDEF_STYLEOPTS:41,DEFAULT:42,class:43,CLASSENTITY_IDS:44,STYLECLASS:45,style:46,STYLE_ENTITY_IDS:47,STYLE_DEFINITION_DATA:48,$accept:0,$end:1},terminals_:{2:"error",4:"SPACELINE",5:"NL",7:"SPACE",8:"EOF",10:"BLOCK_DIAGRAM_KEY",15:"LINK",16:"START_LINK",17:"LINK_LABEL",18:"STR",21:"SPACE_BLOCK",27:"SIZE",28:"COLUMNS",29:"id-block",30:"end",31:"NODE_ID",34:"DIR",35:"NODE_DSTART",36:"NODE_DEND",37:"BLOCK_ARROW_START",38:"BLOCK_ARROW_END",39:"classDef",40:"CLASSDEF_ID",41:"CLASSDEF_STYLEOPTS",42:"DEFAULT",43:"class",44:"CLASSENTITY_IDS",45:"STYLECLASS",46:"style",47:"STYLE_ENTITY_IDS",48:"STYLE_DEFINITION_DATA"},productions_:[0,[3,1],[3,2],[3,2],[6,1],[6,1],[6,1],[9,3],[12,1],[12,1],[12,2],[12,2],[11,1],[11,2],[14,1],[14,4],[13,1],[13,1],[13,1],[13,1],[13,1],[13,1],[13,1],[19,3],[19,2],[19,1],[20,1],[22,4],[22,3],[26,1],[26,2],[33,1],[33,2],[32,3],[32,4],[23,3],[23,3],[24,3],[25,3]],performAction:u(function(g,i,c,p,E,t,M){var n=t.length-1;switch(E){case 4:p.getLogger().debug("Rule: separator (NL) ");break;case 5:p.getLogger().debug("Rule: separator (Space) ");break;case 6:p.getLogger().debug("Rule: separator (EOF) ");break;case 7:p.getLogger().debug("Rule: hierarchy: ",t[n-1]),p.setHierarchy(t[n-1]);break;case 8:p.getLogger().debug("Stop NL ");break;case 9:p.getLogger().debug("Stop EOF ");break;case 10:p.getLogger().debug("Stop NL2 ");break;case 11:p.getLogger().debug("Stop EOF2 ");break;case 12:p.getLogger().debug("Rule: statement: ",t[n]),typeof t[n].length=="number"?this.$=t[n]:this.$=[t[n]];break;case 13:p.getLogger().debug("Rule: statement #2: ",t[n-1]),this.$=[t[n-1]].concat(t[n]);break;case 14:p.getLogger().debug("Rule: link: ",Line truncated
`+D.showPosition()+`
Expecting `+H.join(", ")+", got '"+(this.terminals_[N]||N)+"'":te="Parse error on line "+(P+1)+": Unexpected "+(N==V?"end of input":"'"+(this.terminals_[N]||N)+"'"),this.parseError(te,{text:D.match,token:this.terminals_[N]||N,line:D.yylineno,loc:$,expected:H})}if(C[0]instanceof Array&&C.length>1)throw new Error("Parse Error: multiple actions possible at state: "+R+", token: "+N);switch(C[0]){case 1:c.push(N),E.push(D.yytext),t.push(D.yylloc),c.push(C[1]),N=null,W=D.yyleng,n=D.yytext,P=D.yylineno,$=D.yylloc;break;case 2:if(B=this.productions_[C[1]][1],F.$=E[E.length-B],F._$={first_line:t[t.length-(B||1)].first_line,last_line:t[t.length-1].last_line,first_column:t[t.length-(B||1)].first_column,last_column:t[t.length-1].last_column},Be&&(F._$.range=[t[t.length-(B||1)].range[0],t[t.length-1].range[1]]),ee=this.performAction.apply(F,[n,W,P,A.yy,C[1],E,t].concat(Z)),typeof ee<"u")return ee;B&&(c=c.slice(0,-1*B*2),E=E.slice(0,-1*B),t=t.slice(0,-1*B)),c.push(this.productions_[C[1]][0]),E.push(F.$),t.push(F._$),pe=M[c[c.length-2]][c[c.length-1]],c.push(pe);break;case 3:return!0}}return!0},"parse")},T=(function(){var v={EOF:1,parseError:u(function(i,c){if(this.yy.parser)this.yy.parser.parseError(i,c);else throw new Error(i)},"parseError"),setInput:u(function(g,i){return this.yy=i||this.yy||{},this._input=g,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:u(function(){var g=this._input[0];this.yytext+=g,this.yyleng++,this.offset++,this.match+=g,this.matched+=g;var i=g.match(/(?:\r\n?|\n).*/g);return i?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),g},"input"),unput:u(function(g){var i=g.length,c=g.split(/(?:\r\n?|\n)/g);this._input=g+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-i),this.offset-=i;var p=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),c.length-1&&(this.yylineno-=c.length-1);var E=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:c?(c.length===p.length?this.yylloc.first_column:0)+p[p.length-c.length].length-c[0].length:this.yylloc.first_column-i},this.options.ranges&&(this.yylloc.range=[E[0],E[0]+this.yyleng-i]),this.yyleng=this.yytext.length,this},"unput"),more:u(function(){return this._more=!0,this},"more"),reject:u(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).
`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:u(function(g){this.unput(this.match.slice(g))},"less"),pastInput:u(function(){var g=this.matched.substr(0,this.matched.length-this.match.length);return(g.length>20?"...":"")+g.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:u(function(){var g=this.match;return g.length<20&&(g+=this._input.substr(0,20-g.length)),(g.substr(0,20)+(g.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:u(function(){var g=this.pastInput(),i=new Array(g.length+1).join("-");return g+this.upcomingInput()+`
`+i+"^"},"showPosition"),test_match:u(function(g,i){var c,p,E;if(this.options.backtrack_lexer&&(E={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(E.yylloc.range=this.yylloc.range.slice(0))),p=g[0].match(/(?:\r\n?|\n).*/g),p&&(this.yylineno+=p.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:p?p[p.length-1].length-p[p.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+g[0].length},this.yytext+=g[0],this.match+=g[0],this.matches=g,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(g[0].length),this.matched+=g[0],c=this.performAction.call(this,this.yy,this,i,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),c)return c;if(this._backtrack){for(var t in E)this[t]=E[t];return!1}return!1},"test_match"),next:u(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var g,i,c,p;this._more||(this.yytext="",this.match="");for(var E=this._currentRules(),t=0;t<E.length;t++)if(c=this._input.match(this.rules[E[t]]),c&&(!i||c[0].length>i[0].length)){if(i=c,p=t,this.options.backtrack_lexer){if(g=this.test_match(c,E[t]),g!==!1)return g;if(this._backtrack){i=!1;continue}else return!1}else if(!this.options.flex)break}return i?(g=this.test_match(i,E[p]),g!==!1?g:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text.
`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:u(function(){var i=this.next();return i||this.lex()},"lex"),begin:u(function(i){this.conditionStack.push(i)},"begin"),popState:u(function(){var i=this.conditionStack.length-1;return i>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:u(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:u(function(i){return i=this.conditionStack.length-1-Math.abs(i||0),i>=0?this.conditionStack[i]:"INITIAL"},"topState"),pushState:u(function(i){this.begin(i)},"pushState"),stateStackSize:u(function(){return this.conditionStack.length},"stateStackSize"),options:{},performAction:u(function(i,c,p,E){switch(p){case 0:return i.getLogger().debug("Found block-beta"),10;case 1:return i.getLogger().debug("Found id-block"),29;case 2:return i.getLogger().debug("Found block"),10;case 3:i.getLogger().debug(".",c.yytext);break;case 4:i.getLogger().debug("_",c.yytext);break;case 5:return 5;case 6:return c.yytext=-1,28;case 7:return c.yytext=c.yytext.replace(/columns\s+/,""),i.getLogger().debug("COLUMNS (LEX)",c.yytext),28;case 8:this.pushState("md_string");break;case 9:return"MD_STR";case 10:this.popState();break;case 11:this.pushState("string");break;case 12:i.getLogger().debug("LEX: POPPING STR:",c.yytext),this.popState();break;case 13:return i.getLogger().debug("LEX: STR end:",c.yytext),"STR";case 14:return c.yytext=c.yytext.replace(/space\:/,""),i.getLogger().debug("SPACE NUM (LEX)",c.yytext),21;case 15:return c.yytext="1",i.getLogger().debug("COLUMNS (LEX)",c.yytext),21;case 16:return 42;case 17:return"LINKSTYLE";case 18:return"INTERPOLATE";case 19:return this.pushState("CLASSDEF"),39;case 20:return this.popState(),this.pushState("CLASSDEFID"),"DEFAULT_CLASSDEF_ID";case 21:return this.popState(),this.pushState("CLASSDEFID"),40;case 22:return this.popState(),41;case 23:return this.pushState("CLASS"),43;case 24:return this.popState(),this.pushState("CLASS_STYLE"),44;case 25:return this.popState(),45;case 26:return this.pushState("STYLE_STMNT"),46;case 27:return this.popState(),this.pushState("STYLE_DEFINITION"),47;case 28:return this.popState(),48;case 29:return this.pushState("acc_title"),"acc_title";case 30:return this.popState(),"acc_title_value";case 31:return this.pushState("acc_descr"),"acc_descr";case 32:return this.popState(),"acc_descr_value";case 33:this.pushState("acc_descr_multiline");break;case 34:this.popState();break;case 35:return"acc_descr_multiline_value";case 36:return 30;case 37:return this.popState(),i.getLogger().debug("Lex: (("),"NODE_DEND";case 38:return this.popState(),i.getLogger().debug("Lex: (("),"NODE_DEND";case 39:return this.popState(),i.getLogger().debug("Lex: ))"),"NODE_DEND";case 40:return this.popState(),i.getLogger().debug("Lex: (("),"NODE_DEND";case 41:return this.popState(),i.getLogger().debug("Lex: (("),"NODE_DEND";case 42:return this.popState(),i.getLogger().debug("Lex: (-"),"NODE_DEND";case 43:return this.popState(),i.getLogger().debug("Lex: -)"),"NODE_DEND";case 44:return this.popState(),i.getLogger().debug("Lex: (("),"NODE_DEND";case 45:return this.popState(),i.getLogger().debug("Lex: ]]"),"NODE_DEND";case 46:return this.popState(),i.getLogger().debug("Lex: ("),"NODE_DEND";case 47:return this.popState(),i.getLogger().debug("Lex: ])"),"NODE_DEND";case 48:return this.popState(),i.getLogger().debug("Lex: /]"),"NODE_DEND";case 49:return this.popState(),i.getLogger().debug("Lex: /]"),"NODE_DEND";case 50:return this.popState(),i.getLogger().debug("Lex: )]"),"NODE_DEND";case 51:return this.popState(),i.getLogger().debug("Lex: )"),"NODE_DEND";case 52:return this.popState(),i.getLogger().debug("Lex: ]>"),"NODE_DEND";case 53:return this.popState(),i.getLogger().debug("Lex: ]"),"NODE_DEND";case 54:return i.getLogger().debug("Lexa: -)"),this.pushState("NODE"),35;case 55:return i.getLogger().debug("Lexa: (-"),this.pushState("NODE"),35;case 56:return i.getLogger().debug("Lexa: ))"),this.pushState("NODE"),35;case 57:return i.getLogger().debug("Lexa: )"),this.pushState("NODE"),35;case 58:return i.getLogger().debug("Lex: ((("),this.pushState("NODE"),35;case 59:return i.getLogger().debug("Lexa: )"),this.pushState("NODE"),35;case 60:return i.getLogger().debug("Lexa: )"),this.pushState("NODE"),35;case 61:return i.getLogger().debug("Lexa: )"),this.pushState("NODE"),35;case 62:return i.getLogger().debug("Lexc: >"),this.pushState("NODE"),35;case 63:return i.getLogger().debug("Lexa: (["),this.pushState("NODE"),35;case 64:return i.getLogger().debug("Lexa: )"),this.pushState("NODE"),35;case 65:return this.pushState("NODE"),35;case 66:return this.pushState("NODE"),35;case 67:return this.pushState("NODE"),35;case 68:return this.pushState("NODE"),35;case 69:return this.pushState("NODE"),35;case 70:return this.pushState("NODE"),35;case 71:return this.p Line truncated
font-family: ${e.fontFamily};
color: ${e.nodeTextColor||e.textColor};
}
.cluster-label text {
fill: ${e.titleColor};
}
.cluster-label span {
color: ${e.titleColor};
}
.label text,span {
fill: ${e.nodeTextColor||e.textColor};
color: ${e.nodeTextColor||e.textColor};
}
.node rect,
.node circle,
.node ellipse,
.node polygon,
.node path {
fill: ${e.mainBkg};
stroke: ${e.nodeBorder};
stroke-width: 1px;
}
.flowchart-label text {
text-anchor: middle;
}
// .flowchart-label .text-outer-tspan {
// text-anchor: middle;
// }
// .flowchart-label .text-inner-tspan {
// text-anchor: start;
// }
.node .label {
text-align: center;
}
.node.clickable {
cursor: pointer;
}
.arrowheadPath {
fill: ${e.arrowheadColor};
}
.edgePaths .path {
stroke: ${e.lineColor};
stroke-width: 2.0px;
}
.flowchart-link {
stroke: ${e.lineColor};
fill: none;
}
.edgeLabel {
background-color: ${e.edgeLabelBackground};
/*
* This is for backward compatibility with existing code that didn't
* add a \`<p>\` around edge labels.
*
* TODO: We should probably remove this in a future release.
*/
p {
margin: 0;
padding: 0;
display: inline;
}
rect {
opacity: 0.5;
background-color: ${e.edgeLabelBackground};
fill: ${e.edgeLabelBackground};
}
text-align: center;
}
/* For html labels only */
.labelBkg {
background-color: ${e.edgeLabelBackground};
}
.node .cluster {
// fill: ${re(e.mainBkg,.5)};
fill: ${re(e.clusterBkg,.5)};
stroke: ${re(e.clusterBorder,.2)};
box-shadow: rgba(50, 50, 93, 0.25) 0px 13px 27px -5px, rgba(0, 0, 0, 0.3) 0px 8px 16px -8px;
stroke-width: 1px;
}
.cluster text {
fill: ${e.titleColor};
}
.cluster span {
color: ${e.titleColor};
}
/* .cluster div {
color: ${e.titleColor};
} */
div.mermaidTooltip {
position: absolute;
text-align: center;
max-width: 200px;
padding: 2px;
font-family: ${e.fontFamily};
font-size: 12px;
background: ${e.tertiaryColor};
border: 1px solid ${e.border2};
border-radius: 2px;
pointer-events: none;
z-index: 100;
}
.flowchartTitleText {
text-anchor: middle;
font-size: 18px;
fill: ${e.textColor};
}
${Pe()}
`,"getStyles"),Gt=jt;function ne(e,a){if(e===0||!Number.isInteger(e))throw new Error("Columns must be an integer !== 0.");if(a<0||!Number.isInteger(a))throw new Error("Position must be a non-negative integer."+a);if(e<0)return{px:a,py:0};if(e===1)return{px:0,py:a};const o=a%e,r=Math.floor(a/e);return{px:o,py:r}}u(ne,"calculateBlockPosition");var qt=u(e=>{let a=0,o=0;for(const r of e.children){const{width:l,height:d,x:s,y:h}=r.size??{width:0,height:0,x:0,y:0};if(m.debug("getMaxChildSize abc95 child:",r.id,"width:",l,"height:",d,"x:",s,"y:",h,r.type),r.type==="space")continue;const x=l/(r.widthInColumns??1);x>a&&(a=x),d>o&&(o=d)}return{width:a,height:o}},"getMaxChildSize");function G(e,a,o=0,r=0,l=8){m.debug("setBlockSizes abc95 (start)",e.id,e?.size?.x,"block width =",e?.size,"siblingWidth",o),e?.size?.width||(e.size={width:o,height:r,x:0,y:0});let d=0,s=0;if(e.children?.length>0){for(const b of e.children)G(b,a,0,0,l);const h=qt(e);d=h.width,s=h.height,m.debug("setBlockSizes abc95 maxWidth of",e.id,":s children is ",d,s);for(const b of e.children)b.size&&(m.debug(`abc95 Setting size of children of ${e.id} id=${b.id} ${d} ${s} ${JSON.stringify(b.size)}`),b.size.width=d*(b.widthInColumns??1)+l*((b.widthInColumns??1)-1),b.size.height=s,b.size.x=0,b.size.y=0,m.debug(`abc95 updating size of ${e.id} children child:${b.id} maxWidth:${d} maxHeight:${s}`));for(const b of e.children)G(b,a,d,s,l);const x=e.columns??-1;let y=0;for(const b of e.children)y+=b.widthInColumns??1;let S=e.children.length;x>0&&x<y&&(S=x);const f=Math.ceil(y/S);let L=S*(d+l)+l,w=f*(s+l)+l;if(L<o){m.debug(`Detected to small sibling: abc95 ${e.id} siblingWidth ${o} siblingHeight ${r} width ${L}`),L=o,w=r;const b=(o-S*l-l)/S,k=(r-f*l-l)/f;m.debug("Size indata abc88",e.id,"childWidth",b,"maxWidth",d),m.debug("Size indata abc88",e.id,"childHeight",k,"maxHeight",s),m.debug("Size indata abc88 xSize",S,"padding",l);for(const _ of e.children)_.size&&(_.size.width=b,_.size.height=k,_.size.x=0,_.size.y=0)}if(m.debug(`abc95 (finale calc) ${e.id} xSize ${S} ySize ${f} columns ${x}${e.children.length} width=${Math.max(L,e.size?.width||0)}`),L<(e?.size?.width||0)){L=e?.size?.width||0;const b=x>0?Math.min(e.children.length,x):e.children.length;if(b>0){const k=(L-b*l-l)/b;m.debug("abc95 (growing to fit) width",e.id,L,e.size?.width,k);for(const _ of e.children)_.size&&(_.size.width=k)}}e.size={width:L,height:w,x:0,y:0}}m.debug("setBlockSizes abc94 (done)",e.id,e?.size?.x,e?.size?.width,e?.size?.y,e?.size?.height)}u(G,"setBlockSizes");function ue(e,a,o=8){m.debug(`abc85 layout blocks (=>layoutBlocks) ${e.id} x: ${e?.size?.x} y: ${e?.size?.y} width: ${e?.size?.width}`);const r=e.columns??-1;if(m.debug("layoutBlocks columns abc95",e.id,"=>",r,e),e.children&&e.children.length>0){const l=e?.children[0]?.size?.width??0,d=e.children.length*l+(e.children.length-1)*o;m.debug("widthOfChildren 88",d,"posX");const s=new Map;{let f=0;for(const L of e.children){if(!L.size)continue;const{py:w}=ne(r,f),b=s.get(w)??0;L.size.height>b&&s.set(w,L.size.height);let k=L?.widthInColumns??1;r>0&&(k=Math.min(k,r-f%r)),f+=k}}const h=new Map;{let f=0;const L=[...s.keys()].sort((w,b)=>w-b);for(const w of L)h.set(w,f),f+=(s.get(w)??0)+o}let x=0;m.debug("abc91 block?.size?.x",e.id,e?.size?.x);let y=e?.size?.x?e?.size?.x+(-e?.size?.width/2||0):-o,S=0;for(const f of e.children){const L=e;if(!f.size)continue;const{width:w,height:b}=f.size,{px:k,py:_}=ne(r,x);if(_!=S&&(S=_,y=e?.size?.x?e?.size?.x+(-e?.size?.width/2||0):-o,m.debug("New row in layout for block",e.id," and child ",f.id,S)),m.debug(`abc89 layout blocks (child) id: ${f.id} Pos: ${x} (px, py) ${k},${_} (${L?.size?.x},${L?.size?.y}) parent: ${L.id} width: ${w}${o}`),L.size){const O=w/2;f.size.x=y+o+O,m.debug(`abc91 layout blocks (calc) px, pyid:${f.id} startingPos=X${y} new startingPosX${f.size.x} ${O} padding=${o} width=${w} halfWidth=${O} => x:${f.size.x} y:${f.size.y} ${f.widthInColumns} (width * (child?.w || 1)) / 2 ${w*(f?.widthInColumns??1)/2}`),y=f.size.x+O;const v=h.get(_)??0,g=s.get(_)??b;f.size.y=L.size.y-L.size.height/2+v+g/2+o,m.debug(`abc88 layout blocks (calc) px, pyid:${f.id}startingPosX${y}${o}${O}=>x:${f.size.x}y:${f.size.y}${f.widthInColumns}(width * (child?.w || 1)) / 2${w*(f?.widthInColumns??1)/2}`)}f.children&&ue(f,a,o);let T=f?.widthInColumns??1;r>0&&(T=Math.min(T,r-x%r)),x+=T,m.debug("abc88 columnsPos",f,x)}}m.debug(`layout blocks (<==layoutBlocks) ${e.id} x: ${e?.size?.x} y: ${e?.size?.y} width: ${e?.size?.width}`)}u(ue,"layoutBlocks");function ge(e,{minX:a,minY:o,maxX:r,maxY:l}={minX:0,minY:0,maxX:0,maxY:0}){if(e.size&&e.id!=="root"){const{x:d,y:s,width:h,height:x}=e.size;d-h/2<a&&(a=d-h/2),s-x/2<o&&(o=s-x/2),d+h/2>r&&(r=d+h/2),s+x/2>l&&(l=s+x/2)}if(e.children)for(const d of e.children)({minX:a,minY:o,maxX:r,maxY:l}=ge(d,{minX:a,minY:o,maxX:r,maxY:l}));return{minX:a,minY:o,maxX:r,maxY:l}}u(ge,"findBounds");function Ie(e){const a=e.getBlock("root");if(!a)return;const o=Le()?.block?.padding??8;G(a,e,0,0,o),ue(a, Line truncated
@@ -0,0 +1,38 @@
import{d as Le}from"./chunk-F27PBJKO.js";import{s as Ie,g as Me,a as Be,b as Ve,_ as d,c as wt,l as he,d as Ye,e as Fe,f as Dt,h as Ue,i as be,j as Jt,w as je,k as $t,m as de}from"./mermaid.core.js";import"./index.js";var Ut=(function(){var t=d(function(yt,m,x,E){for(x=x||{},E=yt.length;E--;x[yt[E]]=m);return x},"o"),e=[1,24],s=[1,25],a=[1,26],u=[1,27],i=[1,28],n=[1,63],l=[1,64],r=[1,65],h=[1,66],y=[1,67],f=[1,68],b=[1,69],v=[1,29],w=[1,30],N=[1,31],O=[1,32],P=[1,33],I=[1,34],W=[1,35],X=[1,36],q=[1,37],H=[1,38],Q=[1,39],K=[1,40],G=[1,41],J=[1,42],Z=[1,43],$=[1,44],tt=[1,45],et=[1,46],nt=[1,47],st=[1,48],rt=[1,50],at=[1,51],it=[1,52],ot=[1,53],lt=[1,54],ct=[1,55],ut=[1,56],ht=[1,57],dt=[1,58],pt=[1,59],ft=[1,60],kt=[14,42],Wt=[14,34,36,37,38,39,40,41,42,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74],Ot=[12,14,34,36,37,38,39,40,41,42,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74],S=[1,82],k=[1,83],T=[1,84],C=[1,85],R=[12,14,42],ie=[12,14,33,42],Mt=[12,14,33,42,76,77,79,80],gt=[12,33],Xt=[34,36,37,38,39,40,41,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74],qt={trace:d(function(){},"trace"),yy:{},symbols_:{error:2,start:3,mermaidDoc:4,direction:5,direction_tb:6,direction_bt:7,direction_rl:8,direction_lr:9,graphConfig:10,C4_CONTEXT:11,NEWLINE:12,statements:13,EOF:14,C4_CONTAINER:15,C4_COMPONENT:16,C4_DYNAMIC:17,C4_DEPLOYMENT:18,otherStatements:19,diagramStatements:20,otherStatement:21,title:22,accDescription:23,acc_title:24,acc_title_value:25,acc_descr:26,acc_descr_value:27,acc_descr_multiline_value:28,boundaryStatement:29,boundaryStartStatement:30,boundaryStopStatement:31,boundaryStart:32,LBRACE:33,ENTERPRISE_BOUNDARY:34,attributes:35,SYSTEM_BOUNDARY:36,BOUNDARY:37,CONTAINER_BOUNDARY:38,NODE:39,NODE_L:40,NODE_R:41,RBRACE:42,diagramStatement:43,PERSON:44,PERSON_EXT:45,SYSTEM:46,SYSTEM_DB:47,SYSTEM_QUEUE:48,SYSTEM_EXT:49,SYSTEM_EXT_DB:50,SYSTEM_EXT_QUEUE:51,CONTAINER:52,CONTAINER_DB:53,CONTAINER_QUEUE:54,CONTAINER_EXT:55,CONTAINER_EXT_DB:56,CONTAINER_EXT_QUEUE:57,COMPONENT:58,COMPONENT_DB:59,COMPONENT_QUEUE:60,COMPONENT_EXT:61,COMPONENT_EXT_DB:62,COMPONENT_EXT_QUEUE:63,REL:64,BIREL:65,REL_U:66,REL_D:67,REL_L:68,REL_R:69,REL_B:70,REL_INDEX:71,UPDATE_EL_STYLE:72,UPDATE_REL_STYLE:73,UPDATE_LAYOUT_CONFIG:74,attribute:75,STR:76,STR_KEY:77,STR_VALUE:78,ATTRIBUTE:79,ATTRIBUTE_EMPTY:80,$accept:0,$end:1},terminals_:{2:"error",6:"direction_tb",7:"direction_bt",8:"direction_rl",9:"direction_lr",11:"C4_CONTEXT",12:"NEWLINE",14:"EOF",15:"C4_CONTAINER",16:"C4_COMPONENT",17:"C4_DYNAMIC",18:"C4_DEPLOYMENT",22:"title",23:"accDescription",24:"acc_title",25:"acc_title_value",26:"acc_descr",27:"acc_descr_value",28:"acc_descr_multiline_value",33:"LBRACE",34:"ENTERPRISE_BOUNDARY",36:"SYSTEM_BOUNDARY",37:"BOUNDARY",38:"CONTAINER_BOUNDARY",39:"NODE",40:"NODE_L",41:"NODE_R",42:"RBRACE",44:"PERSON",45:"PERSON_EXT",46:"SYSTEM",47:"SYSTEM_DB",48:"SYSTEM_QUEUE",49:"SYSTEM_EXT",50:"SYSTEM_EXT_DB",51:"SYSTEM_EXT_QUEUE",52:"CONTAINER",53:"CONTAINER_DB",54:"CONTAINER_QUEUE",55:"CONTAINER_EXT",56:"CONTAINER_EXT_DB",57:"CONTAINER_EXT_QUEUE",58:"COMPONENT",59:"COMPONENT_DB",60:"COMPONENT_QUEUE",61:"COMPONENT_EXT",62:"COMPONENT_EXT_DB",63:"COMPONENT_EXT_QUEUE",64:"REL",65:"BIREL",66:"REL_U",67:"REL_D",68:"REL_L",69:"REL_R",70:"REL_B",71:"REL_INDEX",72:"UPDATE_EL_STYLE",73:"UPDATE_REL_STYLE",74:"UPDATE_LAYOUT_CONFIG",76:"STR",77:"STR_KEY",78:"STR_VALUE",79:"ATTRIBUTE",80:"ATTRIBUTE_EMPTY"},productions_:[0,[3,1],[3,1],[5,1],[5,1],[5,1],[5,1],[4,1],[10,4],[10,4],[10,4],[10,4],[10,4],[13,1],[13,1],[13,2],[19,1],[19,2],[19,3],[21,1],[21,1],[21,2],[21,2],[21,1],[29,3],[30,3],[30,3],[30,4],[32,2],[32,2],[32,2],[32,2],[32,2],[32,2],[32,2],[31,1],[20,1],[20,2],[20,3],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,1],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[43,2],[35,1],[35,2],[75,1],[75,2],[75,1],[75,1]],performAction:d(function(m,x,E,_,D,o,Pt){var p=o.length-1;switch(D){case 3:_.setDirection("TB");break;case 4:_.setDirection("BT");break;case 5:_.setDirection("RL");break;case 6:_.setDirection("LR");break;case 8:case 9:case 10:case 11:case 12:_.setC4Type(o[p-3]);break;case 19:_.setTitle(o[p].substring(6)),this.$=o[p].substring(6);break;case 20:_.setAccDescription(o[p].substring(15)),this.$=o[p].substring(15);break;case 21:this.$=o[p].trim(),_.setTitle(this.$);break;case 22:case 23:this.$=o[p].trim(),_.setAccDescription(this.$);break;case 28:o[p].splice(2,0,"ENTERPRISE"),_.addPersonOrSystemBoundary(...o[p]),this.$=o[p];break;case 29:o[p].splice(2,0,"SYSTEM"),_.addPersonOrSystemBoundary(...o[p]),this.$=o[p];break;case 30:_.addPersonOrSystemBoundary(...o[p]),this.$=o[p];break;case 31:o[p].splice(2,0,"CONTAINER"),_.addContainerBoundary(...o[p]),this.$=o[p];break;case 32:_.addDeploymentNode("node",...Line truncated
`+A.showPosition()+`
Expecting `+Yt.join(", ")+", got '"+(this.terminals_[M]||M)+"'":Gt="Parse error on line "+(xt+1)+": Unexpected "+(M==le?"end of input":"'"+(this.terminals_[M]||M)+"'"),this.parseError(Gt,{text:A.match,token:this.terminals_[M]||M,line:A.yylineno,loc:Qt,expected:Yt})}if(V[0]instanceof Array&&V.length>1)throw new Error("Parse Error: multiple actions possible at state: "+Et+", token: "+M);switch(V[0]){case 1:E.push(M),D.push(A.yytext),o.push(A.yylloc),E.push(V[1]),M=null,oe=A.yyleng,p=A.yytext,xt=A.yylineno,Qt=A.yylloc;break;case 2:if(j=this.productions_[V[1]][1],Tt.$=D[D.length-j],Tt._$={first_line:o[o.length-(j||1)].first_line,last_line:o[o.length-1].last_line,first_column:o[o.length-(j||1)].first_column,last_column:o[o.length-1].last_column},Ne&&(Tt._$.range=[o[o.length-(j||1)].range[0],o[o.length-1].range[1]]),Kt=this.performAction.apply(Tt,[p,oe,xt,vt.yy,V[1],D,o].concat(De)),typeof Kt<"u")return Kt;j&&(E=E.slice(0,-1*j*2),D=D.slice(0,-1*j),o=o.slice(0,-1*j)),E.push(this.productions_[V[1]][0]),D.push(Tt.$),o.push(Tt._$),ue=Pt[E[E.length-2]][E[E.length-1]],E.push(ue);break;case 3:return!0}}return!0},"parse")},Oe=(function(){var yt={EOF:1,parseError:d(function(x,E){if(this.yy.parser)this.yy.parser.parseError(x,E);else throw new Error(x)},"parseError"),setInput:d(function(m,x){return this.yy=x||this.yy||{},this._input=m,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:d(function(){var m=this._input[0];this.yytext+=m,this.yyleng++,this.offset++,this.match+=m,this.matched+=m;var x=m.match(/(?:\r\n?|\n).*/g);return x?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),m},"input"),unput:d(function(m){var x=m.length,E=m.split(/(?:\r\n?|\n)/g);this._input=m+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-x),this.offset-=x;var _=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),E.length-1&&(this.yylineno-=E.length-1);var D=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:E?(E.length===_.length?this.yylloc.first_column:0)+_[_.length-E.length].length-E[0].length:this.yylloc.first_column-x},this.options.ranges&&(this.yylloc.range=[D[0],D[0]+this.yyleng-x]),this.yyleng=this.yytext.length,this},"unput"),more:d(function(){return this._more=!0,this},"more"),reject:d(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).
`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:d(function(m){this.unput(this.match.slice(m))},"less"),pastInput:d(function(){var m=this.matched.substr(0,this.matched.length-this.match.length);return(m.length>20?"...":"")+m.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:d(function(){var m=this.match;return m.length<20&&(m+=this._input.substr(0,20-m.length)),(m.substr(0,20)+(m.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:d(function(){var m=this.pastInput(),x=new Array(m.length+1).join("-");return m+this.upcomingInput()+`
`+x+"^"},"showPosition"),test_match:d(function(m,x){var E,_,D;if(this.options.backtrack_lexer&&(D={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(D.yylloc.range=this.yylloc.range.slice(0))),_=m[0].match(/(?:\r\n?|\n).*/g),_&&(this.yylineno+=_.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:_?_[_.length-1].length-_[_.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+m[0].length},this.yytext+=m[0],this.match+=m[0],this.matches=m,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(m[0].length),this.matched+=m[0],E=this.performAction.call(this,this.yy,this,x,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),E)return E;if(this._backtrack){for(var o in D)this[o]=D[o];return!1}return!1},"test_match"),next:d(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var m,x,E,_;this._more||(this.yytext="",this.match="");for(var D=this._currentRules(),o=0;o<D.length;o++)if(E=this._input.match(this.rules[D[o]]),E&&(!x||E[0].length>x[0].length)){if(x=E,_=o,this.options.backtrack_lexer){if(m=this.test_match(E,D[o]),m!==!1)return m;if(this._backtrack){x=!1;continue}else return!1}else if(!this.options.flex)break}return x?(m=this.test_match(x,D[_]),m!==!1?m:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text.
`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:d(function(){var x=this.next();return x||this.lex()},"lex"),begin:d(function(x){this.conditionStack.push(x)},"begin"),popState:d(function(){var x=this.conditionStack.length-1;return x>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:d(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:d(function(x){return x=this.conditionStack.length-1-Math.abs(x||0),x>=0?this.conditionStack[x]:"INITIAL"},"topState"),pushState:d(function(x){this.begin(x)},"pushState"),stateStackSize:d(function(){return this.conditionStack.length},"stateStackSize"),options:{},performAction:d(function(x,E,_,D){switch(_){case 0:return 6;case 1:return 7;case 2:return 8;case 3:return 9;case 4:return 22;case 5:return 23;case 6:return this.begin("acc_title"),24;case 7:return this.popState(),"acc_title_value";case 8:return this.begin("acc_descr"),26;case 9:return this.popState(),"acc_descr_value";case 10:this.begin("acc_descr_multiline");break;case 11:this.popState();break;case 12:return"acc_descr_multiline_value";case 13:break;case 14:c;break;case 15:return 12;case 16:break;case 17:return 11;case 18:return 15;case 19:return 16;case 20:return 17;case 21:return 18;case 22:return this.begin("person_ext"),45;case 23:return this.begin("person"),44;case 24:return this.begin("system_ext_queue"),51;case 25:return this.begin("system_ext_db"),50;case 26:return this.begin("system_ext"),49;case 27:return this.begin("system_queue"),48;case 28:return this.begin("system_db"),47;case 29:return this.begin("system"),46;case 30:return this.begin("boundary"),37;case 31:return this.begin("enterprise_boundary"),34;case 32:return this.begin("system_boundary"),36;case 33:return this.begin("container_ext_queue"),57;case 34:return this.begin("container_ext_db"),56;case 35:return this.begin("container_ext"),55;case 36:return this.begin("container_queue"),54;case 37:return this.begin("container_db"),53;case 38:return this.begin("container"),52;case 39:return this.begin("container_boundary"),38;case 40:return this.begin("component_ext_queue"),63;case 41:return this.begin("component_ext_db"),62;case 42:return this.begin("component_ext"),61;case 43:return this.begin("component_queue"),60;case 44:return this.begin("component_db"),59;case 45:return this.begin("component"),58;case 46:return this.begin("node"),39;case 47:return this.begin("node"),39;case 48:return this.begin("node_l"),40;case 49:return this.begin("node_r"),41;case 50:return this.begin("rel"),64;case 51:return this.begin("birel"),65;case 52:return this.begin("rel_u"),66;case 53:return this.begin("rel_u"),66;case 54:return this.begin("rel_d"),67;case 55:return this.begin("rel_d"),67;case 56:return this.begin("rel_l"),68;case 57:return this.begin("rel_l"),68;case 58:return this.begin("rel_r"),69;case 59:return this.begin("rel_r"),69;case 60:return this.begin("rel_b"),70;case 61:return this.begin("rel_index"),71;case 62:return this.begin("update_el_style"),72;case 63:return this.begin("update_rel_style"),73;case 64:return this.begin("update_layout_config"),74;case 65:return"EOF_IN_STRUCT";case 66:return this.begin("attribute"),"ATTRIBUTE_EMPTY";case 67:this.begin("attribute");break;case 68:this.popState(),this.popState();break;case 69:return 80;case 70:break;case 71:return 80;case 72:this.begin("string");break;case 73:this.popState();break;case 74:return"STR";case 75:this.begin("string_kv");break;case 76:return this.begin("string_kv_key"),"STR_KEY";case 77:this.popState(),this.begin("string_kv_value");break;case 78:return"STR_VALUE";case 79:this.popState(),this.popState();break;case 80:return"STR";case 81:return"LBRACE";case 82:return"RBRACE";case 83:return"SPACE";case 84:return"EOL";case 85:return 14}},"anonymous"),rules:[/^(?:.*direction\s+TB[^\n]*)/,/^(?:.*direction\s+BT[^\n]*)/,/^(?:.*direction\s+RL[^\n]*)/,/^(?:.*direction\s+LR[^\n]*)/,/^(?:title\s[^#\n;]+)/,/^(?:accDescription\s[^#\n;]+)/,/^(?:accTitle\s*:\s*)/,/^(?:(?!\n||)*[^\n]*)/,/^(?:accDescr\s*:\s*)/,/^(?:(?!\n||)*[^\n]*)/,/^(?:accDescr\s*\{\s*)/,/^(?:[\}])/,/^(?:[^\}]*)/,/^(?:%%(?!\{)*[^\n]*(\r?\n?)+)/,/^(?:%%[^\n]*(\r?\n)*)/,/^(?:\s*(\r?\n)+)/,/^(?:\s+)/,/^(?:C4Context\b)/,/^(?:C4Container\b)/,/^(?:C4Component\b)/,/^(?:C4Dynamic\b)/,/^(?:C4Deployment\b)/,/^(?:Person_Ext\b)/,/^(?:Person\b)/,/^(?:SystemQueue_Ext\b)/,/^(?:SystemDb_Ext\b)/,/^(?:System_Ext\b)/,/^(?:SystemQueue\b)/,/^(?:SystemDb\b)/,/^(?:System\b)/,/^(?:Boundary\b)/,/^(?:Enterprise_Boundary\b)/,/^(?:System_Boundary\b)/,/^(?:ContainerQueue_Ext\b)/,/^(?:ContainerDb_Ext\b)/,/^(?:Container_Ext\b)/,/^(?:ContainerQueue\b)/,/^(?:ContainerDb\b)/,/^(?:Container\b)/,/^(?:Container_Boundary\b)/,/^(?:ComponentQueue_Ext\b)/,/^(?:ComponentDb_Ext\b)/,/^(?:Component_Ext\b)/,/^(?:ComponentQueue\b)/,/^(?:ComponentDb\b)/,/^(?:Component\bLine truncated
`)},"elementFontStyles"),W0=d(t=>`.person {
stroke: ${t.personBorder};
fill: ${t.personBkg};
}
${z0()}
/* The element font colour is set inline per element (default white); the
label text takes it via currentColor. */
.c4-shape .label,
.c4-shape .label text {
color: inherit;
fill: currentColor;
}
/* Structurizr typography: bold name, smaller stereotype/type and description lines. */
.c4-shape .label .c4-name {
font-weight: bold;
}
.c4-shape .label .c4-type {
font-size: 0.75em;
}
.c4-shape .label .c4-descr {
font-size: 0.82em;
}
.c4-shape .basic,
.c4-shape rect,
.c4-shape path,
.c4-shape circle,
.c4-shape ellipse,
.c4-shape line {
stroke-width: 2px;
}
`,"getStyles"),X0=W0,K0={parser:ze,db:te,renderer:_e,styles:X0,init:d(({c4:t,wrap:e})=>{_e.setConf(t),te.setWrap(e)},"init")};export{K0 as diagram};
@@ -0,0 +1 @@
import{ai as o,aj as n}from"./mermaid.core.js";const t=(a,r)=>o.lang.round(n.parse(a)[r]);export{t as c};
@@ -0,0 +1 @@
import{_ as s}from"./mermaid.core.js";var t,e=(t=class{constructor(i){this.init=i,this.records=this.init()}reset(){this.records=this.init()}},s(t,"ImperativeState"),t);export{e as I};
@@ -0,0 +1,15 @@
import{_ as e}from"./mermaid.core.js";var l=e(()=>`
/* Font Awesome icon styling - consolidated */
.label-icon {
display: inline-block;
height: 1em;
overflow: visible;
vertical-align: -0.125em;
}
.node .label-icon path {
fill: currentColor;
stroke: revert;
stroke-width: revert;
}
`,"getIconStyles");export{l as g};
@@ -0,0 +1 @@
import{_ as i,j as l,U as d,G as o}from"./mermaid.core.js";var x=i((r,t)=>{const e=r.append("rect");if(e.attr("x",t.x),e.attr("y",t.y),e.attr("fill",t.fill),e.attr("stroke",t.stroke),e.attr("width",t.width),e.attr("height",t.height),t.name&&e.attr("name",t.name),t.rx&&e.attr("rx",t.rx),t.ry&&e.attr("ry",t.ry),t.attrs!==void 0)for(const s in t.attrs)e.attr(s,t.attrs[s]);return t.class&&e.attr("class",t.class),e},"drawRect"),p=i((r,t)=>{const e={x:t.startx,y:t.starty,width:t.stopx-t.startx,height:t.stopy-t.starty,fill:t.fill,stroke:t.stroke,class:"rect"};x(r,e).lower()},"drawBackgroundRect"),y=i((r,t)=>{const e=t.text.replace(d," "),s=r.append("text");s.attr("x",t.x),s.attr("y",t.y),s.attr("class","legend"),s.style("text-anchor",t.anchor),t.class&&s.attr("class",t.class);const a=s.append("tspan");return a.attr("x",t.x+t.textMargin*2),a.text(e),s},"drawText"),m=i((r,t,e,s)=>{const a=r.append("image");a.attr("x",t),a.attr("y",e);const n=o.sanitizeUrl(s);a.attr("xlink:href",n)},"drawImage"),g=i((r,t,e,s)=>{const a=r.append("use");a.attr("x",t),a.attr("y",e);const n=o.sanitizeUrl(s);a.attr("xlink:href",`#${n}`)},"drawEmbeddedImage"),h=i(()=>({x:0,y:0,width:100,height:100,fill:"#EDF2AE",stroke:"#666",anchor:"start",rx:0,ry:0}),"getNoteRect"),f=i(()=>({x:0,y:0,width:100,height:100,"text-anchor":"start",style:"#666",textMargin:0,rx:0,ry:0,tspan:!0}),"getTextObj"),w=i(()=>{let r=l(".mermaidTooltip");return r.empty()&&(r=l("body").append("div").attr("class","mermaidTooltip").style("opacity",0).style("position","absolute").style("text-align","center").style("max-width","200px").style("padding","2px").style("font-size","12px").style("background","#ffffde").style("border","1px solid #333").style("border-radius","2px").style("pointer-events","none").style("z-index","100")),r},"createTooltip");export{p as a,h as b,g as c,x as d,m as e,w as f,f as g,y as h};
@@ -0,0 +1,231 @@
import{g as ee}from"./chunk-XXDRQBXY.js";import{s as se}from"./chunk-POPQ4Y6H.js";import{_ as p,l as b,c as $,v as ie,x as re,a as ae,b as ne,g as oe,s as le,n as ce,o as he,T as ue,k as j,p as de,j as kt,O as fe}from"./mermaid.core.js";import{f as pe}from"./chunk-F27PBJKO.js";var Ct=(function(){var t=p(function(V,o,h,a){for(h=h||{},a=V.length;a--;h[V[a]]=o);return h},"o"),e=[1,2],s=[1,3],n=[1,4],r=[2,4],c=[1,9],u=[1,11],S=[1,16],d=[1,17],T=[1,18],E=[1,19],m=[1,33],L=[1,20],D=[1,21],f=[1,22],I=[1,23],R=[1,24],C=[1,26],F=[1,27],A=[1,28],P=[1,29],w=[1,30],z=[1,31],rt=[1,32],at=[1,35],nt=[1,36],ot=[1,37],lt=[1,38],K=[1,34],y=[1,4,5,16,17,19,21,22,24,25,26,27,28,29,33,35,37,38,41,45,48,51,52,53,54,57],ct=[1,4,5,14,15,16,17,19,21,22,24,25,26,27,28,29,33,35,37,38,39,40,41,45,48,51,52,53,54,57],Lt=[4,5,16,17,19,21,22,24,25,26,27,28,29,33,35,37,38,41,45,48,51,52,53,54,57],gt={trace:p(function(){},"trace"),yy:{},symbols_:{error:2,start:3,SPACE:4,NL:5,SD:6,document:7,line:8,statement:9,classDefStatement:10,styleStatement:11,cssClassStatement:12,idStatement:13,DESCR:14,"-->":15,HIDE_EMPTY:16,scale:17,WIDTH:18,COMPOSIT_STATE:19,STRUCT_START:20,STRUCT_STOP:21,STATE_DESCR:22,AS:23,ID:24,FORK:25,JOIN:26,CHOICE:27,CONCURRENT:28,note:29,notePosition:30,NOTE_TEXT:31,direction:32,acc_title:33,acc_title_value:34,acc_descr:35,acc_descr_value:36,acc_descr_multiline_value:37,CLICK:38,STRING:39,HREF:40,classDef:41,CLASSDEF_ID:42,CLASSDEF_STYLEOPTS:43,DEFAULT:44,style:45,STYLE_IDS:46,STYLEDEF_STYLEOPTS:47,class:48,CLASSENTITY_IDS:49,STYLECLASS:50,direction_tb:51,direction_bt:52,direction_rl:53,direction_lr:54,eol:55,";":56,EDGE_STATE:57,STYLE_SEPARATOR:58,left_of:59,right_of:60,$accept:0,$end:1},terminals_:{2:"error",4:"SPACE",5:"NL",6:"SD",14:"DESCR",15:"-->",16:"HIDE_EMPTY",17:"scale",18:"WIDTH",19:"COMPOSIT_STATE",20:"STRUCT_START",21:"STRUCT_STOP",22:"STATE_DESCR",23:"AS",24:"ID",25:"FORK",26:"JOIN",27:"CHOICE",28:"CONCURRENT",29:"note",31:"NOTE_TEXT",33:"acc_title",34:"acc_title_value",35:"acc_descr",36:"acc_descr_value",37:"acc_descr_multiline_value",38:"CLICK",39:"STRING",40:"HREF",41:"classDef",42:"CLASSDEF_ID",43:"CLASSDEF_STYLEOPTS",44:"DEFAULT",45:"style",46:"STYLE_IDS",47:"STYLEDEF_STYLEOPTS",48:"class",49:"CLASSENTITY_IDS",50:"STYLECLASS",51:"direction_tb",52:"direction_bt",53:"direction_rl",54:"direction_lr",56:";",57:"EDGE_STATE",58:"STYLE_SEPARATOR",59:"left_of",60:"right_of"},productions_:[0,[3,2],[3,2],[3,2],[7,0],[7,2],[8,2],[8,1],[8,1],[9,1],[9,1],[9,1],[9,1],[9,2],[9,3],[9,4],[9,1],[9,2],[9,1],[9,4],[9,3],[9,6],[9,1],[9,1],[9,1],[9,1],[9,4],[9,4],[9,1],[9,2],[9,2],[9,1],[9,5],[9,5],[10,3],[10,3],[11,3],[12,3],[32,1],[32,1],[32,1],[32,1],[55,1],[55,1],[13,1],[13,1],[13,3],[13,3],[30,1],[30,1]],performAction:p(function(o,h,a,g,_,i,B){var l=i.length-1;switch(_){case 3:return g.setRootDoc(i[l]),i[l];case 4:this.$=[];break;case 5:i[l]!="nl"&&(i[l-1].push(i[l]),this.$=i[l-1]);break;case 6:case 7:this.$=i[l];break;case 8:this.$="nl";break;case 12:this.$=i[l];break;case 13:const Z=i[l-1];Z.description=g.trimColon(i[l]),this.$=Z;break;case 14:this.$={stmt:"relation",state1:i[l-2],state2:i[l]};break;case 15:const Tt=g.trimColon(i[l]);this.$={stmt:"relation",state1:i[l-3],state2:i[l-1],description:Tt};break;case 19:this.$={stmt:"state",id:i[l-3],type:"default",description:"",doc:i[l-1]};break;case 20:var Y=i[l],X=i[l-2].trim();if(i[l].match(":")){var ut=i[l].split(":");Y=ut[0],X=[X,ut[1]]}this.$={stmt:"state",id:Y,type:"default",description:X};break;case 21:this.$={stmt:"state",id:i[l-3],type:"default",description:i[l-5],doc:i[l-1]};break;case 22:this.$={stmt:"state",id:i[l],type:"fork"};break;case 23:this.$={stmt:"state",id:i[l],type:"join"};break;case 24:this.$={stmt:"state",id:i[l],type:"choice"};break;case 25:this.$={stmt:"state",id:g.getDividerId(),type:"divider"};break;case 26:this.$={stmt:"state",id:i[l-1].trim(),note:{position:i[l-2].trim(),text:i[l].trim()}};break;case 29:this.$=i[l].trim(),g.setAccTitle(this.$);break;case 30:case 31:this.$=i[l].trim(),g.setAccDescription(this.$);break;case 32:this.$={stmt:"click",id:i[l-3],url:i[l-2],tooltip:i[l-1]};break;case 33:this.$={stmt:"click",id:i[l-3],url:i[l-1],tooltip:""};break;case 34:case 35:this.$={stmt:"classDef",id:i[l-1].trim(),classes:i[l].trim()};break;case 36:this.$={stmt:"style",id:i[l-1].trim(),styleClass:i[l].trim()};break;case 37:this.$={stmt:"applyClass",id:i[l-1].trim(),styleClass:i[l].trim()};break;case 38:g.setDirection("TB"),this.$={stmt:"dir",value:"TB"};break;case 39:g.setDirection("BT"),this.$={stmt:"dir",value:"BT"};break;case 40:g.setDirection("RL"),this.$={stmt:"dir",value:"RL"};break;case 41:g.setDirection("LR"),this.$={stmt:"dir",value:"LR"};break;case 44:case 45:this.$={stmt:"state",id:i[l].trim(),type:"default",description:""};break;case 46:this.$={stmt:"state",id:i[l-2].trim(),classes:[i[l].trim()],type:"default",description:""};break;case 47:this.$={stmt:"state",id:i[l-2].trim(),classes:[i[l].trim()],type:"default",description:""};break}Line truncated
`+k.showPosition()+`
Expecting `+ft.join(", ")+", got '"+(this.terminals_[x]||x)+"'":bt="Parse error on line "+(Y+1)+": Unexpected "+(x==Z?"end of input":"'"+(this.terminals_[x]||x)+"'"),this.parseError(bt,{text:k.match,token:this.terminals_[x]||x,line:k.yylineno,loc:_t,expected:ft})}if(N[0]instanceof Array&&N.length>1)throw new Error("Parse Error: multiple actions possible at state: "+W+", token: "+x);switch(N[0]){case 1:a.push(x),_.push(k.yytext),i.push(k.yylloc),a.push(N[1]),x=null,X=k.yyleng,l=k.yytext,Y=k.yylineno,_t=k.yylloc;break;case 2:if(G=this.productions_[N[1]][1],J.$=_[_.length-G],J._$={first_line:i[i.length-(G||1)].first_line,last_line:i[i.length-1].last_line,first_column:i[i.length-(G||1)].first_column,last_column:i[i.length-1].last_column},Zt&&(J._$.range=[i[i.length-(G||1)].range[0],i[i.length-1].range[1]]),mt=this.performAction.apply(J,[l,X,Y,U.yy,N[1],_,i].concat(Tt)),typeof mt<"u")return mt;G&&(a=a.slice(0,-1*G*2),_=_.slice(0,-1*G),i=i.slice(0,-1*G)),a.push(this.productions_[N[1]][0]),_.push(J.$),i.push(J._$),wt=B[a[a.length-2]][a[a.length-1]],a.push(wt);break;case 3:return!0}}return!0},"parse")},Qt=(function(){var V={EOF:1,parseError:p(function(h,a){if(this.yy.parser)this.yy.parser.parseError(h,a);else throw new Error(h)},"parseError"),setInput:p(function(o,h){return this.yy=h||this.yy||{},this._input=o,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:p(function(){var o=this._input[0];this.yytext+=o,this.yyleng++,this.offset++,this.match+=o,this.matched+=o;var h=o.match(/(?:\r\n?|\n).*/g);return h?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),o},"input"),unput:p(function(o){var h=o.length,a=o.split(/(?:\r\n?|\n)/g);this._input=o+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-h),this.offset-=h;var g=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),a.length-1&&(this.yylineno-=a.length-1);var _=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:a?(a.length===g.length?this.yylloc.first_column:0)+g[g.length-a.length].length-a[0].length:this.yylloc.first_column-h},this.options.ranges&&(this.yylloc.range=[_[0],_[0]+this.yyleng-h]),this.yyleng=this.yytext.length,this},"unput"),more:p(function(){return this._more=!0,this},"more"),reject:p(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).
`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:p(function(o){this.unput(this.match.slice(o))},"less"),pastInput:p(function(){var o=this.matched.substr(0,this.matched.length-this.match.length);return(o.length>20?"...":"")+o.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:p(function(){var o=this.match;return o.length<20&&(o+=this._input.substr(0,20-o.length)),(o.substr(0,20)+(o.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:p(function(){var o=this.pastInput(),h=new Array(o.length+1).join("-");return o+this.upcomingInput()+`
`+h+"^"},"showPosition"),test_match:p(function(o,h){var a,g,_;if(this.options.backtrack_lexer&&(_={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(_.yylloc.range=this.yylloc.range.slice(0))),g=o[0].match(/(?:\r\n?|\n).*/g),g&&(this.yylineno+=g.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:g?g[g.length-1].length-g[g.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+o[0].length},this.yytext+=o[0],this.match+=o[0],this.matches=o,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(o[0].length),this.matched+=o[0],a=this.performAction.call(this,this.yy,this,h,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),a)return a;if(this._backtrack){for(var i in _)this[i]=_[i];return!1}return!1},"test_match"),next:p(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var o,h,a,g;this._more||(this.yytext="",this.match="");for(var _=this._currentRules(),i=0;i<_.length;i++)if(a=this._input.match(this.rules[_[i]]),a&&(!h||a[0].length>h[0].length)){if(h=a,g=i,this.options.backtrack_lexer){if(o=this.test_match(a,_[i]),o!==!1)return o;if(this._backtrack){h=!1;continue}else return!1}else if(!this.options.flex)break}return h?(o=this.test_match(h,_[g]),o!==!1?o:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text.
`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:p(function(){var h=this.next();return h||this.lex()},"lex"),begin:p(function(h){this.conditionStack.push(h)},"begin"),popState:p(function(){var h=this.conditionStack.length-1;return h>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:p(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:p(function(h){return h=this.conditionStack.length-1-Math.abs(h||0),h>=0?this.conditionStack[h]:"INITIAL"},"topState"),pushState:p(function(h){this.begin(h)},"pushState"),stateStackSize:p(function(){return this.conditionStack.length},"stateStackSize"),options:{"case-insensitive":!0},performAction:p(function(h,a,g,_){function i(){const B=a.yytext.indexOf("%%");if(B===0)return!1;if(B>0){const l=a.yytext.slice(0,B),Y=a.yytext.slice(B);Y&&h.lexer.unput(Y),a.yytext=l}return!0}switch(p(i,"processId"),g){case 0:return 38;case 1:return 40;case 2:return 39;case 3:return 44;case 4:return 51;case 5:return 52;case 6:return 53;case 7:return 54;case 8:return 5;case 9:break;case 10:break;case 11:break;case 12:break;case 13:return this.pushState("SCALE"),17;case 14:return 18;case 15:this.popState();break;case 16:return this.begin("acc_title"),33;case 17:return this.popState(),"acc_title_value";case 18:return this.begin("acc_descr"),35;case 19:return this.popState(),"acc_descr_value";case 20:this.begin("acc_descr_multiline");break;case 21:this.popState();break;case 22:return"acc_descr_multiline_value";case 23:return this.pushState("CLASSDEF"),41;case 24:return this.popState(),this.pushState("CLASSDEFID"),"DEFAULT_CLASSDEF_ID";case 25:return this.popState(),this.pushState("CLASSDEFID"),42;case 26:return this.popState(),43;case 27:return this.pushState("CLASS"),48;case 28:return this.popState(),this.pushState("CLASS_STYLE"),49;case 29:return this.popState(),50;case 30:return this.pushState("STYLE"),45;case 31:return this.popState(),this.pushState("STYLEDEF_STYLES"),46;case 32:return this.popState(),47;case 33:return this.pushState("SCALE"),17;case 34:return 18;case 35:this.popState();break;case 36:this.pushState("STATE");break;case 37:return this.popState(),a.yytext=a.yytext.slice(0,-8).trim(),25;case 38:return this.popState(),a.yytext=a.yytext.slice(0,-8).trim(),26;case 39:return this.popState(),a.yytext=a.yytext.slice(0,-10).trim(),27;case 40:return this.popState(),a.yytext=a.yytext.slice(0,-8).trim(),25;case 41:return this.popState(),a.yytext=a.yytext.slice(0,-8).trim(),26;case 42:return this.popState(),a.yytext=a.yytext.slice(0,-10).trim(),27;case 43:return 51;case 44:return 52;case 45:return 53;case 46:return 54;case 47:this.pushState("STATE_STRING");break;case 48:return this.pushState("STATE_ID"),"AS";case 49:return i()?(this.popState(),"ID"):void 0;case 50:this.popState();break;case 51:return"STATE_DESCR";case 52:throw new Error('Error: State name must be a single word. Found: "'+a.yytext.trim()+'"');case 53:return 19;case 54:this.popState();break;case 55:return this.popState(),this.pushState("struct"),20;case 56:return this.popState(),21;case 57:break;case 58:return this.begin("NOTE"),29;case 59:return this.popState(),this.pushState("NOTE_ID"),59;case 60:return this.popState(),this.pushState("NOTE_ID"),60;case 61:this.popState(),this.pushState("FLOATING_NOTE");break;case 62:return this.popState(),this.pushState("FLOATING_NOTE_ID"),"AS";case 63:break;case 64:return"NOTE_TEXT";case 65:return i()?(this.popState(),"ID"):void 0;case 66:return i()?(this.popState(),this.pushState("NOTE_TEXT"),24):void 0;case 67:return this.popState(),a.yytext=a.yytext.substr(2).trim(),31;case 68:return this.popState(),a.yytext=a.yytext.slice(0,-8).trim(),31;case 69:return 6;case 70:return 6;case 71:return 16;case 72:return 57;case 73:return i()?24:void 0;case 74:return a.yytext=a.yytext.trim(),14;case 75:return 15;case 76:return 28;case 77:return 58;case 78:return 5;case 79:return"INVALID"}},"anonymous"),rules:[/^(?:click\b)/i,/^(?:href\b)/i,/^(?:"[^"]*")/i,/^(?:default\b)/i,/^(?:.*direction\s+TB[^\n]*)/i,/^(?:.*direction\s+BT[^\n]*)/i,/^(?:.*direction\s+RL[^\n]*)/i,/^(?:.*direction\s+LR[^\n]*)/i,/^(?:[\n]+)/i,/^(?:[\s]+)/i,/^(?:((?!\n)\s)+)/i,/^(?:#[^\n]*)/i,/^(?:%%(?!\{)[^\n]*)/i,/^(?:scale\s+)/i,/^(?:\d+)/i,/^(?:\s+width\b)/i,/^(?:accTitle\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*\{\s*)/i,/^(?:[\}])/i,/^(?:[^\}]*)/i,/^(?:classDef\s+)/i,/^(?:DEFAULT\s+)/i,/^(?:\w+\s+)/i,/^(?:[^\n]*)/i,/^(?:class\s+)/i,/^(?:(\w+)+((,\s*\w+)*))/i,/^(?:[^\n]*)/i,/^(?:style\s+)/i,/^(?:[\w,]+\s+)/i,/^(?:[^\n]*)/i,/^(?:scale\s+)/i,/^(?:\d+)/i,/^(?:\s+width\b)/i,/^(?:state\s+)/i,/^(?:.*<<fork>>)/i,/^(?:.*<<join>>)/i,/^(?:.*<<choice>>)/i,/^(?:.*\[\[fork\]\])/i,/^(?:.*\[\[join\]\])/i,/^(?:.*\[\[choice\]\])/i,/^(?:.*direction\s+TB[^\n]*)/i,/^(?:.*directio Line truncated
defs [id$="-barbEnd"] {
fill: ${t.transitionColor};
stroke: ${t.transitionColor};
}
g.stateGroup text {
fill: ${t.nodeBorder};
stroke: none;
font-size: 10px;
}
g.stateGroup text {
fill: ${t.textColor};
stroke: none;
font-size: 10px;
}
g.stateGroup .state-title {
font-weight: bolder;
fill: ${t.stateLabelColor};
}
g.stateGroup rect {
fill: ${t.mainBkg};
stroke: ${t.nodeBorder};
}
g.stateGroup line {
stroke: ${t.lineColor};
stroke-width: ${t.strokeWidth||1};
}
.transition {
stroke: ${t.transitionColor};
stroke-width: ${t.strokeWidth||1};
fill: none;
}
.stateGroup .composit {
fill: ${t.background};
border-bottom: 1px
}
.stateGroup .alt-composit {
fill: #e0e0e0;
border-bottom: 1px
}
.state-note {
stroke: ${t.noteBorderColor};
fill: ${t.noteBkgColor};
text {
fill: ${t.noteTextColor};
stroke: none;
font-size: 10px;
}
}
.stateLabel .box {
stroke: none;
stroke-width: 0;
fill: ${t.mainBkg};
opacity: 0.5;
}
.edgeLabel .label rect {
fill: ${t.labelBackgroundColor};
opacity: 0.5;
}
.edgeLabel {
background-color: ${t.edgeLabelBackground};
p {
background-color: ${t.edgeLabelBackground};
}
rect {
opacity: 0.5;
background-color: ${t.edgeLabelBackground};
fill: ${t.edgeLabelBackground};
}
text-align: center;
}
.edgeLabel .label text {
fill: ${t.transitionLabelColor||t.tertiaryTextColor};
}
.label div .edgeLabel {
color: ${t.transitionLabelColor||t.tertiaryTextColor};
}
.stateLabel text {
fill: ${t.stateLabelColor};
font-size: 10px;
font-weight: bold;
}
.node circle.state-start {
fill: ${t.specialStateColor};
stroke: ${t.specialStateColor};
}
.node .fork-join {
fill: ${t.specialStateColor};
stroke: ${t.specialStateColor};
}
.node circle.state-end {
fill: ${t.innerEndBackground};
stroke: ${t.background};
stroke-width: 1.5
}
.end-state-inner {
fill: ${t.compositeBackground||t.background};
// stroke: ${t.background};
stroke-width: 1.5
}
.node rect {
fill: ${t.stateBkg||t.mainBkg};
stroke: ${t.stateBorder||t.nodeBorder};
stroke-width: ${t.strokeWidth||1}px;
}
.node polygon {
fill: ${t.mainBkg};
stroke: ${t.stateBorder||t.nodeBorder};;
stroke-width: ${t.strokeWidth||1}px;
}
[id$="-barbEnd"] {
fill: ${t.lineColor};
}
.statediagram-cluster rect {
fill: ${t.compositeTitleBackground};
stroke: ${t.stateBorder||t.nodeBorder};
stroke-width: ${t.strokeWidth||1}px;
}
.cluster-label, .nodeLabel {
color: ${t.stateLabelColor};
// line-height: 1;
}
.statediagram-cluster rect.outer {
rx: 5px;
ry: 5px;
}
.statediagram-state .divider {
stroke: ${t.stateBorder||t.nodeBorder};
}
.statediagram-state .title-state {
rx: 5px;
ry: 5px;
}
.statediagram-cluster.statediagram-cluster .inner {
fill: ${t.compositeBackground||t.background};
}
.statediagram-cluster.statediagram-cluster-alt .inner {
fill: ${t.altBackground?t.altBackground:"#efefef"};
}
.statediagram-cluster .inner {
rx:0;
ry:0;
}
.statediagram-state rect.basic {
rx: 5px;
ry: 5px;
}
.statediagram-state rect.divider {
stroke-dasharray: 10,10;
fill: ${t.altBackground?t.altBackground:"#efefef"};
}
.note-edge {
stroke-dasharray: 5;
}
.statediagram-note rect {
fill: ${t.noteBkgColor};
stroke: ${t.noteBorderColor};
stroke-width: 1px;
rx: 0;
ry: 0;
}
.statediagram-note rect {
fill: ${t.noteBkgColor};
stroke: ${t.noteBorderColor};
stroke-width: 1px;
rx: 0;
ry: 0;
}
.statediagram-note text {
fill: ${t.noteTextColor};
}
.statediagram-note .nodeLabel {
color: ${t.noteTextColor};
}
.statediagram .edgeLabel {
color: red; // ${t.noteTextColor};
}
[id$="-dependencyStart"], [id$="-dependencyEnd"] {
fill: ${t.lineColor};
stroke: ${t.lineColor};
stroke-width: ${t.strokeWidth||1};
}
.statediagramTitleText {
text-anchor: middle;
font-size: 18px;
fill: ${t.textColor};
}
[data-look="neo"].statediagram-cluster rect {
fill: ${t.mainBkg};
stroke: ${t.useGradient?"url("+t.svgId+"-gradient)":t.stateBorder||t.nodeBorder};
stroke-width: ${t.strokeWidth??1};
}
[data-look="neo"].statediagram-cluster rect.outer {
rx: ${t.radius}px;
ry: ${t.radius}px;
filter: ${t.dropShadow?t.dropShadow.replace("url(#drop-shadow)",`url(${t.svgId}-drop-shadow)`):"none"}
}
`,"getStyles"),ze=Ye;export{He as S,We as a,je as b,ze as s};
@@ -0,0 +1 @@
import{_ as i}from"./mermaid.core.js";function t(c,e){c.accDescr&&e.setAccDescription?.(c.accDescr),c.accTitle&&e.setAccTitle?.(c.accTitle),c.title&&e.setDiagramTitle?.(c.title)}i(t,"populateCommonDb");export{t as p};
@@ -0,0 +1 @@
import{_ as a,d as w,l as x}from"./mermaid.core.js";var B=a((e,t,i,r)=>{e.attr("class",i);const{width:o,height:h,x:n,y:c}=u(e,t);w(e,h,o,r);const s=d(n,c,o,h,t);e.attr("viewBox",s),x.debug(`viewBox configured: ${s} with padding: ${t}`)},"setupViewPortForSVG"),u=a((e,t)=>{const i=e.node()?.getBBox()||{width:0,height:0,x:0,y:0};return{width:i.width+t*2,height:i.height+t*2,x:i.x,y:i.y}},"calculateDimensionsWithPadding"),d=a((e,t,i,r,o)=>`${e-o} ${t-o} ${i} ${r}`,"createViewBox");export{B as s};
@@ -0,0 +1,88 @@
import{_ as p,l as C,D as H,y as B,p as q,E as G,d as U,i as j,c as K}from"./mermaid.core.js";var A="",M="",O="",D=[],b=new Map,v=p(e=>j(e,K()),"sanitizeText"),y=p(e=>{switch(e.type){case"terminal":return{...e,value:v(e.value)};case"nonterminal":return{...e,name:v(e.name)};case"sequence":return{...e,elements:e.elements.map(y)};case"choice":return{...e,alternatives:e.alternatives.map(y)};case"optional":return{...e,element:y(e.element)};case"repetition":return{...e,element:y(e.element),separator:e.separator?y(e.separator):void 0};case"special":return{...e,text:v(e.text)}}},"sanitizeAstNode"),J=p(()=>{A="",M="",O="",D.length=0,b.clear(),q(),C.debug("[Railroad] Database cleared")},"clear"),Y=p(e=>{A=v(e),C.debug("[Railroad] Title set:",e)},"setTitle"),P=p(()=>A,"getTitle"),Q=p(e=>{const i={...e,name:v(e.name),definition:y(e.definition),comment:e.comment?v(e.comment):void 0};C.debug("[Railroad] Adding rule:",i.name),b.has(i.name)&&C.warn(`[Railroad] Rule '${i.name}' is already defined. Overwriting.`),D.push(i),b.set(i.name,i)},"addRule"),Z=p(()=>D,"getRules"),V=p(e=>b.get(e),"getRule"),ee=p(e=>{M=v(e).replace(/^\s+/g,""),C.debug("[Railroad] Accessibility title set:",e)},"setAccTitle"),te=p(()=>M,"getAccTitle"),re=p(e=>{O=v(e).replace(/\n\s+/g,`
`),C.debug("[Railroad] Accessibility description set:",e)},"setAccDescription"),ie=p(()=>O,"getAccDescription"),ne=Y,ae=P,oe={clear:J,setTitle:Y,getTitle:P,addRule:Q,getRules:Z,getRule:V,setAccTitle:ee,getAccTitle:te,setAccDescription:re,getAccDescription:ie,setDiagramTitle:ne,getDiagramTitle:ae},f={compactMode:!1,padding:10,verticalSeparation:8,horizontalSeparation:10,arcRadius:10,fontSize:14,fontFamily:"monospace",terminalFill:"#FFFFC0",terminalStroke:"#000000",terminalTextColor:"#000000",nonTerminalFill:"#FFFFFF",nonTerminalStroke:"#000000",nonTerminalTextColor:"#000000",lineColor:"#000000",strokeWidth:2,markerFill:"#000000",commentFill:"#E8E8E8",commentStroke:"#888888",commentTextColor:"#666666",specialFill:"#F0E0FF",specialStroke:"#8800CC",ruleNameColor:"#000066",showMarkers:!0,markerRadius:5},le=/^#(?:[\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$|^(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch)\([\d\s%+,./-]+\)$|^[a-z]+$/i,se=/^[\w "',.-]+$/,de=new Set(["compactMode","padding","verticalSeparation","horizontalSeparation","arcRadius","fontSize","fontFamily","terminalFill","terminalStroke","terminalTextColor","nonTerminalFill","nonTerminalStroke","nonTerminalTextColor","lineColor","strokeWidth","markerFill","commentFill","commentStroke","commentTextColor","specialFill","specialStroke","ruleNameColor","showMarkers","markerRadius"]),L=p(e=>e?Object.keys(e).every(i=>i==="railroad"||de.has(i)):!1,"isRailroadStyleOptions"),ce=p(e=>e?"railroad"in e&&e.railroad?e.railroad:L(e)?e:{}:{},"extractRailroadOverrides"),me=p(e=>{if(!e||L(e))return{};const{railroad:i,svgId:a,theme:n,look:t,...r}=e;return r},"extractThemeOverrides"),m=p((e,i)=>{if(typeof e!="string")return i;const a=e.trim();return le.test(a)?a:i},"sanitizeColorValue"),I=p((e,i)=>{if(typeof e!="string")return i;const a=e.trim();return se.test(a)?a:i},"sanitizeFontFamilyValue"),F=p((e,i)=>{const a=typeof e=="number"?e:typeof e=="string"?Number.parseFloat(e):Number.NaN;return Number.isFinite(a)&&a>=0?a:i},"sanitizeNumberValue"),he=p(e=>{const i=typeof e=="number"?e:typeof e=="string"?Number.parseFloat(e):Number.NaN;return Number.isFinite(i)&&i>0?i:void 0},"parseThemeFontSize"),pe=p(e=>{const i=I(e.fontFamily,f.fontFamily),a=he(e.fontSize)??f.fontSize;return{...f,fontFamily:i,fontSize:a,terminalFill:m(e.secondBkg??e.secondaryColor,f.terminalFill),terminalStroke:m(e.secondaryBorderColor??e.lineColor,f.terminalStroke),terminalTextColor:m(e.secondaryTextColor??e.textColor,f.terminalTextColor),nonTerminalFill:m(e.mainBkg??e.background,f.nonTerminalFill),nonTerminalStroke:m(e.primaryBorderColor??e.lineColor,f.nonTerminalStroke),nonTerminalTextColor:m(e.primaryTextColor??e.textColor,f.nonTerminalTextColor),lineColor:m(e.lineColor,f.lineColor),markerFill:m(e.lineColor,f.markerFill),commentFill:m(e.labelBackground??e.tertiaryColor,f.commentFill),commentStroke:m(e.tertiaryBorderColor??e.lineColor,f.commentStroke),commentTextColor:m(e.tertiaryTextColor??e.textColor,f.commentTextColor),specialFill:m(e.tertiaryColor??e.secondaryColor,f.specialFill),specialStroke:m(e.tertiaryBorderColor??e.secondaryBorderColor,f.specialStroke),ruleNameColor:m(e.titleColor??e.textColor,f.ruleNameColor)}},"buildThemeDefaults"),E=p(e=>{const i=B(),a={...G(),...i.themeVariables??{},...me(e)},n=pe(a),t={...i.railroad??{},...ce(e)};return{compactMode:t.compactMode??n.compactMode,padding:F(t.padding,n.padding),verticalSeparation:F(t.verticalSeparation,n.verticalSeparation),horizontalSeparation:F(t.horizontalSeparation,n.horizontalSeparation),arcRadius:F(t.arcRadius,n.arcRadius),fontSize:F(t.fontSize,n.fontSize),fontFamily:I(t.fontFamily,n.fontFamily),terminalFill:m(t.terminalFill,n.terminalFill),terminalStroke:m(t.terminalStroke,n.terminalStroke),terminalTextColor:m(t.terminalTextColor,n.terminalTextColor),nonTerminalFill:m(t.nonTerminalFill,n.nonTerminalFill),nonTerminalStroke:m(t.nonTerminalStroke,n.nonTerminalStroke),nonTerminalTextColor:m(t.nonTerminalTextColor,n.nonTerminalTextColor),lineColor:m(t.lineColor,n.lineColor),strokeWidth:F(t.strokeWidth,n.strokeWidth),markerFill:m(t.markerFill,n.markerFill),commentFill:m(t.commentFill,n.commentFill),commentStroke:m(t.commentStroke,n.commentStroke),commentTextColor:m(t.commentTextColor,n.commentTextColor),specialFill:m(t.specialFill,n.specialFill),specialStroke:m(t.specialStroke,n.specialStroke),ruleNameColor:m(t.ruleNameColor,n.ruleNameColor),showMarkers:t.showMarkers??n.showMarkers,markerRadius:F(t.markerRadius,n.markerRadius)}},"buildRailroadStyleOptions"),Te=p(e=>{const{fontFamily:i,fontSize:a,terminalFill:n,terminalStroke:t,terminalTextColor:r,nonTerminalFill:o,nonTerminalStroke:g,nonTerminalTextColor:l,lineColor:s,strokeWidth:h,markerFill:u,commentFill:c,commentStroke:w,commentTextColor:d,specialFill:T,specialStroke:z,ruleNameColor:S}=E(e);return`
.railroad-diagram {
font-family: ${i};
font-size: ${a}px;
}
.railroad-terminal rect {
fill: ${n};
stroke: ${t};
stroke-width: ${h}px;
}
.railroad-terminal text {
fill: ${r};
font-family: ${i};
font-size: ${a}px;
text-anchor: middle;
dominant-baseline: middle;
}
.railroad-nonterminal rect {
fill: ${o};
stroke: ${g};
stroke-width: ${h}px;
}
.railroad-nonterminal text {
fill: ${l};
font-family: ${i};
font-size: ${a}px;
text-anchor: middle;
dominant-baseline: middle;
}
.railroad-line {
stroke: ${s};
stroke-width: ${h}px;
fill: none;
}
.railroad-start circle,
.railroad-end circle {
fill: ${u};
}
.railroad-comment ellipse {
fill: ${c};
stroke: ${w};
stroke-width: ${h}px;
}
.railroad-comment text {
fill: ${d};
font-style: italic;
font-family: ${i};
font-size: ${a}px;
text-anchor: middle;
dominant-baseline: middle;
}
.railroad-special rect {
fill: ${T};
stroke: ${z};
stroke-width: ${h}px;
stroke-dasharray: 5,3;
}
.railroad-special text {
fill: ${l};
font-family: ${i};
font-size: ${a}px;
text-anchor: middle;
dominant-baseline: middle;
}
.railroad-rule-name {
font-weight: bold;
fill: ${S};
font-family: ${i};
font-size: ${a}px;
}
.railroad-group {
/* Grouping container, no specific styles */
}
`},"getStyles"),R,x=(R=class{constructor(){this.d=""}moveTo(i,a){return this.d+=`M ${i} ${a} `,this}lineTo(i,a){return this.d+=`L ${i} ${a} `,this}horizontalTo(i){return this.d+=`H ${i} `,this}verticalTo(i){return this.d+=`V ${i} `,this}arcTo(i,a,n,t,r,o,g){return this.d+=`A ${i} ${a} ${n} ${t?1:0} ${r?1:0} ${o} ${g} `,this}build(){return this.d.trim()}},p(R,"PathBuilder"),R),$,ue=($=class{constructor(i,a=E()){this.textCache=new Map,this.svg=i,this.config=a}measureText(i){if(this.textCache.has(i))return this.textCache.get(i);const a=this.svg.append("text").attr("font-family",this.config.fontFamily).attr("font-size",this.config.fontSize).text(i),n=a.node().getBBox(),t={width:n.width,height:n.height};return a.remove(),this.textCache.set(i,t),t}renderTerminal(i,a){const n=this.measureText(a),t=n.width+this.config.padding*2,r=n.height+this.config.padding*2,o=i.append("g").attr("class","railroad-terminal");return o.append("rect").attr("x",0).attr("y",0).attr("width",t).attr("height",r).attr("rx",10).attr("ry",10),o.append("text").attr("x",t/2).attr("y",r/2).text(a),{element:o.node(),dimensions:{width:t,height:r,up:r/2,down:r/2}}}renderNonTerminal(i,a){const n=this.measureText(a),t=n.width+this.config.padding*2,r=n.height+this.config.padding*2,o=i.append("g").attr("class","railroad-nonterminal");return o.append("rect").attr("x",0).attr("y",0).attr("width",t).attr("height",r),o.append("text").attr("x",t/2).attr("y",r/2).text(a),{element:o.node(),dimensions:{width:t,height:r,up:r/2,down:r/2}}}renderSequence(i,a){const n=a.map(s=>this.renderExpression(i,s));let t=0,r=0,o=0;for(const s of n)t+=s.dimensions.width,r=Math.max(r,s.dimensions.up),o=Math.max(o,s.dimensions.down);t+=(n.length-1)*this.config.horizontalSeparation;const g=i.append("g").attr("class","railroad-sequence");let l=0;for(let s=0;s<n.length;s++){const h=n[s],u=r-h.dimensions.up;if(g.node().appendChild(h.element).setAttribute("transform",`translate(${l}, ${u})`),s<n.length-1){const w=l+h.dimensions.width,d=w+this.config.horizontalSeparation,T=r;g.append("path").attr("class","railroad-line").attr("d",new x().moveTo(w,T).lineTo(d,T).build())}l+=h.dimensions.width+this.config.horizontalSeparation}return{element:g.node(),dimensions:{width:t,height:r+o,up:r,down:o}}}renderChoice(i,a){const n=a.map(c=>this.renderExpression(i,c));let t=0,r=0;for(const c of n)t=Math.max(t,c.dimensions.width),r+=c.dimensions.height;r+=(n.length-1)*this.config.verticalSeparation;const o=this.config.arcRadius,g=o*4,l=t+g,s=i.append("g").attr("class","railroad-choice");let h=0;const u=r/2;for(const c of n){const w=h,d=w+c.dimensions.up,T=o*2+(t-c.dimensions.width)/2;s.node().appendChild(c.element).setAttribute("transform",`translate(${T}, ${w})`);const S=new x,k=d>u;d===u?S.moveTo(0,u).lineTo(T,d):S.moveTo(0,u).arcTo(o,o,0,!1,k,o,u+(k?o:-o)).lineTo(o,d-(k?o:-o)).arcTo(o,o,0,!1,!k,o*2,d).lineTo(T,d),s.append("path").attr("class","railroad-line").attr("d",S.build());const N=new x,_=T+c.dimensions.width,X=l-o*2;d===u?N.moveTo(_,d).lineTo(l,u):N.moveTo(_,d).lineTo(X,d).arcTo(o,o,0,!1,!k,l-o,d+(k?-o:o)).lineTo(l-o,u+(k?o:-o)).arcTo(o,o,0,!1,k,l,u),s.append("path").attr("class","railroad-line").attr("d",N.build()),h+=c.dimensions.height+this.config.verticalSeparation}return{element:s.node(),dimensions:{width:l,height:r,up:u,down:r-u}}}renderOptional(i,a){const n=this.renderExpression(i,a),t=this.config.arcRadius,r=t*2,o=n.dimensions.width+t*4,g=n.dimensions.height+r,l=i.append("g").attr("class","railroad-optional"),s=t*2,h=r;l.node().appendChild(n.element).setAttribute("transform",`translate(${s}, ${h})`);const c=h+n.dimensions.up,w=new x().moveTo(0,c).lineTo(t*2,c);l.append("path").attr("class","railroad-line").attr("d",w.build());const d=new x().moveTo(s+n.dimensions.width,c).lineTo(o,c);l.append("path").attr("class","railroad-line").attr("d",d.build());const T=new x().moveTo(0,c).arcTo(t,t,0,!1,!1,t,c-t).lineTo(t,t).arcTo(t,t,0,!1,!0,t*2,0).lineTo(o-t*2,0).arcTo(t,t,0,!1,!0,o-t,t).lineTo(o-t,c-t).arcTo(t,t,0,!1,!1,o,c);return l.append("path").attr("class","railroad-line").attr("d",T.build()),{element:l.node(),dimensions:{width:o,height:g,up:c,down:g-c}}}renderRepetition(i,a,n){const t=this.renderExpression(i,a),r=this.config.arcRadius,o=r*2,g=t.dimensions.width+r*4,l=n===0,s=t.dimensions.height+o+(l?o:0),h=i.append("g").attr("class","railroad-repetition"),u=r*2,c=l?o:0;h.node().appendChild(t.element).setAttribute("transform",`translate(${u}, ${c})`);const d=c+t.dimensions.up;h.append("path").attr("class","railroad-line").attr("d",new x().moveTo(0,d).lineTo(r*2,d).build()),h.append("path").attr("class","railroad-line").attr("d",new x().moveTo(u+t.dimensions.width,d).lineTo(g,d).build());const T=c+t.dimensions.height+r,z=new x().moveTo(u+t.dimensions.width,d).arcTo(r,r,0,!1,!0,u+t.dimensions.width+r,d+r).lineTo(u+t.dimensions.width+r,T).arcTo(r,r,0,!1,!0,u+t.dimensions.width,T+r).lineTo(r*2,T+r).arcTo(r,r,0,!1,!0,r,T).lineTo(r,d+r).arcTo(r,r,0,!1,!0,r*2,d);if(h.append("path").attr("cl Line truncated
`+e);try{const n=H(i);n.attr("class","railroad-diagram");const r=B().railroad?.useMaxWidth??!0,o=oe.getRules();if(C.debug(`[Railroad] Rendering ${o.length} rules`),o.length===0){C.warn("[Railroad] No rules to render"),W(n,{height:100,width:200},r);return}const l=new ue(n,E()).renderDiagram(o);W(n,l,r),C.debug("[Railroad] Render complete")}catch(n){throw C.error("[Railroad] Render error:",n),n}},"draw"),xe={draw:ge};export{oe as d,Te as g,xe as r};
@@ -0,0 +1,206 @@
import{g as st}from"./chunk-5VM5RSS4.js";import{g as it}from"./chunk-XXDRQBXY.js";import{s as at}from"./chunk-POPQ4Y6H.js";import{_ as f,l as we,c as F,u as nt,v as rt,x as Ve,j as Ae,O as ut,b as lt,a as ct,s as ot,g as ht,n as dt,o as pt,k as I,p as At,q as ft,i as gt,P as U}from"./mermaid.core.js";import{f as mt}from"./chunk-F27PBJKO.js";var Pe=(function(){var s=f(function(O,o,h,p){for(h=h||{},p=O.length;p--;h[O[p]]=o);return h},"o"),i=[1,18],a=[1,19],n=[1,20],r=[1,41],c=[1,26],u=[1,42],d=[1,24],m=[1,25],g=[1,32],N=[1,33],ge=[1,34],b=[1,45],me=[1,35],Ce=[1,36],be=[1,37],ke=[1,38],Ee=[1,27],Te=[1,28],ye=[1,29],De=[1,30],Fe=[1,31],k=[1,44],E=[1,46],T=[1,43],y=[1,47],Be=[1,9],A=[1,8,9],ee=[1,58],te=[1,59],se=[1,60],ie=[1,61],ae=[1,62],_e=[1,63],Se=[1,64],_=[1,8,9,41],Me=[1,77],R=[1,8,9,12,13,22,39,41,44,46,68,69,70,71,72,73,74,79,81],ne=[1,8,9,12,13,18,20,22,39,41,44,46,47,60,68,69,70,71,72,73,74,79,81,86,100,102,103],re=[13,60,86,100,102,103],Y=[13,60,73,74,86,100,102,103],Re=[13,60,68,69,70,71,72,86,100,102,103],ue=[1,103],K=[1,121],W=[1,117],Q=[1,113],j=[1,119],X=[1,114],q=[1,115],H=[1,116],J=[1,118],Z=[1,120],Ge=[22,50,60,61,82,86,87,88,89,90],Ue=[1,128],le=[12,39],Ne=[1,8,9,39,41,44,46],ce=[1,8,9,22],ze=[1,153],Ye=[1,8,9,61],x=[1,8,9,22,50,60,61,82,86,87,88,89,90],Le={trace:f(function(){},"trace"),yy:{},symbols_:{error:2,start:3,mermaidDoc:4,statements:5,graphConfig:6,CLASS_DIAGRAM:7,NEWLINE:8,EOF:9,statement:10,classLabel:11,SQS:12,STR:13,SQE:14,namespaceName:15,alphaNumToken:16,classLiteralName:17,DOT:18,className:19,GENERICTYPE:20,relationStatement:21,LABEL:22,namespaceStatement:23,classStatement:24,memberStatement:25,annotationStatement:26,clickStatement:27,styleStatement:28,cssClassStatement:29,noteStatement:30,classDefStatement:31,direction:32,acc_title:33,acc_title_value:34,acc_descr:35,acc_descr_value:36,acc_descr_multiline_value:37,namespaceIdentifier:38,STRUCT_START:39,classStatements:40,STRUCT_STOP:41,NAMESPACE:42,classIdentifier:43,STYLE_SEPARATOR:44,members:45,ANNOTATION_START:46,ANNOTATION_END:47,CLASS:48,emptyBody:49,SPACE:50,MEMBER:51,SEPARATOR:52,relation:53,NOTE_FOR:54,noteText:55,NOTE:56,CLASSDEF:57,classList:58,stylesOpt:59,ALPHA:60,COMMA:61,direction_tb:62,direction_bt:63,direction_rl:64,direction_lr:65,relationType:66,lineType:67,AGGREGATION:68,EXTENSION:69,COMPOSITION:70,DEPENDENCY:71,LOLLIPOP:72,LINE:73,DOTTED_LINE:74,CALLBACK:75,LINK:76,LINK_TARGET:77,CLICK:78,CALLBACK_NAME:79,CALLBACK_ARGS:80,HREF:81,STYLE:82,CSSCLASS:83,style:84,styleComponent:85,NUM:86,COLON:87,UNIT:88,BRKT:89,PCT:90,commentToken:91,textToken:92,graphCodeTokens:93,textNoTagsToken:94,TAGSTART:95,TAGEND:96,"==":97,"--":98,DEFAULT:99,MINUS:100,keywords:101,UNICODE_TEXT:102,BQUOTE_STR:103,$accept:0,$end:1},terminals_:{2:"error",7:"CLASS_DIAGRAM",8:"NEWLINE",9:"EOF",12:"SQS",13:"STR",14:"SQE",18:"DOT",20:"GENERICTYPE",22:"LABEL",33:"acc_title",34:"acc_title_value",35:"acc_descr",36:"acc_descr_value",37:"acc_descr_multiline_value",39:"STRUCT_START",41:"STRUCT_STOP",42:"NAMESPACE",44:"STYLE_SEPARATOR",46:"ANNOTATION_START",47:"ANNOTATION_END",48:"CLASS",50:"SPACE",51:"MEMBER",52:"SEPARATOR",54:"NOTE_FOR",56:"NOTE",57:"CLASSDEF",60:"ALPHA",61:"COMMA",62:"direction_tb",63:"direction_bt",64:"direction_rl",65:"direction_lr",68:"AGGREGATION",69:"EXTENSION",70:"COMPOSITION",71:"DEPENDENCY",72:"LOLLIPOP",73:"LINE",74:"DOTTED_LINE",75:"CALLBACK",76:"LINK",77:"LINK_TARGET",78:"CLICK",79:"CALLBACK_NAME",80:"CALLBACK_ARGS",81:"HREF",82:"STYLE",83:"CSSCLASS",86:"NUM",87:"COLON",88:"UNIT",89:"BRKT",90:"PCT",93:"graphCodeTokens",95:"TAGSTART",96:"TAGEND",97:"==",98:"--",99:"DEFAULT",100:"MINUS",101:"keywords",102:"UNICODE_TEXT",103:"BQUOTE_STR"},productions_:[0,[3,1],[3,1],[4,1],[6,4],[5,1],[5,2],[5,3],[11,3],[15,1],[15,1],[15,3],[15,2],[19,1],[19,3],[19,1],[19,2],[19,2],[19,2],[10,1],[10,2],[10,1],[10,1],[10,1],[10,1],[10,1],[10,1],[10,1],[10,1],[10,1],[10,1],[10,2],[10,2],[10,1],[23,4],[23,5],[38,2],[38,3],[40,1],[40,2],[40,3],[40,1],[40,2],[40,3],[40,1],[40,2],[40,3],[24,1],[24,3],[24,4],[24,3],[24,6],[24,4],[24,7],[24,6],[43,2],[43,3],[49,0],[49,2],[49,2],[26,4],[45,1],[45,2],[25,1],[25,2],[25,1],[25,1],[21,3],[21,4],[21,4],[21,5],[30,3],[30,2],[31,3],[58,1],[58,3],[32,1],[32,1],[32,1],[32,1],[53,3],[53,2],[53,2],[53,1],[66,1],[66,1],[66,1],[66,1],[66,1],[67,1],[67,1],[27,3],[27,4],[27,3],[27,4],[27,4],[27,5],[27,3],[27,4],[27,4],[27,5],[27,4],[27,5],[27,5],[27,6],[28,3],[29,3],[59,1],[59,3],[84,1],[84,2],[85,1],[85,1],[85,1],[85,1],[85,1],[85,1],[85,1],[85,1],[85,1],[91,1],[91,1],[92,1],[92,1],[92,1],[92,1],[92,1],[92,1],[92,1],[94,1],[94,1],[94,1],[94,1],[16,1],[16,1],[16,1],[16,1],[17,1],[55,1]],performAction:f(function(o,h,p,l,C,e,$){var t=e.length-1;switch(C){case 8:this.$=e[t-1];break;case 9:case 10:case 13:case 15:this.$=e[t];break;case 11:case 14:this.$=e[t-2]+"."+e[t];break;case 12:case 16:this.$=e[t-1]+e[t];break;case 17:case 18:this.$=e[t-1]+"~"+e[t]+"~";break;case 19:l.addRelation(e[t]);break;case 20:e[t-1].titlLine truncated
`+D.showPosition()+`
Expecting `+pe.join(", ")+", got '"+(this.terminals_[B]||B)+"'":Oe="Parse error on line "+(he+1)+": Unexpected "+(B==We?"end of input":"'"+(this.terminals_[B]||B)+"'"),this.parseError(Oe,{text:D.match,token:this.terminals_[B]||B,line:D.yylineno,loc:ve,expected:pe})}if(L[0]instanceof Array&&L.length>1)throw new Error("Parse Error: multiple actions possible at state: "+P+", token: "+B);switch(L[0]){case 1:p.push(B),C.push(D.yytext),e.push(D.yylloc),p.push(L[1]),B=null,Ke=D.yyleng,t=D.yytext,he=D.yylineno,ve=D.yylloc;break;case 2:if(v=this.productions_[L[1]][1],G.$=C[C.length-v],G._$={first_line:e[e.length-(v||1)].first_line,last_line:e[e.length-1].last_line,first_column:e[e.length-(v||1)].first_column,last_column:e[e.length-1].last_column},et&&(G._$.range=[e[e.length-(v||1)].range[0],e[e.length-1].range[1]]),Ie=this.performAction.apply(G,[t,Ke,he,V.yy,L[1],C,e].concat($e)),typeof Ie<"u")return Ie;v&&(p=p.slice(0,-1*v*2),C=C.slice(0,-1*v),e=e.slice(0,-1*v)),p.push(this.productions_[L[1]][0]),C.push(G.$),e.push(G._$),je=$[p[p.length-2]][p[p.length-1]],p.push(je);break;case 3:return!0}}return!0},"parse")},Je=(function(){var O={EOF:1,parseError:f(function(h,p){if(this.yy.parser)this.yy.parser.parseError(h,p);else throw new Error(h)},"parseError"),setInput:f(function(o,h){return this.yy=h||this.yy||{},this._input=o,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:f(function(){var o=this._input[0];this.yytext+=o,this.yyleng++,this.offset++,this.match+=o,this.matched+=o;var h=o.match(/(?:\r\n?|\n).*/g);return h?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),o},"input"),unput:f(function(o){var h=o.length,p=o.split(/(?:\r\n?|\n)/g);this._input=o+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-h),this.offset-=h;var l=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),p.length-1&&(this.yylineno-=p.length-1);var C=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:p?(p.length===l.length?this.yylloc.first_column:0)+l[l.length-p.length].length-p[0].length:this.yylloc.first_column-h},this.options.ranges&&(this.yylloc.range=[C[0],C[0]+this.yyleng-h]),this.yyleng=this.yytext.length,this},"unput"),more:f(function(){return this._more=!0,this},"more"),reject:f(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).
`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:f(function(o){this.unput(this.match.slice(o))},"less"),pastInput:f(function(){var o=this.matched.substr(0,this.matched.length-this.match.length);return(o.length>20?"...":"")+o.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:f(function(){var o=this.match;return o.length<20&&(o+=this._input.substr(0,20-o.length)),(o.substr(0,20)+(o.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:f(function(){var o=this.pastInput(),h=new Array(o.length+1).join("-");return o+this.upcomingInput()+`
`+h+"^"},"showPosition"),test_match:f(function(o,h){var p,l,C;if(this.options.backtrack_lexer&&(C={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(C.yylloc.range=this.yylloc.range.slice(0))),l=o[0].match(/(?:\r\n?|\n).*/g),l&&(this.yylineno+=l.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:l?l[l.length-1].length-l[l.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+o[0].length},this.yytext+=o[0],this.match+=o[0],this.matches=o,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(o[0].length),this.matched+=o[0],p=this.performAction.call(this,this.yy,this,h,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),p)return p;if(this._backtrack){for(var e in C)this[e]=C[e];return!1}return!1},"test_match"),next:f(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var o,h,p,l;this._more||(this.yytext="",this.match="");for(var C=this._currentRules(),e=0;e<C.length;e++)if(p=this._input.match(this.rules[C[e]]),p&&(!h||p[0].length>h[0].length)){if(h=p,l=e,this.options.backtrack_lexer){if(o=this.test_match(p,C[e]),o!==!1)return o;if(this._backtrack){h=!1;continue}else return!1}else if(!this.options.flex)break}return h?(o=this.test_match(h,C[l]),o!==!1?o:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text.
`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:f(function(){var h=this.next();return h||this.lex()},"lex"),begin:f(function(h){this.conditionStack.push(h)},"begin"),popState:f(function(){var h=this.conditionStack.length-1;return h>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:f(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:f(function(h){return h=this.conditionStack.length-1-Math.abs(h||0),h>=0?this.conditionStack[h]:"INITIAL"},"topState"),pushState:f(function(h){this.begin(h)},"pushState"),stateStackSize:f(function(){return this.conditionStack.length},"stateStackSize"),options:{},performAction:f(function(h,p,l,C){switch(l){case 0:return 62;case 1:return 63;case 2:return 64;case 3:return 65;case 4:break;case 5:break;case 6:return this.begin("acc_title"),33;case 7:return this.popState(),"acc_title_value";case 8:return this.begin("acc_descr"),35;case 9:return this.popState(),"acc_descr_value";case 10:this.begin("acc_descr_multiline");break;case 11:this.popState();break;case 12:return"acc_descr_multiline_value";case 13:return 8;case 14:break;case 15:return 7;case 16:return 7;case 17:return"EDGE_STATE";case 18:this.begin("callback_name");break;case 19:this.popState();break;case 20:this.popState(),this.begin("callback_args");break;case 21:return 79;case 22:this.popState();break;case 23:return 80;case 24:this.popState();break;case 25:return"STR";case 26:this.begin("string");break;case 27:return 82;case 28:return 57;case 29:return this.begin("namespace"),42;case 30:return this.popState(),8;case 31:break;case 32:return this.begin("namespace-body"),39;case 33:this.popState(),this.less(0);break;case 34:return this.popState(),41;case 35:return"EOF_IN_STRUCT";case 36:return 8;case 37:break;case 38:return"EDGE_STATE";case 39:return this.begin("class"),48;case 40:return this.popState(),8;case 41:break;case 42:return this.popState(),this.popState(),41;case 43:return this.begin("class-body"),39;case 44:return this.popState(),41;case 45:return"EOF_IN_STRUCT";case 46:return"EDGE_STATE";case 47:return"OPEN_IN_STRUCT";case 48:break;case 49:return"MEMBER";case 50:return 83;case 51:return 75;case 52:return 76;case 53:return 78;case 54:return 54;case 55:return 56;case 56:return 46;case 57:return 47;case 58:return 81;case 59:this.popState();break;case 60:return"GENERICTYPE";case 61:this.begin("generic");break;case 62:this.popState();break;case 63:return"BQUOTE_STR";case 64:this.begin("bqstring");break;case 65:return 77;case 66:return 77;case 67:return 77;case 68:return 77;case 69:return 69;case 70:return 69;case 71:return 71;case 72:return 71;case 73:return 70;case 74:return 68;case 75:return 72;case 76:return 73;case 77:return 74;case 78:return 22;case 79:return 44;case 80:return 100;case 81:return 18;case 82:return"PLUS";case 83:return 87;case 84:return 61;case 85:return 89;case 86:return 89;case 87:return 90;case 88:return"EQUALS";case 89:return"EQUALS";case 90:return 60;case 91:return 12;case 92:return 14;case 93:return"PUNCTUATION";case 94:return 86;case 95:return 102;case 96:return 50;case 97:return 50;case 98:return 9}},"anonymous"),rules:[/^(?:.*direction\s+TB[^\n]*)/,/^(?:.*direction\s+BT[^\n]*)/,/^(?:.*direction\s+RL[^\n]*)/,/^(?:.*direction\s+LR[^\n]*)/,/^(?:%%(?!\{)*[^\n]*(\r?\n?)+)/,/^(?:%%[^\n]*(\r?\n)*)/,/^(?:accTitle\s*:\s*)/,/^(?:(?!\n||)*[^\n]*)/,/^(?:accDescr\s*:\s*)/,/^(?:(?!\n||)*[^\n]*)/,/^(?:accDescr\s*\{\s*)/,/^(?:[\}])/,/^(?:[^\}]*)/,/^(?:\s*(\r?\n)+)/,/^(?:\s+)/,/^(?:classDiagram-v2\b)/,/^(?:classDiagram\b)/,/^(?:\[\*\])/,/^(?:call[\s]+)/,/^(?:\([\s]*\))/,/^(?:\()/,/^(?:[^(]*)/,/^(?:\))/,/^(?:[^)]*)/,/^(?:["])/,/^(?:[^"]*)/,/^(?:["])/,/^(?:style\b)/,/^(?:classDef\b)/,/^(?:namespace\b)/,/^(?:\s*(\r?\n)+)/,/^(?:\s+)/,/^(?:[{])/,/^(?:[}])/,/^(?:[}])/,/^(?:$)/,/^(?:\s*(\r?\n)+)/,/^(?:\s+)/,/^(?:\[\*\])/,/^(?:class\b)/,/^(?:\s*(\r?\n)+)/,/^(?:\s+)/,/^(?:[}])/,/^(?:[{])/,/^(?:[}])/,/^(?:$)/,/^(?:\[\*\])/,/^(?:[{])/,/^(?:[\n])/,/^(?:[^{}\n]*)/,/^(?:cssClass\b)/,/^(?:callback\b)/,/^(?:link\b)/,/^(?:click\b)/,/^(?:note for\b)/,/^(?:note\b)/,/^(?:<<)/,/^(?:>>)/,/^(?:href\b)/,/^(?:[~])/,/^(?:[^~]*)/,/^(?:~)/,/^(?:[`])/,/^(?:[^`]+)/,/^(?:[`])/,/^(?:_self\b)/,/^(?:_blank\b)/,/^(?:_parent\b)/,/^(?:_top\b)/,/^(?:\s*<\|)/,/^(?:\s*\|>)/,/^(?:\s*>)/,/^(?:\s*<)/,/^(?:\s*\*)/,/^(?:\s*o\b)/,/^(?:\s*\(\))/,/^(?:--)/,/^(?:\.\.)/,/^(?::{1}[^:\n;]+)/,/^(?::{3})/,/^(?:-)/,/^(?:\.)/,/^(?:\+)/,/^(?::)/,/^(?:,)/,/^(?:#)/,/^(?:#)/,/^(?:%)/,/^(?:=)/,/^(?:=)/,/^(?:\w+)/,/^(?:\[)/,/^(?:\])/,/^(?:[!"#$%&'*+,-.`?\\/])/,/^(?:[0-9]+)/,/^(?:[\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6]|[\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377]|[\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5]|[\u03F7-\u0481\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA]|[\uLine truncated
fill: ${s.nodeBorder||s.classText};
stroke: none;
font-family: ${s.fontFamily};
font-size: 10px;
.title {
font-weight: bolder;
}
}
.cluster-label text {
fill: ${s.titleColor};
}
.cluster-label span {
color: ${s.titleColor};
}
.cluster-label span p {
background-color: transparent;
}
.cluster rect {
fill: ${s.clusterBkg};
stroke: ${s.clusterBorder};
stroke-width: 1px;
}
.cluster text {
fill: ${s.titleColor};
}
.cluster span {
color: ${s.titleColor};
}
.nodeLabel, .edgeLabel {
color: ${s.classText};
}
.noteLabel .nodeLabel, .noteLabel .edgeLabel {
color: ${s.noteTextColor};
}
.edgeLabel .label rect {
fill: ${s.mainBkg};
}
.label text {
fill: ${s.classText};
}
.labelBkg {
background: ${s.mainBkg};
}
.edgeLabel .label span {
background: ${s.mainBkg};
}
.classTitle {
font-weight: bolder;
}
.node rect,
.node circle,
.node ellipse,
.node polygon,
.node path {
fill: ${s.mainBkg};
stroke: ${s.nodeBorder};
stroke-width: ${s.strokeWidth};
}
.divider {
stroke: ${s.nodeBorder};
stroke-width: 1;
}
g.clickable {
cursor: pointer;
}
g.classGroup rect {
fill: ${s.mainBkg};
stroke: ${s.nodeBorder};
}
g.classGroup line {
stroke: ${s.nodeBorder};
stroke-width: 1;
}
.classLabel .box {
stroke: none;
stroke-width: 0;
fill: ${s.mainBkg};
opacity: 0.5;
}
.classLabel .label {
fill: ${s.nodeBorder};
font-size: 10px;
}
.relation {
stroke: ${s.lineColor};
stroke-width: ${s.strokeWidth};
fill: none;
}
.dashed-line{
stroke-dasharray: 3;
}
.dotted-line{
stroke-dasharray: 1 2;
}
[id$="-compositionStart"], .composition {
fill: ${s.lineColor} !important;
stroke: ${s.lineColor} !important;
stroke-width: 1;
}
[id$="-compositionEnd"], .composition {
fill: ${s.lineColor} !important;
stroke: ${s.lineColor} !important;
stroke-width: 1;
}
[id$="-dependencyStart"], .dependency {
fill: ${s.lineColor} !important;
stroke: ${s.lineColor} !important;
stroke-width: 1;
}
[id$="-dependencyEnd"], .dependency {
fill: ${s.lineColor} !important;
stroke: ${s.lineColor} !important;
stroke-width: 1;
}
[id$="-extensionStart"], .extension {
fill: transparent !important;
stroke: ${s.lineColor} !important;
stroke-width: 1;
}
[id$="-extensionEnd"], .extension {
fill: transparent !important;
stroke: ${s.lineColor} !important;
stroke-width: 1;
}
[id$="-aggregationStart"], .aggregation {
fill: transparent !important;
stroke: ${s.lineColor} !important;
stroke-width: 1;
}
[id$="-aggregationEnd"], .aggregation {
fill: transparent !important;
stroke: ${s.lineColor} !important;
stroke-width: 1;
}
[id$="-lollipopStart"], .lollipop {
fill: ${s.mainBkg} !important;
stroke: ${s.lineColor} !important;
stroke-width: 1;
}
[id$="-lollipopEnd"], .lollipop {
fill: ${s.mainBkg} !important;
stroke: ${s.lineColor} !important;
stroke-width: 1;
}
.edgeTerminals {
font-size: 11px;
line-height: initial;
}
.classTitleText {
text-anchor: middle;
font-size: 18px;
fill: ${s.textColor};
}
.edgeLabel[data-look="neo"] {
background-color: ${s.edgeLabelBackground};
p {
background-color: ${s.edgeLabelBackground};
}
rect {
opacity: 0.5;
background-color: ${s.edgeLabelBackground};
fill: ${s.edgeLabelBackground};
}
text-align: center;
}
${st()}
`,"getStyles"),Nt=Ct,bt=f((s,i="TB")=>{if(!s.doc)return i;let a=i;for(const n of s.doc)n.stmt==="dir"&&(a=n.value);return a},"getDir"),kt=f(function(s,i){return i.db.getClasses()},"getClasses"),Et=f(async function(s,i,a,n){we.info("REF0:"),we.info("Drawing class diagram (v3)",i);const{securityLevel:r,state:c,layout:u}=F();n.db.setDiagramId(i);const d=n.db.getData(),m=it(i,r);d.type=n.type,d.layoutAlgorithm=nt(u),d.nodeSpacing=c?.nodeSpacing||50,d.rankSpacing=c?.rankSpacing||50,d.markers=["aggregation","extension","composition","dependency","lollipop"],d.diagramId=i,await rt(d,m);const g=8;Ve.insertTitle(m,"classDiagramTitleText",c?.titleTopMargin??25,n.db.getDiagramTitle()),at(m,g,"classDiagram",c?.useMaxWidth??!0)},"draw"),Lt={getClasses:kt,draw:Et,getDir:bt};export{St as C,_t as a,Lt as c,Nt as s};
@@ -0,0 +1 @@
import{_ as a,j as o}from"./mermaid.core.js";var g=a((t,e)=>{let n;return e==="sandbox"&&(n=o("#i"+t)),(e==="sandbox"?o(n.nodes()[0].contentDocument.body):o("body")).select(`[id="${t}"]`)},"getDiagramElement");export{g};
@@ -0,0 +1 @@
import{s as a,c as s,a as e,C as t}from"./chunk-TICWLB2K.js";import{_ as i}from"./mermaid.core.js";import"./chunk-5VM5RSS4.js";import"./chunk-XXDRQBXY.js";import"./chunk-POPQ4Y6H.js";import"./chunk-F27PBJKO.js";import"./index.js";var n={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{n as diagram};
@@ -0,0 +1 @@
import{s as a,c as s,a as e,C as t}from"./chunk-TICWLB2K.js";import{_ as i}from"./mermaid.core.js";import"./chunk-5VM5RSS4.js";import"./chunk-XXDRQBXY.js";import"./chunk-POPQ4Y6H.js";import"./chunk-F27PBJKO.js";import"./index.js";var n={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{n as diagram};
@@ -1 +1 @@
var b;(function(s){s.assertEqual=n=>{};function e(n){}s.assertIs=e;function t(n){throw new Error}s.assertNever=t,s.arrayToEnum=n=>{const i={};for(const a of n)i[a]=a;return i},s.getValidEnumValues=n=>{const i=s.objectKeys(n).filter(o=>typeof n[n[o]]!="number"),a={};for(const o of i)a[o]=n[o];return s.objectValues(a)},s.objectValues=n=>s.objectKeys(n).map(function(i){return n[i]}),s.objectKeys=typeof Object.keys=="function"?n=>Object.keys(n):n=>{const i=[];for(const a in n)Object.prototype.hasOwnProperty.call(n,a)&&i.push(a);return i},s.find=(n,i)=>{for(const a of n)if(i(a))return a},s.isInteger=typeof Number.isInteger=="function"?n=>Number.isInteger(n):n=>typeof n=="number"&&Number.isFinite(n)&&Math.floor(n)===n;function r(n,i=" | "){return n.map(a=>typeof a=="string"?`'${a}'`:a).join(i)}s.joinValues=r,s.jsonStringifyReplacer=(n,i)=>typeof i=="bigint"?i.toString():i})(b||(b={}));var Ie;(function(s){s.mergeShapes=(e,t)=>({...e,...t})})(Ie||(Ie={}));const f=b.arrayToEnum(["string","nan","number","integer","float","boolean","date","bigint","symbol","function","undefined","null","array","object","unknown","promise","void","never","map","set"]),I=s=>{switch(typeof s){case"undefined":return f.undefined;case"string":return f.string;case"number":return Number.isNaN(s)?f.nan:f.number;case"boolean":return f.boolean;case"function":return f.function;case"bigint":return f.bigint;case"symbol":return f.symbol;case"object":return Array.isArray(s)?f.array:s===null?f.null:s.then&&typeof s.then=="function"&&s.catch&&typeof s.catch=="function"?f.promise:typeof Map<"u"&&s instanceof Map?f.map:typeof Set<"u"&&s instanceof Set?f.set:typeof Date<"u"&&s instanceof Date?f.date:f.object;default:return f.unknown}},c=b.arrayToEnum(["invalid_type","invalid_literal","custom","invalid_union","invalid_union_discriminator","invalid_enum_value","unrecognized_keys","invalid_arguments","invalid_return_type","invalid_date","invalid_string","too_small","too_big","invalid_intersection_types","not_multiple_of","not_finite"]);class T extends Error{get errors(){return this.issues}constructor(e){super(),this.issues=[],this.addIssue=r=>{this.issues=[...this.issues,r]},this.addIssues=(r=[])=>{this.issues=[...this.issues,...r]};const t=new.target.prototype;Object.setPrototypeOf?Object.setPrototypeOf(this,t):this.__proto__=t,this.name="ZodError",this.issues=e}format(e){const t=e||function(i){return i.message},r={_errors:[]},n=i=>{for(const a of i.issues)if(a.code==="invalid_union")a.unionErrors.map(n);else if(a.code==="invalid_return_type")n(a.returnTypeError);else if(a.code==="invalid_arguments")n(a.argumentsError);else if(a.path.length===0)r._errors.push(t(a));else{let o=r,l=0;for(;l<a.path.length;){const d=a.path[l];l===a.path.length-1?(o[d]=o[d]||{_errors:[]},o[d]._errors.push(t(a))):o[d]=o[d]||{_errors:[]},o=o[d],l++}}};return n(this),r}static assert(e){if(!(e instanceof T))throw new Error(`Not a ZodError: ${e}`)}toString(){return this.message}get message(){return JSON.stringify(this.issues,b.jsonStringifyReplacer,2)}get isEmpty(){return this.issues.length===0}flatten(e=t=>t.message){const t={},r=[];for(const n of this.issues)if(n.path.length>0){const i=n.path[0];t[i]=t[i]||[],t[i].push(e(n))}else r.push(e(n));return{formErrors:r,fieldErrors:t}}get formErrors(){return this.flatten()}}T.create=s=>new T(s);const _e=(s,e)=>{let t;switch(s.code){case c.invalid_type:s.received===f.undefined?t="Required":t=`Expected ${s.expected}, received ${s.received}`;break;case c.invalid_literal:t=`Invalid literal value, expected ${JSON.stringify(s.expected,b.jsonStringifyReplacer)}`;break;case c.unrecognized_keys:t=`Unrecognized key(s) in object: ${b.joinValues(s.keys,", ")}`;break;case c.invalid_union:t="Invalid input";break;case c.invalid_union_discriminator:t=`Invalid discriminator value. Expected ${b.joinValues(s.options)}`;break;case c.invalid_enum_value:t=`Invalid enum value. Expected ${b.joinValues(s.options)}, received '${s.received}'`;break;case c.invalid_arguments:t="Invalid function arguments";break;case c.invalid_return_type:t="Invalid function return type";break;case c.invalid_date:t="Invalid date";break;case c.invalid_string:typeof s.validation=="object"?"includes"in s.validation?(t=`Invalid input: must include "${s.validation.includes}"`,typeof s.validation.position=="number"&&(t=`${t} at one or more positions greater than or equal to ${s.validation.position}`)):"startsWith"in s.validation?t=`Invalid input: must start with "${s.validation.startsWith}"`:"endsWith"in s.validation?t=`Invalid input: must end with "${s.validation.endsWith}"`:b.assertNever(s.validation):s.validation!=="regex"?t=`Invalid ${s.validation}`:t="Invalid";break;case c.too_small:s.type==="array"?t=`Array must contain ${s.exact?"exactly":s.inclusive?"at least":"more than"} ${s.minimum} element(s)`:s.type==="string"?t=`String must contain ${s.exact?"exactly":s.inclusive?"at least":"over"} ${s.minimum} character(s)`:s.type==="number"?t=`Number must be ${s.exact?"exactly equal to ":s.iLine truncated
var b;(function(s){s.assertEqual=n=>{};function e(n){}s.assertIs=e;function t(n){throw new Error}s.assertNever=t,s.arrayToEnum=n=>{const i={};for(const a of n)i[a]=a;return i},s.getValidEnumValues=n=>{const i=s.objectKeys(n).filter(o=>typeof n[n[o]]!="number"),a={};for(const o of i)a[o]=n[o];return s.objectValues(a)},s.objectValues=n=>s.objectKeys(n).map(function(i){return n[i]}),s.objectKeys=typeof Object.keys=="function"?n=>Object.keys(n):n=>{const i=[];for(const a in n)Object.prototype.hasOwnProperty.call(n,a)&&i.push(a);return i},s.find=(n,i)=>{for(const a of n)if(i(a))return a},s.isInteger=typeof Number.isInteger=="function"?n=>Number.isInteger(n):n=>typeof n=="number"&&Number.isFinite(n)&&Math.floor(n)===n;function r(n,i=" | "){return n.map(a=>typeof a=="string"?`'${a}'`:a).join(i)}s.joinValues=r,s.jsonStringifyReplacer=(n,i)=>typeof i=="bigint"?i.toString():i})(b||(b={}));var Ie;(function(s){s.mergeShapes=(e,t)=>({...e,...t})})(Ie||(Ie={}));const f=b.arrayToEnum(["string","nan","number","integer","float","boolean","date","bigint","symbol","function","undefined","null","array","object","unknown","promise","void","never","map","set"]),I=s=>{switch(typeof s){case"undefined":return f.undefined;case"string":return f.string;case"number":return Number.isNaN(s)?f.nan:f.number;case"boolean":return f.boolean;case"function":return f.function;case"bigint":return f.bigint;case"symbol":return f.symbol;case"object":return Array.isArray(s)?f.array:s===null?f.null:s.then&&typeof s.then=="function"&&s.catch&&typeof s.catch=="function"?f.promise:typeof Map<"u"&&s instanceof Map?f.map:typeof Set<"u"&&s instanceof Set?f.set:typeof Date<"u"&&s instanceof Date?f.date:f.object;default:return f.unknown}},c=b.arrayToEnum(["invalid_type","invalid_literal","custom","invalid_union","invalid_union_discriminator","invalid_enum_value","unrecognized_keys","invalid_arguments","invalid_return_type","invalid_date","invalid_string","too_small","too_big","invalid_intersection_types","not_multiple_of","not_finite"]);class T extends Error{get errors(){return this.issues}constructor(e){super(),this.issues=[],this.addIssue=r=>{this.issues=[...this.issues,r]},this.addIssues=(r=[])=>{this.issues=[...this.issues,...r]};const t=new.target.prototype;Object.setPrototypeOf?Object.setPrototypeOf(this,t):this.__proto__=t,this.name="ZodError",this.issues=e}format(e){const t=e||function(i){return i.message},r={_errors:[]},n=i=>{for(const a of i.issues)if(a.code==="invalid_union")a.unionErrors.map(n);else if(a.code==="invalid_return_type")n(a.returnTypeError);else if(a.code==="invalid_arguments")n(a.argumentsError);else if(a.path.length===0)r._errors.push(t(a));else{let o=r,l=0;for(;l<a.path.length;){const d=a.path[l];l===a.path.length-1?(o[d]=o[d]||{_errors:[]},o[d]._errors.push(t(a))):o[d]=o[d]||{_errors:[]},o=o[d],l++}}};return n(this),r}static assert(e){if(!(e instanceof T))throw new Error(`Not a ZodError: ${e}`)}toString(){return this.message}get message(){return JSON.stringify(this.issues,b.jsonStringifyReplacer,2)}get isEmpty(){return this.issues.length===0}flatten(e=t=>t.message){const t={},r=[];for(const n of this.issues)if(n.path.length>0){const i=n.path[0];t[i]=t[i]||[],t[i].push(e(n))}else r.push(e(n));return{formErrors:r,fieldErrors:t}}get formErrors(){return this.flatten()}}T.create=s=>new T(s);const _e=(s,e)=>{let t;switch(s.code){case c.invalid_type:s.received===f.undefined?t="Required":t=`Expected ${s.expected}, received ${s.received}`;break;case c.invalid_literal:t=`Invalid literal value, expected ${JSON.stringify(s.expected,b.jsonStringifyReplacer)}`;break;case c.unrecognized_keys:t=`Unrecognized key(s) in object: ${b.joinValues(s.keys,", ")}`;break;case c.invalid_union:t="Invalid input";break;case c.invalid_union_discriminator:t=`Invalid discriminator value. Expected ${b.joinValues(s.options)}`;break;case c.invalid_enum_value:t=`Invalid enum value. Expected ${b.joinValues(s.options)}, received '${s.received}'`;break;case c.invalid_arguments:t="Invalid function arguments";break;case c.invalid_return_type:t="Invalid function return type";break;case c.invalid_date:t="Invalid date";break;case c.invalid_string:typeof s.validation=="object"?"includes"in s.validation?(t=`Invalid input: must include "${s.validation.includes}"`,typeof s.validation.position=="number"&&(t=`${t} at one or more positions greater than or equal to ${s.validation.position}`)):"startsWith"in s.validation?t=`Invalid input: must start with "${s.validation.startsWith}"`:"endsWith"in s.validation?t=`Invalid input: must end with "${s.validation.endsWith}"`:b.assertNever(s.validation):s.validation!=="regex"?t=`Invalid ${s.validation}`:t="Invalid";break;case c.too_small:s.type==="array"?t=`Array must contain ${s.exact?"exactly":s.inclusive?"at least":"more than"} ${s.minimum} element(s)`:s.type==="string"?t=`String must contain ${s.exact?"exactly":s.inclusive?"at least":"over"} ${s.minimum} character(s)`:s.type==="number"?t=`Number must be ${s.exact?"exactly equal to ":s.iLine truncated
@@ -0,0 +1 @@
import{_ as V,l as $,j as lt}from"./mermaid.core.js";import{c as tt}from"./cytoscape.esm.js";import{g as gt}from"./index.js";var k={exports:{}},Z={exports:{}},Q={exports:{}},ut=Q.exports,q;function ft(){return q||(q=1,(function(G,b){(function(I,L){G.exports=L()})(ut,function(){return(function(N){var I={};function L(o){if(I[o])return I[o].exports;var e=I[o]={i:o,l:!1,exports:{}};return N[o].call(e.exports,e,e.exports,L),e.l=!0,e.exports}return L.m=N,L.c=I,L.i=function(o){return o},L.d=function(o,e,t){L.o(o,e)||Object.defineProperty(o,e,{configurable:!1,enumerable:!0,get:t})},L.n=function(o){var e=o&&o.__esModule?function(){return o.default}:function(){return o};return L.d(e,"a",e),e},L.o=function(o,e){return Object.prototype.hasOwnProperty.call(o,e)},L.p="",L(L.s=26)})([(function(N,I,L){function o(){}o.QUALITY=1,o.DEFAULT_CREATE_BENDS_AS_NEEDED=!1,o.DEFAULT_INCREMENTAL=!1,o.DEFAULT_ANIMATION_ON_LAYOUT=!0,o.DEFAULT_ANIMATION_DURING_LAYOUT=!1,o.DEFAULT_ANIMATION_PERIOD=50,o.DEFAULT_UNIFORM_LEAF_NODE_SIZES=!1,o.DEFAULT_GRAPH_MARGIN=15,o.NODE_DIMENSIONS_INCLUDE_LABELS=!1,o.SIMPLE_NODE_SIZE=40,o.SIMPLE_NODE_HALF_SIZE=o.SIMPLE_NODE_SIZE/2,o.EMPTY_COMPOUND_NODE_SIZE=40,o.MIN_EDGE_LENGTH=1,o.WORLD_BOUNDARY=1e6,o.INITIAL_WORLD_BOUNDARY=o.WORLD_BOUNDARY/1e3,o.WORLD_CENTER_X=1200,o.WORLD_CENTER_Y=900,N.exports=o}),(function(N,I,L){var o=L(2),e=L(8),t=L(9);function i(g,n,d){o.call(this,d),this.isOverlapingSourceAndTarget=!1,this.vGraphObject=d,this.bendpoints=[],this.source=g,this.target=n}i.prototype=Object.create(o.prototype);for(var l in o)i[l]=o[l];i.prototype.getSource=function(){return this.source},i.prototype.getTarget=function(){return this.target},i.prototype.isInterGraph=function(){return this.isInterGraph},i.prototype.getLength=function(){return this.length},i.prototype.isOverlapingSourceAndTarget=function(){return this.isOverlapingSourceAndTarget},i.prototype.getBendpoints=function(){return this.bendpoints},i.prototype.getLca=function(){return this.lca},i.prototype.getSourceInLca=function(){return this.sourceInLca},i.prototype.getTargetInLca=function(){return this.targetInLca},i.prototype.getOtherEnd=function(g){if(this.source===g)return this.target;if(this.target===g)return this.source;throw"Node is not incident with this edge"},i.prototype.getOtherEndInGraph=function(g,n){for(var d=this.getOtherEnd(g),r=n.getGraphManager().getRoot();;){if(d.getOwner()==n)return d;if(d.getOwner()==r)break;d=d.getOwner().getParent()}return null},i.prototype.updateLength=function(){var g=new Array(4);this.isOverlapingSourceAndTarget=e.getIntersection(this.target.getRect(),this.source.getRect(),g),this.isOverlapingSourceAndTarget||(this.lengthX=g[0]-g[2],this.lengthY=g[1]-g[3],Math.abs(this.lengthX)<1&&(this.lengthX=t.sign(this.lengthX)),Math.abs(this.lengthY)<1&&(this.lengthY=t.sign(this.lengthY)),this.length=Math.sqrt(this.lengthX*this.lengthX+this.lengthY*this.lengthY))},i.prototype.updateLengthSimple=function(){this.lengthX=this.target.getCenterX()-this.source.getCenterX(),this.lengthY=this.target.getCenterY()-this.source.getCenterY(),Math.abs(this.lengthX)<1&&(this.lengthX=t.sign(this.lengthX)),Math.abs(this.lengthY)<1&&(this.lengthY=t.sign(this.lengthY)),this.length=Math.sqrt(this.lengthX*this.lengthX+this.lengthY*this.lengthY)},N.exports=i}),(function(N,I,L){function o(e){this.vGraphObject=e}N.exports=o}),(function(N,I,L){var o=L(2),e=L(10),t=L(13),i=L(0),l=L(16),g=L(4);function n(r,h,a,p){a==null&&p==null&&(p=h),o.call(this,p),r.graphManager!=null&&(r=r.graphManager),this.estimatedSize=e.MIN_VALUE,this.inclusionTreeDepth=e.MAX_VALUE,this.vGraphObject=p,this.edges=[],this.graphManager=r,a!=null&&h!=null?this.rect=new t(h.x,h.y,a.width,a.height):this.rect=new t}n.prototype=Object.create(o.prototype);for(var d in o)n[d]=o[d];n.prototype.getEdges=function(){return this.edges},n.prototype.getChild=function(){return this.child},n.prototype.getOwner=function(){return this.owner},n.prototype.getWidth=function(){return this.rect.width},n.prototype.setWidth=function(r){this.rect.width=r},n.prototype.getHeight=function(){return this.rect.height},n.prototype.setHeight=function(r){this.rect.height=r},n.prototype.getCenterX=function(){return this.rect.x+this.rect.width/2},n.prototype.getCenterY=function(){return this.rect.y+this.rect.height/2},n.prototype.getCenter=function(){return new g(this.rect.x+this.rect.width/2,this.rect.y+this.rect.height/2)},n.prototype.getLocation=function(){return new g(this.rect.x,this.rect.y)},n.prototype.getRect=function(){return this.rect},n.prototype.getDiagonal=function(){return Math.sqrt(this.rect.width*this.rect.width+this.rect.height*this.rect.height)},n.prototype.getHalfTheDiagonal=function(){return Math.sqrt(this.rect.height*this.rect.height+this.rect.width*this.rect.width)/2},n.prototype.setRect=function(r,h){this.rect.x=r.x,this.rect.y=r.y,this.rect.width=h.width,this.rect.height=h.height},n.prototype.setCenter=function(r,h){this.rect.x=r-this.rect.width/2,this.rect.y=h-this.rect.height/2},nLine truncated
@@ -0,0 +1,166 @@
import{_ as ut}from"./index.js";var fk=Object.create,Lu=Object.defineProperty,dk=Object.getOwnPropertyDescriptor,vh=Object.getOwnPropertyNames,pk=Object.getPrototypeOf,mk=Object.prototype.hasOwnProperty,s=(t,e)=>Lu(t,"name",{value:e,configurable:!0}),hk=(t,e)=>function(){return t&&(e=(0,t[vh(t)[0]])(t=0)),e},X=(t,e)=>function(){return e||(0,t[vh(t)[0]])((e={exports:{}}).exports,e),e.exports},en=(t,e)=>{for(var r in e)Lu(t,r,{get:e[r],enumerable:!0})},Th=(t,e,r,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of vh(e))!mk.call(t,a)&&a!==r&&Lu(t,a,{get:()=>e[a],enumerable:!(n=dk(e,a))||n.enumerable});return t},kf=(t,e,r)=>(Th(t,e,"default"),r),$h=(t,e,r)=>(r=t!=null?fk(pk(t)):{},Th(Lu(r,"default",{value:t,enumerable:!0}),t)),Rh=t=>Th(Lu({},"__esModule",{value:!0}),t),Of={};en(Of,{AnnotatedTextEdit:()=>Ar,ChangeAnnotation:()=>pn,ChangeAnnotationIdentifier:()=>et,CodeAction:()=>Zp,CodeActionContext:()=>Jp,CodeActionKind:()=>Xp,CodeActionTriggerKind:()=>Yl,CodeDescription:()=>wp,CodeLens:()=>Qp,Color:()=>Ac,ColorInformation:()=>Ap,ColorPresentation:()=>Ep,Command:()=>dn,CompletionItem:()=>Gp,CompletionItemKind:()=>kp,CompletionItemLabelDetails:()=>Mp,CompletionItemTag:()=>Lp,CompletionList:()=>Fp,CreateFile:()=>ba,DeleteFile:()=>Sa,Diagnostic:()=>Wl,DiagnosticRelatedInformation:()=>Ec,DiagnosticSeverity:()=>_p,DiagnosticTag:()=>Sp,DocumentHighlight:()=>Kp,DocumentHighlightKind:()=>Up,DocumentLink:()=>tm,DocumentSymbol:()=>Yp,DocumentUri:()=>Tp,EOL:()=>x$,FoldingRange:()=>bp,FoldingRangeKind:()=>Cp,FormattingOptions:()=>em,Hover:()=>zp,InlayHint:()=>cm,InlayHintKind:()=>_c,InlayHintLabelPart:()=>Sc,InlineCompletionContext:()=>ym,InlineCompletionItem:()=>dm,InlineCompletionList:()=>pm,InlineCompletionTriggerKind:()=>mm,InlineValueContext:()=>um,InlineValueEvaluatableExpression:()=>lm,InlineValueText:()=>sm,InlineValueVariableLookup:()=>om,InsertReplaceEdit:()=>Dp,InsertTextFormat:()=>Op,InsertTextMode:()=>xp,Location:()=>Kl,LocationLink:()=>Rp,MarkedString:()=>Hl,MarkupContent:()=>wa,MarkupKind:()=>bc,OptionalVersionedTextDocumentIdentifier:()=>ql,ParameterInformation:()=>jp,Position:()=>oe,Range:()=>te,RenameFile:()=>_a,SelectedCompletionInfo:()=>hm,SelectionRange:()=>rm,SemanticTokenModifiers:()=>am,SemanticTokenTypes:()=>nm,SemanticTokens:()=>im,SignatureInformation:()=>Bp,StringValue:()=>fm,SymbolInformation:()=>qp,SymbolKind:()=>Wp,SymbolTag:()=>Vp,TextDocument:()=>vm,TextDocumentEdit:()=>Vl,TextDocumentIdentifier:()=>Ip,TextDocumentItem:()=>Pp,TextEdit:()=>ar,URI:()=>Rc,VersionedTextDocumentIdentifier:()=>Np,WorkspaceChange:()=>D$,WorkspaceEdit:()=>Cc,WorkspaceFolder:()=>gm,WorkspaceSymbol:()=>Hp,integer:()=>$p,uinteger:()=>Ul});var Tp,Rc,$p,Ul,oe,te,Kl,Rp,Ac,Ap,Ep,Cp,bp,Ec,_p,Sp,wp,Wl,dn,ar,pn,et,Ar,Vl,ba,_a,Sa,Cc,Pl,Bd,D$,Ip,Np,ql,Pp,bc,wa,kp,Op,Lp,Dp,xp,Mp,Gp,Fp,Hl,zp,jp,Bp,Up,Kp,Wp,Vp,qp,Hp,Yp,Xp,Yl,Jp,Zp,Qp,em,tm,rm,nm,am,im,sm,om,lm,um,_c,Sc,cm,fm,dm,pm,mm,hm,ym,gm,x$,vm,av,E,Du=hk({"../../node_modules/.pnpm/[email protected]/node_modules/vscode-languageserver-types/lib/esm/main.js"(){var t,e,r,n;(function(a){function i(o){return typeof o=="string"}s(i,"is"),a.is=i})(Tp||(Tp={})),(function(a){function i(o){return typeof o=="string"}s(i,"is"),a.is=i})(Rc||(Rc={})),(function(a){a.MIN_VALUE=-2147483648,a.MAX_VALUE=2147483647;function i(o){return typeof o=="number"&&a.MIN_VALUE<=o&&o<=a.MAX_VALUE}s(i,"is"),a.is=i})($p||($p={})),(function(a){a.MIN_VALUE=0,a.MAX_VALUE=2147483647;function i(o){return typeof o=="number"&&a.MIN_VALUE<=o&&o<=a.MAX_VALUE}s(i,"is"),a.is=i})(Ul||(Ul={})),(function(a){function i(u,l){return u===Number.MAX_VALUE&&(u=Ul.MAX_VALUE),l===Number.MAX_VALUE&&(l=Ul.MAX_VALUE),{line:u,character:l}}s(i,"create"),a.create=i;function o(u){let l=u;return E.objectLiteral(l)&&E.uinteger(l.line)&&E.uinteger(l.character)}s(o,"is"),a.is=o})(oe||(oe={})),(function(a){function i(u,l,c,f){if(E.uinteger(u)&&E.uinteger(l)&&E.uinteger(c)&&E.uinteger(f))return{start:oe.create(u,l),end:oe.create(c,f)};if(oe.is(u)&&oe.is(l))return{start:u,end:l};throw new Error(`Range#create called with invalid arguments[${u}, ${l}, ${c}, ${f}]`)}s(i,"create"),a.create=i;function o(u){let l=u;return E.objectLiteral(l)&&oe.is(l.start)&&oe.is(l.end)}s(o,"is"),a.is=o})(te||(te={})),(function(a){function i(u,l){return{uri:u,range:l}}s(i,"create"),a.create=i;function o(u){let l=u;return E.objectLiteral(l)&&te.is(l.range)&&(E.string(l.uri)||E.undefined(l.uri))}s(o,"is"),a.is=o})(Kl||(Kl={})),(function(a){function i(u,l,c,f){return{targetUri:u,targetRange:l,targetSelectionRange:c,originSelectionRange:f}}s(i,"create"),a.create=i;function o(u){let l=u;return E.objectLiteral(l)&&te.is(l.targetRange)&&E.string(l.targetUri)&&te.is(l.targetSelectionRange)&&(te.is(l.originSelectionRange)||E.undefined(l.originSelectionRange))}s(o,"is"),a.is=o})(Rp||(Rp={})),(function(a){function i(u,l,c,f){return{red:u,green:l,blue:c,alpha:f}}s(i,"create"),a.create=i;function o(u){const l=u;return E.objectLiteral(l)&&E.numberRaLine truncated
`,`\r
`,"\r"],(function(a){function i(c,f,d,p){return new av(c,f,d,p)}s(i,"create"),a.create=i;function o(c){let f=c;return!!(E.defined(f)&&E.string(f.uri)&&(E.undefined(f.languageId)||E.string(f.languageId))&&E.uinteger(f.lineCount)&&E.func(f.getText)&&E.func(f.positionAt)&&E.func(f.offsetAt))}s(o,"is"),a.is=o;function u(c,f){let d=c.getText(),p=l(f,(h,T)=>{let C=h.range.start.line-T.range.start.line;return C===0?h.range.start.character-T.range.start.character:C}),y=d.length;for(let h=p.length-1;h>=0;h--){let T=p[h],C=c.offsetAt(T.range.start),v=c.offsetAt(T.range.end);if(v<=y)d=d.substring(0,C)+T.newText+d.substring(v,d.length);else throw new Error("Overlapping edit");y=C}return d}s(u,"applyEdits"),a.applyEdits=u;function l(c,f){if(c.length<=1)return c;const d=c.length/2|0,p=c.slice(0,d),y=c.slice(d);l(p,f),l(y,f);let h=0,T=0,C=0;for(;h<p.length&&T<y.length;)f(p[h],y[T])<=0?c[C++]=p[h++]:c[C++]=y[T++];for(;h<p.length;)c[C++]=p[h++];for(;T<y.length;)c[C++]=y[T++];return c}s(l,"mergeSort")})(vm||(vm={})),av=(n=class{constructor(i,o,u,l){this._uri=i,this._languageId=o,this._version=u,this._content=l,this._lineOffsets=void 0}get uri(){return this._uri}get languageId(){return this._languageId}get version(){return this._version}getText(i){if(i){let o=this.offsetAt(i.start),u=this.offsetAt(i.end);return this._content.substring(o,u)}return this._content}update(i,o){this._content=i.text,this._version=o,this._lineOffsets=void 0}getLineOffsets(){if(this._lineOffsets===void 0){let i=[],o=this._content,u=!0;for(let l=0;l<o.length;l++){u&&(i.push(l),u=!1);let c=o.charAt(l);u=c==="\r"||c===`
`,c==="\r"&&l+1<o.length&&o.charAt(l+1)===`
`&&l++}u&&o.length>0&&i.push(o.length),this._lineOffsets=i}return this._lineOffsets}positionAt(i){i=Math.max(Math.min(i,this._content.length),0);let o=this.getLineOffsets(),u=0,l=o.length;if(l===0)return oe.create(0,i);for(;u<l;){let f=Math.floor((u+l)/2);o[f]>i?l=f:u=f+1}let c=u-1;return oe.create(c,i-o[c])}offsetAt(i){let o=this.getLineOffsets();if(i.line>=o.length)return this._content.length;if(i.line<0)return 0;let u=o[i.line],l=i.line+1<o.length?o[i.line+1]:this._content.length;return Math.max(Math.min(u+i.character,l),u)}get lineCount(){return this.getLineOffsets().length}},s(n,"FullTextDocument"),n),(function(a){const i=Object.prototype.toString;function o(v){return typeof v<"u"}s(o,"defined"),a.defined=o;function u(v){return typeof v>"u"}s(u,"undefined"),a.undefined=u;function l(v){return v===!0||v===!1}s(l,"boolean"),a.boolean=l;function c(v){return i.call(v)==="[object String]"}s(c,"string"),a.string=c;function f(v){return i.call(v)==="[object Number]"}s(f,"number"),a.number=f;function d(v,w,b){return i.call(v)==="[object Number]"&&w<=v&&v<=b}s(d,"numberRange"),a.numberRange=d;function p(v){return i.call(v)==="[object Number]"&&-2147483648<=v&&v<=2147483647}s(p,"integer"),a.integer=p;function y(v){return i.call(v)==="[object Number]"&&0<=v&&v<=2147483647}s(y,"uinteger"),a.uinteger=y;function h(v){return i.call(v)==="[object Function]"}s(h,"func"),a.func=h;function T(v){return v!==null&&typeof v=="object"}s(T,"objectLiteral"),a.objectLiteral=T;function C(v,w){return Array.isArray(v)&&v.every(w)}s(C,"typedArray"),a.typedArray=C})(E||(E={}))}}),Wn=X({"../../node_modules/.pnpm/[email protected]/node_modules/vscode-jsonrpc/lib/common/ral.js"(t){Object.defineProperty(t,"__esModule",{value:!0});var e;function r(){if(e===void 0)throw new Error("No runtime abstraction layer installed");return e}s(r,"RAL"),(function(n){function a(i){if(i===void 0)throw new Error("No runtime abstraction layer provided");e=i}s(a,"install"),n.install=a})(r||(r={})),t.default=r}}),xu=X({"../../node_modules/.pnpm/[email protected]/node_modules/vscode-jsonrpc/lib/common/is.js"(t){Object.defineProperty(t,"__esModule",{value:!0}),t.stringArray=t.array=t.func=t.error=t.number=t.string=t.boolean=void 0;function e(l){return l===!0||l===!1}s(e,"boolean"),t.boolean=e;function r(l){return typeof l=="string"||l instanceof String}s(r,"string"),t.string=r;function n(l){return typeof l=="number"||l instanceof Number}s(n,"number"),t.number=n;function a(l){return l instanceof Error}s(a,"error"),t.error=a;function i(l){return typeof l=="function"}s(i,"func"),t.func=i;function o(l){return Array.isArray(l)}s(o,"array"),t.array=o;function u(l){return o(l)&&l.every(c=>r(c))}s(u,"stringArray"),t.stringArray=u}}),nl=X({"../../node_modules/.pnpm/[email protected]/node_modules/vscode-jsonrpc/lib/common/events.js"(t){var i,o;Object.defineProperty(t,"__esModule",{value:!0}),t.Emitter=t.Event=void 0;var e=Wn(),r;(function(u){const l={dispose(){}};u.None=function(){return l}})(r||(t.Event=r={}));var n=(i=class{add(l,c=null,f){this._callbacks||(this._callbacks=[],this._contexts=[]),this._callbacks.push(l),this._contexts.push(c),Array.isArray(f)&&f.push({dispose:s(()=>this.remove(l,c),"dispose")})}remove(l,c=null){if(!this._callbacks)return;let f=!1;for(let d=0,p=this._callbacks.length;d<p;d++)if(this._callbacks[d]===l)if(this._contexts[d]===c){this._callbacks.splice(d,1),this._contexts.splice(d,1);return}else f=!0;if(f)throw new Error("When adding a listener with a context, you should remove it with the same context")}invoke(...l){if(!this._callbacks)return[];const c=[],f=this._callbacks.slice(0),d=this._contexts.slice(0);for(let p=0,y=f.length;p<y;p++)try{c.push(f[p].apply(d[p],l))}catch(h){(0,e.default)().console.error(h)}return c}isEmpty(){return!this._callbacks||this._callbacks.length===0}dispose(){this._callbacks=void 0,this._contexts=void 0}},s(i,"CallbackList"),i),a=(o=class{constructor(l){this._options=l}get event(){return this._event||(this._event=(l,c,f)=>{this._callbacks||(this._callbacks=new n),this._options&&this._options.onFirstListenerAdd&&this._callbacks.isEmpty()&&this._options.onFirstListenerAdd(this),this._callbacks.add(l,c);const d={dispose:s(()=>{this._callbacks&&(this._callbacks.remove(l,c),d.dispose=o._noop,this._options&&this._options.onLastListenerRemove&&this._callbacks.isEmpty()&&this._options.onLastListenerRemove(this))},"dispose")};return Array.isArray(f)&&f.push(d),d}),this._event}fire(l){this._callbacks&&this._callbacks.invoke.call(this._callbacks,l)}dispose(){this._callbacks&&(this._callbacks.dispose(),this._callbacks=void 0)}},s(o,"Emitter"),o);t.Emitter=a,a._noop=function(){}}}),Lf=X({"../../node_modules/.pnpm/[email protected]/node_modules/vscode-jsonrpc/lib/common/cancellation.js"(t){var l,c;Object.defineProperty(t,"__esModule",{value:!0}),t.CancellationTokenSource=t.CancellationToken=void 0;var e=Wn(),r=xu(),n=nl(),a;(function(f){f.None=Object.freeze({isCancellationRequested:!1,onCancellationRequested:n.Even Line truncated
${JSON.stringify(Object.fromEntries(h))}`));return}const C=parseInt(T);if(isNaN(C)){this.fireError(new Error(`Content-Length value must be a number. Got ${T}`));return}this.nextMessageLength=C}const y=this.buffer.tryReadBody(this.nextMessageLength);if(y===void 0){this.setPartialMessageTimer();return}this.clearPartialMessageTimer(),this.nextMessageLength=-1,this.readSemaphore.lock(async()=>{const h=this.options.contentDecoder!==void 0?await this.options.contentDecoder.decode(y):y,T=await this.options.contentTypeDecoder.decode(h,this.options);this.callback(T)}).catch(h=>{this.fireError(h)})}}catch(y){this.fireError(y)}}clearPartialMessageTimer(){this.partialMessageTimer&&(this.partialMessageTimer.dispose(),this.partialMessageTimer=void 0)}setPartialMessageTimer(){this.clearPartialMessageTimer(),!(this._partialMessageTimeout<=0)&&(this.partialMessageTimer=(0,e.default)().timer.setTimeout((p,y)=>{this.partialMessageTimer=void 0,p===this.messageToken&&(this.firePartialMessage({messageToken:p,waitingTime:y}),this.setPartialMessageTimer())},this._partialMessageTimeout,this.messageToken,this._partialMessageTimeout))}},s(f,"ReadableStreamMessageReader"),f);t.ReadableStreamMessageReader=l}}),Tk=X({"../../node_modules/.pnpm/[email protected]/node_modules/vscode-jsonrpc/lib/common/messageWriter.js"(t){var d,p;Object.defineProperty(t,"__esModule",{value:!0}),t.WriteableStreamMessageWriter=t.AbstractMessageWriter=t.MessageWriter=void 0;var e=Wn(),r=xu(),n=F$(),a=nl(),i="Content-Length: ",o=`\r
`,u;(function(y){function h(T){let C=T;return C&&r.func(C.dispose)&&r.func(C.onClose)&&r.func(C.onError)&&r.func(C.write)}s(h,"is"),y.is=h})(u||(t.MessageWriter=u={}));var l=(d=class{constructor(){this.errorEmitter=new a.Emitter,this.closeEmitter=new a.Emitter}dispose(){this.errorEmitter.dispose(),this.closeEmitter.dispose()}get onError(){return this.errorEmitter.event}fireError(h,T,C){this.errorEmitter.fire([this.asError(h),T,C])}get onClose(){return this.closeEmitter.event}fireClose(){this.closeEmitter.fire(void 0)}asError(h){return h instanceof Error?h:new Error(`Writer received error. Reason: ${r.string(h.message)?h.message:"unknown"}`)}},s(d,"AbstractMessageWriter"),d);t.AbstractMessageWriter=l;var c;(function(y){function h(T){return T===void 0||typeof T=="string"?{charset:T??"utf-8",contentTypeEncoder:(0,e.default)().applicationJson.encoder}:{charset:T.charset??"utf-8",contentEncoder:T.contentEncoder,contentTypeEncoder:T.contentTypeEncoder??(0,e.default)().applicationJson.encoder}}s(h,"fromOptions"),y.fromOptions=h})(c||(c={}));var f=(p=class extends l{constructor(h,T){super(),this.writable=h,this.options=c.fromOptions(T),this.errorCount=0,this.writeSemaphore=new n.Semaphore(1),this.writable.onError(C=>this.fireError(C)),this.writable.onClose(()=>this.fireClose())}async write(h){return this.writeSemaphore.lock(async()=>this.options.contentTypeEncoder.encode(h,this.options).then(C=>this.options.contentEncoder!==void 0?this.options.contentEncoder.encode(C):C).then(C=>{const v=[];return v.push(i,C.byteLength.toString(),o),v.push(o),this.doWrite(h,v,C)},C=>{throw this.fireError(C),C}))}async doWrite(h,T,C){try{return await this.writable.write(T.join(""),"ascii"),this.writable.write(C)}catch(v){return this.handleError(v,h),Promise.reject(v)}}handleError(h,T){this.errorCount++,this.fireError(h,T,this.errorCount)}end(){this.writable.end()}},s(p,"WriteableStreamMessageWriter"),p);t.WriteableStreamMessageWriter=f}}),$k=X({"../../node_modules/.pnpm/[email protected]/node_modules/vscode-jsonrpc/lib/common/messageBuffer.js"(t){var i;Object.defineProperty(t,"__esModule",{value:!0}),t.AbstractMessageBuffer=void 0;var e=13,r=10,n=`\r
`,a=(i=class{constructor(u="utf-8"){this._encoding=u,this._chunks=[],this._totalLength=0}get encoding(){return this._encoding}append(u){const l=typeof u=="string"?this.fromString(u,this._encoding):u;this._chunks.push(l),this._totalLength+=l.byteLength}tryReadHeaders(u=!1){if(this._chunks.length===0)return;let l=0,c=0,f=0,d=0;e:for(;c<this._chunks.length;){const T=this._chunks[c];for(f=0;f<T.length;){switch(T[f]){case e:switch(l){case 0:l=1;break;case 2:l=3;break;default:l=0}break;case r:switch(l){case 1:l=2;break;case 3:l=4,f++;break e;default:l=0}break;default:l=0}f++}d+=T.byteLength,c++}if(l!==4)return;const p=this._read(d+f),y=new Map,h=this.toString(p,"ascii").split(n);if(h.length<2)return y;for(let T=0;T<h.length-2;T++){const C=h[T],v=C.indexOf(":");if(v===-1)throw new Error(`Message header must separate key and value using ':'
${C}`);const w=C.substr(0,v),b=C.substr(v+1).trim();y.set(u?w.toLowerCase():w,b)}return y}tryReadBody(u){if(!(this._totalLength<u))return this._read(u)}get numberOfBytes(){return this._totalLength}_read(u){if(u===0)return this.emptyBuffer();if(u>this._totalLength)throw new Error("Cannot read so many bytes!");if(this._chunks[0].byteLength===u){const d=this._chunks[0];return this._chunks.shift(),this._totalLength-=u,this.asNative(d)}if(this._chunks[0].byteLength>u){const d=this._chunks[0],p=this.asNative(d,u);return this._chunks[0]=d.slice(u),this._totalLength-=u,p}const l=this.allocNative(u);let c=0,f=0;for(;u>0;){const d=this._chunks[f];if(d.byteLength>u){const p=d.slice(0,u);l.set(p,c),c+=u,this._chunks[f]=d.slice(u),this._totalLength-=u,u-=u}else l.set(d,c),c+=d.byteLength,this._chunks.shift(),this._totalLength-=d.byteLength,u-=d.byteLength}return l}},s(i,"AbstractMessageBuffer"),i);t.AbstractMessageBuffer=a}}),Rk=X({"../../node_modules/.pnpm/[email protected]/node_modules/vscode-jsonrpc/lib/common/connection.js"(t){var k,_;Object.defineProperty(t,"__esModule",{value:!0}),t.createMessageConnection=t.ConnectionOptions=t.MessageStrategy=t.CancellationStrategy=t.CancellationSenderStrategy=t.CancellationReceiverStrategy=t.RequestCancellationReceiverStrategy=t.IdCancellationReceiverStrategy=t.ConnectionStrategy=t.ConnectionError=t.ConnectionErrors=t.LogTraceNotification=t.SetTraceNotification=t.TraceFormat=t.TraceValues=t.Trace=t.NullLogger=t.ProgressType=t.ProgressToken=void 0;var e=Wn(),r=xu(),n=M$(),a=G$(),i=nl(),o=Lf(),u;(function($){$.type=new n.NotificationType("$/cancelRequest")})(u||(u={}));var l;(function($){function I(R){return typeof R=="string"||typeof R=="number"}s(I,"is"),$.is=I})(l||(t.ProgressToken=l={}));var c;(function($){$.type=new n.NotificationType("$/progress")})(c||(c={}));var f=(k=class{constructor(){}},s(k,"ProgressType"),k);t.ProgressType=f;var d;(function($){function I(R){return r.func(R)}s(I,"is"),$.is=I})(d||(d={})),t.NullLogger=Object.freeze({error:s(()=>{},"error"),warn:s(()=>{},"warn"),info:s(()=>{},"info"),log:s(()=>{},"log")});var p;(function($){$[$.Off=0]="Off",$[$.Messages=1]="Messages",$[$.Compact=2]="Compact",$[$.Verbose=3]="Verbose"})(p||(t.Trace=p={}));var y;(function($){$.Off="off",$.Messages="messages",$.Compact="compact",$.Verbose="verbose"})(y||(t.TraceValues=y={})),(function($){function I(A){if(!r.string(A))return $.Off;switch(A=A.toLowerCase(),A){case"off":return $.Off;case"messages":return $.Messages;case"compact":return $.Compact;case"verbose":return $.Verbose;default:return $.Off}}s(I,"fromString"),$.fromString=I;function R(A){switch(A){case $.Off:return"off";case $.Messages:return"messages";case $.Compact:return"compact";case $.Verbose:return"verbose";default:return"off"}}s(R,"toString"),$.toString=R})(p||(t.Trace=p={}));var h;(function($){$.Text="text",$.JSON="json"})(h||(t.TraceFormat=h={})),(function($){function I(R){return r.string(R)?(R=R.toLowerCase(),R==="json"?$.JSON:$.Text):$.Text}s(I,"fromString"),$.fromString=I})(h||(t.TraceFormat=h={}));var T;(function($){$.type=new n.NotificationType("$/setTrace")})(T||(t.SetTraceNotification=T={}));var C;(function($){$.type=new n.NotificationType("$/logTrace")})(C||(t.LogTraceNotification=C={}));var v;(function($){$[$.Closed=1]="Closed",$[$.Disposed=2]="Disposed",$[$.AlreadyListening=3]="AlreadyListening"})(v||(t.ConnectionErrors=v={}));var w=(_=class extends Error{constructor(I,R){super(R),this.code=I,Object.setPrototypeOf(this,_.prototype)}},s(_,"ConnectionError"),_);t.ConnectionError=w;var b;(function($){function I(R){const A=R;return A&&r.func(A.cancelUndispatched)}s(I,"is"),$.is=I})(b||(t.ConnectionStrategy=b={}));var N;(function($){function I(R){const A=R;return A&&(A.kind===void 0||A.kind==="id")&&r.func(A.createCancellationTokenSource)&&(A.dispose===void 0||r.func(A.dispose))}s(I,"is"),$.is=I})(N||(t.IdCancellationReceiverStrategy=N={}));var B;(function($){function I(R){const A=R;return A&&A.kind==="request"&&r.func(A.createCancellationTokenSource)&&(A.dispose===void 0||r.func(A.dispose))}s(I,"is"),$.is=I})(B||(t.RequestCancellationReceiverStrategy=B={}));var ne;(function($){$.Message=Object.freeze({createCancellationTokenSource(R){return new o.CancellationTokenSource}});function I(R){return N.is(R)||B.is(R)}s(I,"is"),$.is=I})(ne||(t.CancellationReceiverStrategy=ne={}));var J;(function($){$.Message=Object.freeze({sendCancellation(R,A){return R.sendNotification(u.type,{id:A})},cleanup(R){}});function I(R){const A=R;return A&&r.func(A.sendCancellation)&&r.func(A.cleanup)}s(I,"is"),$.is=I})(J||(t.CancellationSenderStrategy=J={}));var he;(function($){$.Message=Object.freeze({receiver:ne.Message,sender:J.Message});function I(R){const A=R;return A&&ne.is(A.receiver)&&J.is(A.sender)}s(I,"is"),$.is=I})(he||(t.CancellationStrategy=he={}));var Ae;(function($){function I(R){const A=R;return A&&r.func(A.handleMessage)}s(I,"is"),$.is=I})(Ae||(t.MessageStrategy=Ae={}));var ye;(function($){function I(R){const A=R;ret Line truncated
${JSON.stringify(g.error,void 0,4)}`):S.error("Received response message without id. No further error information provided.");else{const P=g.id,D=Ee.get(P);if(wl(g,D),D!==void 0){Ee.delete(P);try{if(g.error){const F=g.error;D.reject(new n.ResponseError(F.code,F.message,F.data))}else if(g.result!==void 0)D.resolve(g.result);else throw new Error("Should never happen.")}catch(F){F.message?S.error(`Response handler '${D.method}' failed with message: ${F.message}`):S.error(`Response handler '${D.method}' failed unexpectedly.`)}}}}s(Al,"handleResponse");function El(g){if(er())return;let P,D;if(g.method===u.type.method){const F=g.params.id;Ve.delete(F),sn(g);return}else{const F=Z.get(g.method);F&&(D=F.handler,P=F.type)}if(D||q)try{if(sn(g),D)if(g.params===void 0)P!==void 0&&P.numberOfParams!==0&&P.parameterStructures!==n.ParameterStructures.byName&&S.error(`Notification ${g.method} defines ${P.numberOfParams} params but received none.`),D();else if(Array.isArray(g.params)){const F=g.params;g.method===c.type.method&&F.length===2&&l.is(F[0])?D({token:F[0],value:F[1]}):(P!==void 0&&(P.parameterStructures===n.ParameterStructures.byName&&S.error(`Notification ${g.method} defines parameters by name but received parameters by position`),P.numberOfParams!==g.params.length&&S.error(`Notification ${g.method} defines ${P.numberOfParams} params but received ${F.length} arguments`)),D(...F))}else P!==void 0&&P.parameterStructures===n.ParameterStructures.byPosition&&S.error(`Notification ${g.method} defines parameters by position but received parameters by name`),D(g.params);else q&&q(g.method,g.params)}catch(F){F.message?S.error(`Notification handler '${g.method}' failed with message: ${F.message}`):S.error(`Notification handler '${g.method}' failed unexpectedly.`)}else je.fire(g)}s(El,"handleNotification");function Cl(g){if(!g){S.error("Received empty message.");return}S.error(`Received message which is neither a response nor a notification message:
${JSON.stringify(g,null,4)}`);const P=g;if(r.string(P.id)||r.number(P.id)){const D=P.id,F=Ee.get(D);F&&F.reject(new Error("The received response has neither a result nor an error property."))}}s(Cl,"handleInvalidMessage");function xt(g){if(g!=null)switch(ee){case p.Verbose:return JSON.stringify(g,null,4);case p.Compact:return JSON.stringify(g);default:return}}s(xt,"stringifyTrace");function bl(g){if(!(ee===p.Off||!ce))if(Qe===h.Text){let P;(ee===p.Verbose||ee===p.Compact)&&g.params&&(P=`Params: ${xt(g.params)}
`),ce.log(`Sending request '${g.method} - (${g.id})'.`,P)}else tr("send-request",g)}s(bl,"traceSendingRequest");function _l(g){if(!(ee===p.Off||!ce))if(Qe===h.Text){let P;(ee===p.Verbose||ee===p.Compact)&&(g.params?P=`Params: ${xt(g.params)}
`:P=`No parameters provided.
`),ce.log(`Sending notification '${g.method}'.`,P)}else tr("send-notification",g)}s(_l,"traceSendingNotification");function jr(g,P,D){if(!(ee===p.Off||!ce))if(Qe===h.Text){let F;(ee===p.Verbose||ee===p.Compact)&&(g.error&&g.error.data?F=`Error data: ${xt(g.error.data)}
`:g.result?F=`Result: ${xt(g.result)}
`:g.error===void 0&&(F=`No result returned.
`)),ce.log(`Sending response '${P} - (${g.id})'. Processing request took ${Date.now()-D}ms`,F)}else tr("send-response",g)}s(jr,"traceSendingResponse");function Sl(g){if(!(ee===p.Off||!ce))if(Qe===h.Text){let P;(ee===p.Verbose||ee===p.Compact)&&g.params&&(P=`Params: ${xt(g.params)}
`),ce.log(`Received request '${g.method} - (${g.id})'.`,P)}else tr("receive-request",g)}s(Sl,"traceReceivedRequest");function sn(g){if(!(ee===p.Off||!ce||g.method===C.type.method))if(Qe===h.Text){let P;(ee===p.Verbose||ee===p.Compact)&&(g.params?P=`Params: ${xt(g.params)}
`:P=`No parameters provided.
`),ce.log(`Received notification '${g.method}'.`,P)}else tr("receive-notification",g)}s(sn,"traceReceivedNotification");function wl(g,P){if(!(ee===p.Off||!ce))if(Qe===h.Text){let D;if((ee===p.Verbose||ee===p.Compact)&&(g.error&&g.error.data?D=`Error data: ${xt(g.error.data)}
`:g.result?D=`Result: ${xt(g.result)}
`:g.error===void 0&&(D=`No result returned.
`)),P){const F=g.error?` Request failed: ${g.error.message} (${g.error.code}).`:"";ce.log(`Received response '${P.method} - (${g.id})' in ${Date.now()-P.timerStart}ms.${F}`,D)}else ce.log(`Received response ${g.id} without active response promise.`,D)}else tr("receive-response",g)}s(wl,"traceReceivedResponse");function tr(g,P){if(!ce||ee===p.Off)return;const D={isLSPMessage:!0,type:g,message:P,timestamp:Date.now()};ce.log(D)}s(tr,"logLSPMessage");function Tr(){if(ma())throw new w(v.Closed,"Connection is closed.");if(er())throw new w(v.Disposed,"Connection is disposed.")}s(Tr,"throwIfClosedOrDisposed");function Il(){if(pa())throw new w(v.AlreadyListening,"Connection is already listening")}s(Il,"throwIfListening");function Nl(){if(!pa())throw new Error("Call listen() first.")}s(Nl,"throwIfNotListening");function m(g){return g===void 0?null:g}s(m,"undefinedToNull");function le(g){if(g!==null)return g}s(le,"nullToUndefined");function we(g){return g!=null&&!Array.isArray(g)&&typeof g=="object"}s(we,"isNamedParam");function H(g,P){switch(g){case n.ParameterStructures.auto:return we(P)?le(P):[m(P)];case n.ParameterStructures.byName:if(!we(P))throw new Error("Received parameters by name but param is not an object literal.");return le(P);case n.ParameterStructures.byPosition:return[m(P)];default:throw new Error(`Unknown parameter structure ${g.toString()}`)}}s(H,"computeSingleParam");function Ie(g,P){let D;const F=g.numberOfParams;switch(F){case 0:D=void 0;break;case 1:D=H(g.parameterStructures,P[0]);break;default:D=[];for(let Ce=0;Ce<P.length&&Ce<F;Ce++)D.push(m(P[Ce]));if(P.length<F)for(let Ce=P.length;Ce<F;Ce++)D.push(null);break}return D}s(Ie,"computeMessageParams");const va={sendNotification:s((g,...P)=>{Tr();let D,F;if(r.string(g)){D=g;const me=P[0];let De=0,Ge=n.ParameterStructures.auto;n.ParameterStructures.is(me)&&(De=1,Ge=me);let ie=P.length;const Ne=ie-De;switch(Ne){case 0:F=void 0;break;case 1:F=H(Ge,P[De]);break;default:if(Ge===n.ParameterStructures.byName)throw new Error(`Received ${Ne} parameters for 'by Name' notification parameter structure.`);F=P.slice(De,ie).map(fe=>m(fe));break}}else{const me=P;D=g.method,F=Ie(g,me)}const Ce={jsonrpc:z,method:D,params:F};return _l(Ce),I.write(Ce).catch(me=>{throw S.error("Sending notification failed."),me})},"sendNotification"),onNotification:s((g,P)=>{Tr();let D;return r.func(g)?q=g:P&&(r.string(g)?(D=g,Z.set(g,{type:void 0,handler:P})):(D=g.method,Z.set(g.method,{type:g,handler:P}))),{dispose:s(()=>{D!==void 0?Z.delete(D):q=void 0},"dispose")}},"onNotification"),onProgress:s((g,P,D)=>{if(ae.has(P))throw new Error(`Progress handler for token ${P} already registered`);return ae.set(P,D),{dispose:s(()=>{ae.delete(P)},"dispose")}},"onProgress"),sendProgress:s((g,P,D)=>va.sendNotification(c.type,{token:P,value:D}),"sendProgress"),onUnhandledProgress:Qt.event,sendRequest:s((g,...P)=>{Tr(),Nl();let D,F,Ce;if(r.string(g)){D=g;const ie=P[0],Ne=P[P.length-1];let fe=0,Ue=n.ParameterStructures.auto;n.ParameterStructures.is(ie)&&(fe=1,Ue=ie);let lt=P.length;o.CancellationToken.is(Ne)&&(lt=lt-1,Ce=Ne);const rr=lt-fe;switch(rr){case 0:F=void 0;break;case 1:F=H(Ue,P[fe]);break;default:if(Ue===n.ParameterStructures.byName)throw new Error(`Received ${rr} parameters for 'by Name' request parameter structure.`);F=P.slice(fe,lt).map(ck=>m(ck));break}}else{const ie=P;D=g.method,F=Ie(g,ie);const Ne=g.numberOfParams;Ce=o.CancellationToken.is(ie[Ne])?ie[Ne]:void 0}const me=L++;let De;Ce&&(De=Ce.onCancellationRequested(()=>{const ie=ve.sender.sendCancellation(va,me);return ie===void 0?(S.log(`Received no promise from cancellation strategy when cancelling id ${me}`),Promise.resolve()):ie.catch(()=>{S.log(`Sending cancellation messages for id ${me} failed`)})}));const Ge={jsonrpc:z,id:me,method:D,params:F};return bl(Ge),typeof ve.sender.enableCancellation=="function"&&ve.sender.enableCancellation(Ge),new Promise(async(ie,Ne)=>{const fe=s(rr=>{ie(rr),ve.sender.cleanup(me),De?.dispose()},"resolveWithCleanup"),Ue=s(rr=>{Ne(rr),ve.sender.cleanup(me),De?.dispose()},"rejectWithCleanup"),lt={method:D,timerStart:Date.now(),resolve:fe,reject:Ue};try{await I.write(Ge),Ee.set(me,lt)}catch(rr){throw S.error("Sending request failed."),lt.reject(new n.ResponseError(n.ErrorCodes.MessageWriteError,rr.message?rr.message:"Unknown reason")),rr}})},"sendRequest"),onRequest:s((g,P)=>{Tr();let D=null;return d.is(g)?(D=void 0,M=g):r.string(g)?(D=null,P!==void 0&&(D=g,Y.set(g,{handler:P,type:void 0}))):P!==void 0&&(D=g.method,Y.set(g.method,{type:g,handler:P})),{dispose:s(()=>{D!==null&&(D!==void 0?Y.delete(D):M=void 0)},"dispose")}},"onRequest"),hasPendingResponse:s(()=>Ee.size>0,"hasPendingResponse"),trace:s(async(g,P,D)=>{let F=!1,Ce=h.Text;D!==void 0&&(r.boolean(D)?F=D:(F=D.sendNotification||!1,Ce=D.traceFormat||h.Text)),ee=g,Qe=Ce,ee===p.Off?ce=void 0:ce=P,F&&!ma()&&!er()&&await va.sendNotification(T.type,{value:p.toString(g)})},"trace"),onError:ge.event,onClose:G.event,onUnhandledNotification:je.event,onD Line truncated
`),W("\r"),W(" "),W("\v"),W(" "),W(" "),W(" "),W(" "),W(" "),W(" "),W(" "),W(" "),W(" "),W(" "),W(" "),W(" "),W(" "),W(" "),W("\u2028"),W("\u2029"),W(" "),W(" "),W(" "),W("\uFEFF")],Yk=/[0-9a-fA-F]/,nc=/[0-9]/,Xk=/[1-9]/,ri,mR=(ri=class{constructor(){this.idx=0,this.input="",this.groupIdx=0}saveState(){return{idx:this.idx,input:this.input,groupIdx:this.groupIdx}}restoreState(e){this.idx=e.idx,this.input=e.input,this.groupIdx=e.groupIdx}pattern(e){this.idx=0,this.input=e,this.groupIdx=0,this.consumeChar("/");const r=this.disjunction();this.consumeChar("/");const n={type:"Flags",loc:{begin:this.idx,end:e.length},global:!1,ignoreCase:!1,multiLine:!1,unicode:!1,sticky:!1};for(;this.isRegExpFlag();)switch(this.popChar()){case"g":Ia(n,"global");break;case"i":Ia(n,"ignoreCase");break;case"m":Ia(n,"multiLine");break;case"u":Ia(n,"unicode");break;case"y":Ia(n,"sticky");break}if(this.idx!==this.input.length)throw Error("Redundant input: "+this.input.substring(this.idx));return{type:"Pattern",flags:n,value:r,loc:this.loc(0)}}disjunction(){const e=[],r=this.idx;for(e.push(this.alternative());this.peekChar()==="|";)this.consumeChar("|"),e.push(this.alternative());return{type:"Disjunction",value:e,loc:this.loc(r)}}alternative(){const e=[],r=this.idx;for(;this.isTerm();)e.push(this.term());return{type:"Alternative",value:e,loc:this.loc(r)}}term(){return this.isAssertion()?this.assertion():this.atom()}assertion(){const e=this.idx;switch(this.popChar()){case"^":return{type:"StartAnchor",loc:this.loc(e)};case"$":return{type:"EndAnchor",loc:this.loc(e)};case"\\":switch(this.popChar()){case"b":return{type:"WordBoundary",loc:this.loc(e)};case"B":return{type:"NonWordBoundary",loc:this.loc(e)}}throw Error("Invalid Assertion Escape");case"(":this.consumeChar("?");let r;switch(this.popChar()){case"=":r="Lookahead";break;case"!":r="NegativeLookahead";break;case"<":{switch(this.popChar()){case"=":r="Lookbehind";break;case"!":r="NegativeLookbehind"}break}}mn(r);const n=this.disjunction();return this.consumeChar(")"),{type:r,value:n,loc:this.loc(e)}}return pR()}quantifier(e=!1){let r;const n=this.idx;switch(this.popChar()){case"*":r={atLeast:0,atMost:1/0};break;case"+":r={atLeast:1,atMost:1/0};break;case"?":r={atLeast:0,atMost:1};break;case"{":const a=this.integerIncludingZero();switch(this.popChar()){case"}":r={atLeast:a,atMost:a};break;case",":let i;this.isDigit()?(i=this.integerIncludingZero(),r={atLeast:a,atMost:i}):r={atLeast:a,atMost:1/0},this.consumeChar("}");break}if(e===!0&&r===void 0)return;mn(r);break}if(!(e===!0&&r===void 0)&&mn(r))return this.peekChar(0)==="?"?(this.consumeChar("?"),r.greedy=!1):r.greedy=!0,r.type="Quantifier",r.loc=this.loc(n),r}atom(){let e;const r=this.idx;switch(this.peekChar()){case".":e=this.dotAll();break;case"\\":e=this.atomEscape();break;case"[":e=this.characterClass();break;case"(":e=this.group();break}if(e===void 0&&this.isPatternCharacter()&&(e=this.patternCharacter()),mn(e))return e.loc=this.loc(r),this.isQuantifier()&&(e.quantifier=this.quantifier()),e}dotAll(){return this.consumeChar("."),{type:"Set",complement:!0,value:[W(`
`),W("\r"),W("\u2028"),W("\u2029")]}}atomEscape(){switch(this.consumeChar("\\"),this.peekChar()){case"1":case"2":case"3":case"4":case"5":case"6":case"7":case"8":case"9":return this.decimalEscapeAtom();case"d":case"D":case"s":case"S":case"w":case"W":return this.characterClassEscape();case"f":case"n":case"r":case"t":case"v":return this.controlEscapeAtom();case"c":return this.controlLetterEscapeAtom();case"0":return this.nulCharacterAtom();case"x":return this.hexEscapeSequenceAtom();case"u":return this.regExpUnicodeEscapeSequenceAtom();default:return this.identityEscapeAtom()}}decimalEscapeAtom(){return{type:"GroupBackReference",value:this.positiveInteger()}}characterClassEscape(){let e,r=!1;switch(this.popChar()){case"d":e=lf;break;case"D":e=lf,r=!0;break;case"s":e=sv;break;case"S":e=sv,r=!0;break;case"w":e=uf;break;case"W":e=uf,r=!0;break}if(mn(e))return{type:"Set",value:e,complement:r}}controlEscapeAtom(){let e;switch(this.popChar()){case"f":e=W("\f");break;case"n":e=W(`
`);break;case"r":e=W("\r");break;case"t":e=W(" ");break;case"v":e=W("\v");break}if(mn(e))return{type:"Character",value:e}}controlLetterEscapeAtom(){this.consumeChar("c");const e=this.popChar();if(/[a-zA-Z]/.test(e)===!1)throw Error("Invalid ");return{type:"Character",value:e.toUpperCase().charCodeAt(0)-64}}nulCharacterAtom(){return this.consumeChar("0"),{type:"Character",value:W("\0")}}hexEscapeSequenceAtom(){return this.consumeChar("x"),this.parseHexDigits(2)}regExpUnicodeEscapeSequenceAtom(){return this.consumeChar("u"),this.parseHexDigits(4)}identityEscapeAtom(){const e=this.popChar();return{type:"Character",value:W(e)}}classPatternCharacterAtom(){switch(this.peekChar()){case`
`:case"\r":case"\u2028":case"\u2029":case"\\":case"]":throw Error("TBD");default:const e=this.popChar();return{type:"Character",value:W(e)}}}characterClass(){const e=[];let r=!1;for(this.consumeChar("["),this.peekChar(0)==="^"&&(this.consumeChar("^"),r=!0);this.isClassAtom();){const n=this.classAtom();if(n.type,Rm(n)&&this.isRangeDash()){this.consumeChar("-");const a=this.classAtom();if(a.type,Rm(a)){if(a.value<n.value)throw Error("Range out of order in character class");e.push({from:n.value,to:a.value})}else wc(n.value,e),e.push(W("-")),wc(a.value,e)}else wc(n.value,e)}return this.consumeChar("]"),{type:"Set",complement:r,value:e}}classAtom(){switch(this.peekChar()){case"]":case`
`:case"\r":case"\u2028":case"\u2029":throw Error("TBD");case"\\":return this.classEscape();default:return this.classPatternCharacterAtom()}}classEscape(){switch(this.consumeChar("\\"),this.peekChar()){case"b":return this.consumeChar("b"),{type:"Character",value:W("\b")};case"d":case"D":case"s":case"S":case"w":case"W":return this.characterClassEscape();case"f":case"n":case"r":case"t":case"v":return this.controlEscapeAtom();case"c":return this.controlLetterEscapeAtom();case"0":return this.nulCharacterAtom();case"x":return this.hexEscapeSequenceAtom();case"u":return this.regExpUnicodeEscapeSequenceAtom();default:return this.identityEscapeAtom()}}group(){let e=!0;this.consumeChar("("),this.peekChar(0)==="?"?(this.consumeChar("?"),this.consumeChar(":"),e=!1):this.groupIdx++;const r=this.disjunction();this.consumeChar(")");const n={type:"Group",capturing:e,value:r};return e&&(n.idx=this.groupIdx),n}positiveInteger(){let e=this.popChar();if(Xk.test(e)===!1)throw Error("Expecting a positive integer");for(;nc.test(this.peekChar(0));)e+=this.popChar();return parseInt(e,10)}integerIncludingZero(){let e=this.popChar();if(nc.test(e)===!1)throw Error("Expecting an integer");for(;nc.test(this.peekChar(0));)e+=this.popChar();return parseInt(e,10)}patternCharacter(){const e=this.popChar();switch(e){case`
`:case"\r":case"\u2028":case"\u2029":case"^":case"$":case"\\":case".":case"*":case"+":case"?":case"(":case")":case"[":case"|":throw Error("TBD");default:return{type:"Character",value:W(e)}}}isRegExpFlag(){switch(this.peekChar(0)){case"g":case"i":case"m":case"u":case"y":return!0;default:return!1}}isRangeDash(){return this.peekChar()==="-"&&this.isClassAtom(1)}isDigit(){return nc.test(this.peekChar(0))}isClassAtom(e=0){switch(this.peekChar(e)){case"]":case`
`:case"\r":case"\u2028":case"\u2029":return!1;default:return!0}}isTerm(){return this.isAtom()||this.isAssertion()}isAtom(){if(this.isPatternCharacter())return!0;switch(this.peekChar(0)){case".":case"\\":case"[":case"(":return!0;default:return!1}}isAssertion(){switch(this.peekChar(0)){case"^":case"$":return!0;case"\\":switch(this.peekChar(1)){case"b":case"B":return!0;default:return!1}case"(":return this.peekChar(1)==="?"&&(this.peekChar(2)==="="||this.peekChar(2)==="!"||this.peekChar(2)==="<"&&(this.peekChar(3)==="="||this.peekChar(3)==="!"));default:return!1}}isQuantifier(){const e=this.saveState();try{return this.quantifier(!0)!==void 0}catch{return!1}finally{this.restoreState(e)}}isPatternCharacter(){switch(this.peekChar()){case"^":case"$":case"\\":case".":case"*":case"+":case"?":case"(":case")":case"[":case"|":case"/":case`
`:case"\r":case"\u2028":case"\u2029":return!1;default:return!0}}parseHexDigits(e){let r="";for(let a=0;a<e;a++){const i=this.popChar();if(Yk.test(i)===!1)throw Error("Expecting a HexDecimal digits");r+=i}return{type:"Character",value:parseInt(r,16)}}peekChar(e=0){return this.input[this.idx+e]}popChar(){const e=this.peekChar(0);return this.consumeChar(void 0),e}consumeChar(e){if(e!==void 0&&this.input[this.idx]!==e)throw Error("Expected: '"+e+"' but found: '"+this.input[this.idx]+"' at offset: "+this.idx);if(this.idx>=this.input.length)throw Error("Unexpected end of input");this.idx++}loc(e){return{begin:e,end:this.idx}}},s(ri,"RegExpParser"),ri),ni,Kf=(ni=class{visitChildren(e){for(const r in e){const n=e[r];e.hasOwnProperty(r)&&(n.type!==void 0?this.visit(n):Array.isArray(n)&&n.forEach(a=>{this.visit(a)},this))}}visit(e){switch(e.type){case"Pattern":this.visitPattern(e);break;case"Flags":this.visitFlags(e);break;case"Disjunction":this.visitDisjunction(e);break;case"Alternative":this.visitAlternative(e);break;case"StartAnchor":this.visitStartAnchor(e);break;case"EndAnchor":this.visitEndAnchor(e);break;case"WordBoundary":this.visitWordBoundary(e);break;case"NonWordBoundary":this.visitNonWordBoundary(e);break;case"Lookahead":this.visitLookahead(e);break;case"NegativeLookahead":this.visitNegativeLookahead(e);break;case"Lookbehind":this.visitLookbehind(e);break;case"NegativeLookbehind":this.visitNegativeLookbehind(e);break;case"Character":this.visitCharacter(e);break;case"Set":this.visitSet(e);break;case"Group":this.visitGroup(e);break;case"GroupBackReference":this.visitGroupBackReference(e);break;case"Quantifier":this.visitQuantifier(e);break}this.visitChildren(e)}visitPattern(e){}visitFlags(e){}visitDisjunction(e){}visitAlternative(e){}visitStartAnchor(e){}visitEndAnchor(e){}visitWordBoundary(e){}visitNonWordBoundary(e){}visitLookahead(e){}visitNegativeLookahead(e){}visitLookbehind(e){}visitNegativeLookbehind(e){}visitCharacter(e){}visitSet(e){}visitGroup(e){}visitGroupBackReference(e){}visitQuantifier(e){}},s(ni,"BaseRegExpVisitor"),ni),hR=/\r?\n/gm,yR=new mR,ai,Jk=(ai=class extends Kf{constructor(){super(...arguments),this.isStarting=!0,this.endRegexpStack=[],this.multiline=!1}get endRegex(){return this.endRegexpStack.join("")}reset(e){this.multiline=!1,this.regex=e,this.startRegexp="",this.isStarting=!0,this.endRegexpStack=[]}visitGroup(e){e.quantifier&&(this.isStarting=!1,this.endRegexpStack=[])}visitCharacter(e){const r=String.fromCharCode(e.value);if(!this.multiline&&r===`
`&&(this.multiline=!0),e.quantifier)this.isStarting=!1,this.endRegexpStack=[];else{const n=il(r);this.endRegexpStack.push(n),this.isStarting&&(this.startRegexp+=n)}}visitSet(e){if(!this.multiline){const r=this.regex.substring(e.loc.begin,e.loc.end),n=new RegExp(r);this.multiline=!!`
`.match(n)}if(e.quantifier)this.isStarting=!1,this.endRegexpStack=[];else{const r=this.regex.substring(e.loc.begin,e.loc.end);this.endRegexpStack.push(r),this.isStarting&&(this.startRegexp+=r)}}visitChildren(e){e.type==="Group"&&e.quantifier||super.visitChildren(e)}},s(ai,"TerminalRegExpVisitor"),ai),On=new Jk;function gR(t){try{typeof t!="string"&&(t=t.source),t=`/${t}/`;const e=yR.pattern(t),r=[];for(const n of e.value.value)On.reset(t),On.visit(n),r.push({start:On.startRegexp,end:On.endRegex});return r}catch{return[]}}s(gR,"getTerminalParts");function iy(t){try{return typeof t=="string"&&(t=new RegExp(t)),t=t.toString(),On.reset(t),On.visit(yR.pattern(t)),On.multiline}catch{return!1}}s(iy,"isMultilineComment");var vR=`\f
\r \v              \u2028\u2029   \uFEFF`.split("");function Wf(t){const e=typeof t=="string"?new RegExp(t):t;return vR.some(r=>e.test(r))}s(Wf,"isWhitespace");function il(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}s(il,"escapeRegExp");function sy(t,e){const r=oy(t),n=e.match(r);return!!n&&n[0].length>0}s(sy,"partialMatches");function oy(t){typeof t=="string"&&(t=new RegExp(t));const e=t,r=t.source;let n=0;function a(){let i="",o;function u(c){i+=r.substr(n,c),n+=c}s(u,"appendRaw");function l(c){i+="(?:"+r.substr(n,c)+"|$)",n+=c}for(s(l,"appendOptional");n<r.length;)switch(r[n]){case"\\":switch(r[n+1]){case"c":l(3);break;case"x":l(4);break;case"u":e.unicode?r[n+2]==="{"?l(r.indexOf("}",n)-n+1):l(6):l(2);break;case"p":case"P":e.unicode?l(r.indexOf("}",n)-n+1):l(2);break;case"k":l(r.indexOf(">",n)-n+1);break;default:l(2);break}break;case"[":o=/\[(?:\\.|.)*?\]/g,o.lastIndex=n,o=o.exec(r)||[],l(o[0].length);break;case"|":case"^":case"$":case"*":case"+":case"?":u(1);break;case"{":o=/\{\d+,?\d*\}/g,o.lastIndex=n,o=o.exec(r),o?u(o[0].length):l(1);break;case"(":if(r[n+1]==="?")switch(r[n+2]){case":":i+="(?:",n+=3,i+=a()+"|$)";break;case"=":i+="(?=",n+=3,i+=a()+")";break;case"!":o=n,n+=3,a(),i+=r.substr(o,n-o);break;case"<":switch(r[n+3]){case"=":case"!":o=n,n+=4,a(),i+=r.substr(o,n-o);break;default:u(r.indexOf(">",n)-n+1),i+=a()+"|$)";break}break}else u(1),i+=a()+"|$)";break;case")":return++n,i;default:l(1);break}return i}return s(a,"process"),new RegExp(a(),t.flags)}s(oy,"partialRegExp");function ly(t){return t.rules.find(e=>mt(e)&&e.entry)}s(ly,"getEntryRule");function uy(t){return t.rules.filter(e=>jt(e)&&e.hidden)}s(uy,"getHiddenRules");function Vf(t,e){const r=new Set,n=ly(t);if(!n)return new Set(t.rules);const a=[n].concat(uy(t));for(const o of a)cy(o,r,e);const i=new Set;for(const o of t.rules)(r.has(o.name)||jt(o)&&o.hidden)&&i.add(o);return i}s(Vf,"getAllReachableRules");function cy(t,e,r){e.add(t.name),Mr(t).forEach(n=>{if(Nr(n)||r&&Ff(n)){const a=n.rule.ref;a&&!e.has(a.name)&&cy(a,e,r)}})}s(cy,"ruleDfs");function TR(t){const e=new Set;return Mr(t).forEach(r=>{Yn(r)&&(mt(r.type.ref)&&e.add(r.type.ref),Gu(r.type.ref)&&mt(r.type.ref.$container)&&e.add(r.type.ref.$container))}),e}s(TR,"getAllRulesUsedForCrossReferences");function fy(t){if(t.terminal)return t.terminal;if(t.type.ref)return Xf(t.type.ref)?.terminal}s(fy,"getCrossReferenceTerminal");function dy(t){return t.hidden&&!Wf(ju(t))}s(dy,"isCommentTerminal");function py(t,e){return!t||!e?[]:Hf(t,e,t.astNode,!0)}s(py,"findNodesForProperty");function qf(t,e,r){if(!t||!e)return;const n=Hf(t,e,t.astNode,!0);if(n.length!==0)return r!==void 0?r=Math.max(0,Math.min(r,n.length-1)):r=0,n[r]}s(qf,"findNodeForProperty");function Hf(t,e,r,n){if(!n){const a=qn(t.grammarSource,wr);if(a&&a.feature===e)return[t]}return Sr(t)&&t.astNode===r?t.content.flatMap(a=>Hf(a,e,r,!1)):[]}s(Hf,"findNodesForPropertyInternal");function $R(t,e){return t?Yf(t,e,t?.astNode):[]}s($R,"findNodesForKeyword");function my(t,e,r){if(!t)return;const n=Yf(t,e,t?.astNode);if(n.length!==0)return r!==void 0?r=Math.max(0,Math.min(r,n.length-1)):r=0,n[r]}s(my,"findNodeForKeyword");function Yf(t,e,r){if(t.astNode!==r)return[];if(Ir(t.grammarSource)&&t.grammarSource.value===e)return[t];const n=Jo(t).iterator();let a;const i=[];do if(a=n.next(),!a.done){const o=a.value;o.astNode===r?Ir(o.grammarSource)&&o.grammarSource.value===e&&i.push(o):n.prune()}while(!a.done);return i}s(Yf,"findNodesForKeywordInternal");function hy(t){const e=t.astNode;for(;e===t.container?.astNode;){const r=qn(t.grammarSource,wr);if(r)return r;t=t.container}}s(hy,"findAssignment");function Xf(t){let e=t;return Gu(e)&&(Yr(e.$container)?e=e.$container.$container:Hn(e.$container)?e=e.$container:tn(e.$container)),yy(t,e,new Map)}s(Xf,"findNameAssignment");function yy(t,e,r){function n(a,i){let o;return qn(a,wr)||(o=yy(i,i,r)),r.set(t,o),o}if(s(n,"go"),r.has(t))return r.get(t);r.set(t,void 0);for(const a of Mr(e)){if(wr(a)&&a.feature.toLowerCase()==="name")return r.set(t,a),a;if(Nr(a)&&mt(a.rule.ref))return n(a,a.rule.ref);if(Gf(a)&&a.typeRef?.ref)return n(a,a.typeRef.ref)}}s(yy,"findNameAssignmentInternal");function gy(t){const e=t.$container;if(Xn(e)){const r=e.elements,n=r.indexOf(t);for(let a=n-1;a>=0;a--){const i=r[a];if(Yr(i))return i;{const o=Mr(r[a]).find(Yr);if(o)return o}}}if(xf(e))return gy(e)}s(gy,"getActionAtElement");function RR(t,e){return t==="?"||t==="*"||Xn(e)&&!!e.guardCondition}s(RR,"isOptionalCardinality");function AR(t){return t==="*"||t==="+"}s(AR,"isArrayCardinality");function ER(t){return t==="+="}s(ER,"isArrayOperator");function Fu(t){return vy(t,new Set)}s(Fu,"isDataTypeRule");function vy(t,e){if(e.has(t))return!0;e.add(t);for(const r of Mr(t))if(Nr(r)){if(!r.rule.ref||mt(r.rule.ref)&&!vy(r.rule.ref,e)||Xo(r.rule.ref))return!1}else{if(wr(r))return!1;if(Yr(r))return!1}return!!t.definition}s(vy,"isDataTypeRuleInternal");function CR(t){return cf(t.type,new Set)}s(CR,"isDat Line truncated
`;function Fb(t,e=!1){try{const r=Ju(t);return hf(r.value,{},r.flags.ignoreCase)}catch(r){if(r.message===Gb)e&&Uy(`${mf} Unable to optimize: < ${t.toString()} >
Complement Sets cannot be automatically optimized.
This will disable the lexer's first char optimizations.
See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#COMPLEMENT for details.`);else{let n="";e&&(n=`
This will disable the lexer's first char optimizations.
See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#REGEXP_PARSING for details.`),pf(`${mf}
Failed parsing: < ${t.toString()} >
Using the @chevrotain/regexp-to-ast library
Please open an issue at: https://github.com/chevrotain/chevrotain/issues`+n)}}return[]}s(Fb,"getOptimizedStartCodesIndices");function hf(t,e,r){switch(t.type){case"Disjunction":for(let a=0;a<t.value.length;a++)hf(t.value[a],e,r);break;case"Alternative":const n=t.value;for(let a=0;a<n.length;a++){const i=n[a];switch(i.type){case"EndAnchor":case"GroupBackReference":case"Lookahead":case"NegativeLookahead":case"Lookbehind":case"NegativeLookbehind":case"StartAnchor":case"WordBoundary":case"NonWordBoundary":continue}const o=i;switch(o.type){case"Character":Xl(o.value,e,r);break;case"Set":if(o.complement===!0)throw Error(Gb);V(o.value,l=>{if(typeof l=="number")Xl(l,e,r);else{const c=l;if(r===!0)for(let f=c.from;f<=c.to;f++)Xl(f,e,r);else{for(let f=c.from;f<=c.to&&f<Zl;f++)Xl(f,e,r);if(c.to>=Zl){const f=c.from>=Zl?c.from:Zl,d=c.to,p=Or(f),y=Or(d);for(let h=p;h<=y;h++)e[h]=h}}}});break;case"Group":hf(o.value,e,r);break;default:throw Error("Non Exhaustive Match")}const u=o.quantifier!==void 0&&o.quantifier.atLeast===0;if(o.type==="Group"&&yf(o)===!1||o.type!=="Group"&&u===!1)break}break;default:throw Error("non exhaustive match!")}return Ke(e)}s(hf,"firstCharOptimizedIndices");function Xl(t,e,r){const n=Or(t);e[n]=n,r===!0&&zb(t,e)}s(Xl,"addOptimizedIdxToResult");function zb(t,e){const r=String.fromCharCode(t),n=r.toUpperCase();if(n!==r){const a=Or(n.charCodeAt(0));e[a]=a}else{const a=r.toLowerCase();if(a!==r){const i=Or(a.charCodeAt(0));e[i]=i}}}s(zb,"handleIgnoreCase");function Sm(t,e){return el(t.value,r=>{if(typeof r=="number")return vt(e,r);{const n=r;return el(e,a=>n.from<=a&&a<=n.to)!==void 0}})}s(Sm,"findCode");function yf(t){const e=t.quantifier;return e&&e.atLeast===0?!0:t.value?se(t.value)?Yt(t.value,yf):yf(t.value):!1}s(yf,"isWholeOptional");var Ti,v1=(Ti=class extends Kf{constructor(e){super(),this.targetCharCodes=e,this.found=!1}visitChildren(e){if(this.found!==!0){switch(e.type){case"Lookahead":this.visitLookahead(e);return;case"NegativeLookahead":this.visitNegativeLookahead(e);return;case"Lookbehind":this.visitLookbehind(e);return;case"NegativeLookbehind":this.visitNegativeLookbehind(e);return}super.visitChildren(e)}}visitCharacter(e){vt(this.targetCharCodes,e.value)&&(this.found=!0)}visitSet(e){e.complement?Sm(e,this.targetCharCodes)===void 0&&(this.found=!0):Sm(e,this.targetCharCodes)!==void 0&&(this.found=!0)}},s(Ti,"CharCodeFinder"),Ti);function dd(t,e){if(e instanceof RegExp){const r=Ju(e),n=new v1(t);return n.visit(r),n.found}else return el(e,r=>vt(t,r.charCodeAt(0)))!==void 0}s(dd,"canMatchCharCode");var Un="PATTERN",Jl="defaultMode",ic="modes";function jb(t,e){e=jy(e,{debug:!1,safeMode:!1,positionTracking:"full",lineTerminatorCharacters:["\r",`
`],tracer:s((w,b)=>b(),"tracer")});const r=e.tracer;r("initCharCodeToOptimizedIndexMap",()=>{l_()});let n;r("Reject Lexer.NA",()=>{n=cd(t,w=>w[Un]===pt.NA)});let a=!1,i;r("Transform Patterns",()=>{a=!1,i=j(n,w=>{const b=w[Un];if(Pr(b)){const N=b.source;return N.length===1&&N!=="^"&&N!=="$"&&N!=="."&&!b.ignoreCase?N:N.length===2&&N[0]==="\\"&&!vt(["d","D","s","S","t","r","n","t","0","c","b","B","f","v","w","W"],N[1])?N[1]:wm(b)}else{if(Gr(b))return a=!0,{exec:b};if(typeof b=="object")return a=!0,b;if(typeof b=="string"){if(b.length===1)return b;{const N=b.replace(/[\\^$.*+?()[\]{}|]/g,"\\$&"),B=new RegExp(N);return wm(B)}}else throw Error("non exhaustive match")}})});let o,u,l,c,f;r("misc mapping",()=>{o=j(n,w=>w.tokenTypeIdx),u=j(n,w=>{const b=w.GROUP;if(b!==pt.SKIPPED){if(Ct(b))return b;if(kr(b))return!1;throw Error("non exhaustive match")}}),l=j(n,w=>{const b=w.LONGER_ALT;if(b)return se(b)?j(b,B=>Fv(n,B)):[Fv(n,b)]}),c=j(n,w=>w.PUSH_MODE),f=j(n,w=>K(w,"POP_MODE"))});let d;r("Line Terminator Handling",()=>{const w=Hy(e.lineTerminatorCharacters);d=j(n,b=>!1),e.positionTracking!=="onlyOffset"&&(d=j(n,b=>K(b,"LINE_BREAKS")?!!b.LINE_BREAKS:qy(b,w)===!1&&dd(w,b.PATTERN)))});let p,y,h,T;r("Misc Mapping #2",()=>{p=j(n,Vy),y=j(i,s_),h=kt(n,(w,b)=>{const N=b.GROUP;return Ct(N)&&N!==pt.SKIPPED&&(w[N]=[]),w},{}),T=j(i,(w,b)=>({pattern:i[b],longerAlt:l[b],canLineTerminator:d[b],isCustom:p[b],short:y[b],group:u[b],push:c[b],pop:f[b],tokenTypeIdx:o[b],tokenType:n[b]}))});let C=!0,v=[];return e.safeMode||r("First Char Optimization",()=>{v=kt(n,(w,b,N)=>{if(typeof b.PATTERN=="string"){const B=b.PATTERN.charCodeAt(0),ne=Or(B);Pc(w,ne,T[N])}else if(se(b.START_CHARS_HINT)){let B;V(b.START_CHARS_HINT,ne=>{const J=typeof ne=="string"?ne.charCodeAt(0):ne,he=Or(J);B!==he&&(B=he,Pc(w,he,T[N]))})}else if(Pr(b.PATTERN))if(b.PATTERN.unicode)C=!1,e.ensureOptimizations&&pf(`${mf} Unable to analyze < ${b.PATTERN.toString()} > pattern.
The regexp unicode flag is not currently supported by the regexp-to-ast library.
This will disable the lexer's first char optimizations.
For details See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#UNICODE_OPTIMIZE`);else{const B=Fb(b.PATTERN,e.ensureOptimizations);Re(B)&&(C=!1),V(B,ne=>{Pc(w,ne,T[N])})}else e.ensureOptimizations&&pf(`${mf} TokenType: <${b.name}> is using a custom token pattern without providing <start_chars_hint> parameter.
This will disable the lexer's first char optimizations.
For details See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#CUSTOM_OPTIMIZE`),C=!1;return w},[])}),{emptyGroups:h,patternIdxToConfig:T,charCodeToPatternIdxToConfig:v,hasCustom:a,canBeOptimized:C}}s(jb,"analyzeTokenTypes");function Bb(t,e){let r=[];const n=Kb(t);r=r.concat(n.errors);const a=Wb(n.valid),i=a.valid;return r=r.concat(a.errors),r=r.concat(Ub(i)),r=r.concat(Jb(i)),r=r.concat(Zb(i,e)),r=r.concat(Qb(i)),r}s(Bb,"validatePatterns");function Ub(t){let e=[];const r=Bt(t,n=>Pr(n[Un]));return e=e.concat(Vb(r)),e=e.concat(Hb(r)),e=e.concat(Yb(r)),e=e.concat(Xb(r)),e=e.concat(qb(r)),e}s(Ub,"validateRegExpPattern");function Kb(t){const e=Bt(t,a=>!K(a,Un)),r=j(e,a=>({message:"Token Type: ->"+a.name+"<- missing static 'PATTERN' property",type:Me.MISSING_PATTERN,tokenTypes:[a]})),n=ud(t,e);return{errors:r,valid:n}}s(Kb,"findMissingPatterns");function Wb(t){const e=Bt(t,a=>{const i=a[Un];return!Pr(i)&&!Gr(i)&&!K(i,"exec")&&!Ct(i)}),r=j(e,a=>({message:"Token Type: ->"+a.name+"<- static 'PATTERN' can only be a RegExp, a Function matching the {CustomPatternMatcherFunc} type or an Object matching the {ICustomPattern} interface.",type:Me.INVALID_PATTERN,tokenTypes:[a]})),n=ud(t,e);return{errors:r,valid:n}}s(Wb,"findInvalidPatterns");var T1=/[^\\][$]/;function Vb(t){const a=class a extends Kf{constructor(){super(...arguments),this.found=!1}visitEndAnchor(o){this.found=!0}};s(a,"EndAnchorFinder");let e=a;const r=Bt(t,i=>{const o=i.PATTERN;try{const u=Ju(o),l=new e;return l.visit(u),l.found}catch{return T1.test(o.source)}});return j(r,i=>({message:`Unexpected RegExp Anchor Error:
Token Type: ->`+i.name+`<- static 'PATTERN' cannot contain end of input anchor '$'
See chevrotain.io/docs/guide/resolving_lexer_errors.html#ANCHORS for details.`,type:Me.EOI_ANCHOR_FOUND,tokenTypes:[i]}))}s(Vb,"findEndOfInputAnchor");function qb(t){const e=Bt(t,n=>n.PATTERN.test(""));return j(e,n=>({message:"Token Type: ->"+n.name+"<- static 'PATTERN' must not match an empty string",type:Me.EMPTY_MATCH_PATTERN,tokenTypes:[n]}))}s(qb,"findEmptyMatchRegExps");var $1=/[^\\[][\^]|^\^/;function Hb(t){const a=class a extends Kf{constructor(){super(...arguments),this.found=!1}visitStartAnchor(o){this.found=!0}};s(a,"StartAnchorFinder");let e=a;const r=Bt(t,i=>{const o=i.PATTERN;try{const u=Ju(o),l=new e;return l.visit(u),l.found}catch{return $1.test(o.source)}});return j(r,i=>({message:`Unexpected RegExp Anchor Error:
Token Type: ->`+i.name+`<- static 'PATTERN' cannot contain start of input anchor '^'
See https://chevrotain.io/docs/guide/resolving_lexer_errors.html#ANCHORS for details.`,type:Me.SOI_ANCHOR_FOUND,tokenTypes:[i]}))}s(Hb,"findStartOfInputAnchor");function Yb(t){const e=Bt(t,n=>{const a=n[Un];return a instanceof RegExp&&(a.multiline||a.global)});return j(e,n=>({message:"Token Type: ->"+n.name+"<- static 'PATTERN' may NOT contain global('g') or multiline('m')",type:Me.UNSUPPORTED_FLAGS_FOUND,tokenTypes:[n]}))}s(Yb,"findUnsupportedFlags");function Xb(t){const e=[];let r=j(t,i=>kt(t,(o,u)=>(i.PATTERN.source===u.PATTERN.source&&!vt(e,u)&&u.PATTERN!==pt.NA&&(e.push(u),o.push(u)),o),[]));r=Xu(r);const n=Bt(r,i=>i.length>1);return j(n,i=>{const o=j(i,l=>l.name);return{message:`The same RegExp pattern ->${Jt(i).PATTERN}<-has been used in all of the following Token Types: ${o.join(", ")} <-`,type:Me.DUPLICATE_PATTERNS_FOUND,tokenTypes:i}})}s(Xb,"findDuplicatePatterns");function Jb(t){const e=Bt(t,n=>{if(!K(n,"GROUP"))return!1;const a=n.GROUP;return a!==pt.SKIPPED&&a!==pt.NA&&!Ct(a)});return j(e,n=>({message:"Token Type: ->"+n.name+"<- static 'GROUP' can only be Lexer.SKIPPED/Lexer.NA/A String",type:Me.INVALID_GROUP_TYPE_FOUND,tokenTypes:[n]}))}s(Jb,"findInvalidGroupType");function Zb(t,e){const r=Bt(t,a=>a.PUSH_MODE!==void 0&&!vt(e,a.PUSH_MODE));return j(r,a=>({message:`Token Type: ->${a.name}<- static 'PUSH_MODE' value cannot refer to a Lexer Mode ->${a.PUSH_MODE}<-which does not exist`,type:Me.PUSH_MODE_DOES_NOT_EXIST,tokenTypes:[a]}))}s(Zb,"findModesThatDoNotExist");function Qb(t){const e=[],r=kt(t,(n,a,i)=>{const o=a.PATTERN;return o===pt.NA||(Ct(o)?n.push({str:o,idx:i,tokenType:a}):Pr(o)&&t_(o)&&n.push({str:o.source,idx:i,tokenType:a})),n},[]);return V(t,(n,a)=>{V(r,({str:i,idx:o,tokenType:u})=>{if(a<o&&e_(i,n.PATTERN)){const l=`Token: ->${u.name}<- can never be matched.
Because it appears AFTER the Token Type ->${n.name}<-in the lexer's definition.
See https://chevrotain.io/docs/guide/resolving_lexer_errors.html#UNREACHABLE`;e.push({message:l,type:Me.UNREACHABLE_PATTERN,tokenTypes:[n,u]})}})}),e}s(Qb,"findUnreachablePatterns");function e_(t,e){if(Pr(e)){if(r_(e))return!1;const r=e.exec(t);return r!==null&&r.index===0}else{if(Gr(e))return e(t,0,[],{});if(K(e,"exec"))return e.exec(t,0,[],{});if(typeof e=="string")return e===t;throw Error("non exhaustive match")}}s(e_,"tryToMatchStrToPattern");function t_(t){return el([".","\\","[","]","|","^","$","(",")","?","*","+","{"],r=>t.source.indexOf(r)!==-1)===void 0}s(t_,"noMetaChar");function r_(t){return/(\(\?=)|(\(\?!)|(\(\?<=)|(\(\?<!)/.test(t.source)}s(r_,"usesLookAheadOrBehind");function wm(t){const e=t.ignoreCase?"iy":"y";return new RegExp(`${t.source}`,e)}s(wm,"addStickyFlag");function n_(t,e,r){const n=[];return K(t,Jl)||n.push({message:"A MultiMode Lexer cannot be initialized without a <"+Jl+`> property in its definition
`,type:Me.MULTI_MODE_LEXER_WITHOUT_DEFAULT_MODE}),K(t,ic)||n.push({message:"A MultiMode Lexer cannot be initialized without a <"+ic+`> property in its definition
`,type:Me.MULTI_MODE_LEXER_WITHOUT_MODES_PROPERTY}),K(t,ic)&&K(t,Jl)&&!K(t.modes,t.defaultMode)&&n.push({message:`A MultiMode Lexer cannot be initialized with a ${Jl}: <${t.defaultMode}>which does not exist
`,type:Me.MULTI_MODE_LEXER_DEFAULT_MODE_VALUE_DOES_NOT_EXIST}),K(t,ic)&&V(t.modes,(a,i)=>{V(a,(o,u)=>{if(kr(o))n.push({message:`A Lexer cannot be initialized using an undefined Token Type. Mode:<${i}> at index: <${u}>
`,type:Me.LEXER_DEFINITION_CANNOT_CONTAIN_UNDEFINED});else if(K(o,"LONGER_ALT")){const l=se(o.LONGER_ALT)?o.LONGER_ALT:[o.LONGER_ALT];V(l,c=>{!kr(c)&&!vt(a,c)&&n.push({message:`A MultiMode Lexer cannot be initialized with a longer_alt <${c.name}> on token <${o.name}> outside of mode <${i}>
`,type:Me.MULTI_MODE_LEXER_LONGER_ALT_NOT_IN_CURRENT_MODE})})}})}),n}s(n_,"performRuntimeChecks");function a_(t,e,r){const n=[];let a=!1;const i=Xu(Ht(Ke(t.modes))),o=cd(i,l=>l[Un]===pt.NA),u=Hy(r);return e&&V(o,l=>{const c=qy(l,u);if(c!==!1){const d={message:o_(l,c),type:c.issue,tokenType:l};n.push(d)}else K(l,"LINE_BREAKS")?l.LINE_BREAKS===!0&&(a=!0):dd(u,l.PATTERN)&&(a=!0)}),e&&!a&&n.push({message:`Warning: No LINE_BREAKS Found.
This Lexer has been defined to track line and column information,
But none of the Token Types can be identified as matching a line terminator.
See https://chevrotain.io/docs/guide/resolving_lexer_errors.html#LINE_BREAKS
for details.`,type:Me.NO_LINE_BREAKS_FLAGS}),n}s(a_,"performWarningRuntimeChecks");function i_(t){const e={},r=Nt(t);return V(r,n=>{const a=t[n];if(se(a))e[n]=[];else throw Error("non exhaustive match")}),e}s(i_,"cloneEmptyGroups");function Vy(t){const e=t.PATTERN;if(Pr(e))return!1;if(Gr(e))return!0;if(K(e,"exec"))return!0;if(Ct(e))return!1;throw Error("non exhaustive match")}s(Vy,"isCustomPattern");function s_(t){return Ct(t)&&t.length===1?t.charCodeAt(0):!1}s(s_,"isShortPattern");var R1={test:s(function(t){const e=t.length;for(let r=this.lastIndex;r<e;r++){const n=t.charCodeAt(r);if(n===10)return this.lastIndex=r+1,!0;if(n===13)return t.charCodeAt(r+1)===10?this.lastIndex=r+2:this.lastIndex=r+1,!0}return!1},"test"),lastIndex:0};function qy(t,e){if(K(t,"LINE_BREAKS"))return!1;if(Pr(t.PATTERN)){try{dd(e,t.PATTERN)}catch(r){return{issue:Me.IDENTIFY_TERMINATOR,errMsg:r.message}}return!1}else{if(Ct(t.PATTERN))return!1;if(Vy(t))return{issue:Me.CUSTOM_LINE_BREAK};throw Error("non exhaustive match")}}s(qy,"checkLineBreaksIssues");function o_(t,e){if(e.issue===Me.IDENTIFY_TERMINATOR)return`Warning: unable to identify line terminator usage in pattern.
The problem is in the <${t.name}> Token Type
Root cause: ${e.errMsg}.
For details See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#IDENTIFY_TERMINATOR`;if(e.issue===Me.CUSTOM_LINE_BREAK)return`Warning: A Custom Token Pattern should specify the <line_breaks> option.
The problem is in the <${t.name}> Token Type
For details See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#CUSTOM_LINE_BREAK`;throw Error("non exhaustive match")}s(o_,"buildLineBreakIssueMessage");function Hy(t){return j(t,r=>Ct(r)?r.charCodeAt(0):r)}s(Hy,"getCharCodes");function Pc(t,e,r){t[e]===void 0?t[e]=[r]:t[e].push(r)}s(Pc,"addToMapOfArrays");var Zl=256,kc=[];function Or(t){return t<Zl?t:kc[t]}s(Or,"charCodeToOptimizedIndex");function l_(){if(Re(kc)){kc=new Array(65536);for(let t=0;t<65536;t++)kc[t]=t>255?255+~~(t/255):t}}s(l_,"initCharCodeToOptimizedIndexMap");function ul(t,e){const r=t.tokenTypeIdx;return r===e.tokenTypeIdx?!0:e.isParent===!0&&e.categoryMatchesMap[r]===!0}s(ul,"tokenStructuredMatcher");function _u(t,e){return t.tokenTypeIdx===e.tokenTypeIdx}s(_u,"tokenStructuredMatcherNoCategories");var jv=1,u_={};function cl(t){const e=c_(t);f_(e),p_(e),d_(e),V(e,r=>{r.isParent=r.categoryMatches.length>0})}s(cl,"augmentTokenTypes");function c_(t){let e=it(t),r=t,n=!0;for(;n;){r=Xu(Ht(j(r,i=>i.CATEGORIES)));const a=ud(r,e);e=e.concat(a),Re(a)?n=!1:r=a}return e}s(c_,"expandCategories");function f_(t){V(t,e=>{Xy(e)||(u_[jv]=e,e.tokenTypeIdx=jv++),Im(e)&&!se(e.CATEGORIES)&&(e.CATEGORIES=[e.CATEGORIES]),Im(e)||(e.CATEGORIES=[]),m_(e)||(e.categoryMatches=[]),h_(e)||(e.categoryMatchesMap={})})}s(f_,"assignTokenDefaultProps");function d_(t){V(t,e=>{e.categoryMatches=[],V(e.categoryMatchesMap,(r,n)=>{e.categoryMatches.push(u_[n].tokenTypeIdx)})})}s(d_,"assignCategoriesTokensProp");function p_(t){V(t,e=>{Yy([],e)})}s(p_,"assignCategoriesMapProp");function Yy(t,e){V(t,r=>{e.categoryMatchesMap[r.tokenTypeIdx]=!0}),V(e.CATEGORIES,r=>{const n=t.concat(e);vt(n,r)||Yy(n,r)})}s(Yy,"singleAssignCategoriesToksMap");function Xy(t){return K(t,"tokenTypeIdx")}s(Xy,"hasShortKeyProperty");function Im(t){return K(t,"CATEGORIES")}s(Im,"hasCategoriesProperty");function m_(t){return K(t,"categoryMatches")}s(m_,"hasExtendingTokensTypesProperty");function h_(t){return K(t,"categoryMatchesMap")}s(h_,"hasExtendingTokensTypesMapProperty");function y_(t){return K(t,"tokenTypeIdx")}s(y_,"isTokenType");var Nm={buildUnableToPopLexerModeMessage(t){return`Unable to pop Lexer Mode after encountering Token ->${t.image}<- The Mode Stack is empty`},buildUnexpectedCharactersMessage(t,e,r,n,a,i){return`unexpected character: ->${t.charAt(e)}<- at offset: ${e}, skipped ${r} characters.`}},Me;(function(t){t[t.MISSING_PATTERN=0]="MISSING_PATTERN",t[t.INVALID_PATTERN=1]="INVALID_PATTERN",t[t.EOI_ANCHOR_FOUND=2]="EOI_ANCHOR_FOUND",t[t.UNSUPPORTED_FLAGS_FOUND=3]="UNSUPPORTED_FLAGS_FOUND",t[t.DUPLICATE_PATTERNS_FOUND=4]="DUPLICATE_PATTERNS_FOUND",t[t.INVALID_GROUP_TYPE_FOUND=5]="INVALID_GROUP_TYPE_FOUND",t[t.PUSH_MODE_DOES_NOT_EXIST=6]="PUSH_MODE_DOES_NOT_EXIST",t[t.MULTI_MODE_LEXER_WITHOUT_DEFAULT_MODE=7]="MULTI_MODE_LEXER_WITHOUT_DEFAULT_MODE",t[t.MULTI_MODE_LEXER_WITHOUT_MODES_PROPERTY=8]="MULTI_MODE_LEXER_WITHOUT_MODES_PROPERTY",t[t.MULTI_MODE_LEXER_DEFAULT_MODE_VALUE_DOES_NOT_EXIST=9]="MULTI_MODE_LEXER_DEFAULT_MODE_VALUE_DOES_NOT_EXIST",t[t.LEXER_DEFINITION_CANNOT_CONTAIN_UNDEFINED=10]="LEXER_DEFINITION_CANNOT_CONTAIN_UNDEFINED",t[t.SOI_ANCHOR_FOUND=11]="SOI_ANCHOR_FOUND",t[t.EMPTY_MATCH_PATTERN=12]="EMPTY_MATCH_PATTERN",t[t.NO_LINE_BREAKS_FLAGS=13]="NO_LINE_BREAKS_FLAGS",t[t.UNREACHABLE_PATTERN=14]="UNREACHABLE_PATTERN",t[t.IDENTIFY_TERMINATOR=15]="IDENTIFY_TERMINATOR",t[t.CUSTOM_LINE_BREAK=16]="CUSTOM_LINE_BREAK",t[t.MULTI_MODE_LEXER_LONGER_ALT_NOT_IN_CURRENT_MODE=17]="MULTI_MODE_LEXER_LONGER_ALT_NOT_IN_CURRENT_MODE"})(Me||(Me={}));var Ql={deferDefinitionErrorsHandling:!1,positionTracking:"full",lineTerminatorsPattern:/\n|\r\n?/g,lineTerminatorCharacters:[`
`,"\r"],ensureOptimizations:!1,safeMode:!1,errorMessageProvider:Nm,traceInitPerf:!1,skipValidations:!1,recoveryEnabled:!0};Object.freeze(Ql);var $i,pt=($i=class{constructor(e,r=Ql){if(this.lexerDefinition=e,this.lexerDefinitionErrors=[],this.lexerDefinitionWarning=[],this.patternIdxToConfig={},this.charCodeToPatternIdxToConfig={},this.modes=[],this.emptyGroups={},this.trackStartLines=!0,this.trackEndLines=!0,this.hasCustom=!1,this.canModeBeOptimized={},this.TRACE_INIT=(a,i)=>{if(this.traceInitPerf===!0){this.traceInitIndent++;const o=new Array(this.traceInitIndent+1).join(" ");this.traceInitIndent<this.traceInitMaxIdent&&console.log(`${o}--> <${a}>`);const{time:u,value:l}=Ky(i),c=u>10?console.warn:console.log;return this.traceInitIndent<this.traceInitMaxIdent&&c(`${o}<-- <${a}> time: ${u}ms`),this.traceInitIndent--,l}else return i()},typeof r=="boolean")throw Error(`The second argument to the Lexer constructor is now an ILexerConfig Object.
a boolean 2nd argument is no longer supported`);this.config=Pt({},Ql,r);const n=this.config.traceInitPerf;n===!0?(this.traceInitMaxIdent=1/0,this.traceInitPerf=!0):typeof n=="number"&&(this.traceInitMaxIdent=n,this.traceInitPerf=!0),this.traceInitIndent=-1,this.TRACE_INIT("Lexer Constructor",()=>{let a,i=!0;this.TRACE_INIT("Lexer Config handling",()=>{if(this.config.lineTerminatorsPattern===Ql.lineTerminatorsPattern)this.config.lineTerminatorsPattern=R1;else if(this.config.lineTerminatorCharacters===Ql.lineTerminatorCharacters)throw Error(`Error: Missing <lineTerminatorCharacters> property on the Lexer config.
For details See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#MISSING_LINE_TERM_CHARS`);if(r.safeMode&&r.ensureOptimizations)throw Error('"safeMode" and "ensureOptimizations" flags are mutually exclusive.');this.trackStartLines=/full|onlyStart/i.test(this.config.positionTracking),this.trackEndLines=/full/i.test(this.config.positionTracking),se(e)?a={modes:{defaultMode:it(e)},defaultMode:Jl}:(i=!1,a=it(e))}),this.config.skipValidations===!1&&(this.TRACE_INIT("performRuntimeChecks",()=>{this.lexerDefinitionErrors=this.lexerDefinitionErrors.concat(n_(a,this.trackStartLines,this.config.lineTerminatorCharacters))}),this.TRACE_INIT("performWarningRuntimeChecks",()=>{this.lexerDefinitionWarning=this.lexerDefinitionWarning.concat(a_(a,this.trackStartLines,this.config.lineTerminatorCharacters))})),a.modes=a.modes?a.modes:{},V(a.modes,(u,l)=>{a.modes[l]=cd(u,c=>kr(c))});const o=Nt(a.modes);if(V(a.modes,(u,l)=>{this.TRACE_INIT(`Mode: <${l}> processing`,()=>{if(this.modes.push(l),this.config.skipValidations===!1&&this.TRACE_INIT("validatePatterns",()=>{this.lexerDefinitionErrors=this.lexerDefinitionErrors.concat(Bb(u,o))}),Re(this.lexerDefinitionErrors)){cl(u);let c;this.TRACE_INIT("analyzeTokenTypes",()=>{c=jb(u,{lineTerminatorCharacters:this.config.lineTerminatorCharacters,positionTracking:r.positionTracking,ensureOptimizations:r.ensureOptimizations,safeMode:r.safeMode,tracer:this.TRACE_INIT})}),this.patternIdxToConfig[l]=c.patternIdxToConfig,this.charCodeToPatternIdxToConfig[l]=c.charCodeToPatternIdxToConfig,this.emptyGroups=Pt({},this.emptyGroups,c.emptyGroups),this.hasCustom=c.hasCustom||this.hasCustom,this.canModeBeOptimized[l]=c.canBeOptimized}})}),this.defaultMode=a.defaultMode,!Re(this.lexerDefinitionErrors)&&!this.config.deferDefinitionErrorsHandling){const l=j(this.lexerDefinitionErrors,c=>c.message).join(`-----------------------
`);throw new Error(`Errors detected in definition of Lexer:
`+l)}V(this.lexerDefinitionWarning,u=>{Uy(u.message)}),this.TRACE_INIT("Choosing sub-methods implementations",()=>{if(i&&(this.handleModes=He),this.trackStartLines===!1&&(this.computeNewColumn=Ku),this.trackEndLines===!1&&(this.updateTokenEndLineColumnLocation=He),/full/i.test(this.config.positionTracking))this.createTokenInstance=this.createFullToken;else if(/onlyStart/i.test(this.config.positionTracking))this.createTokenInstance=this.createStartOnlyToken;else if(/onlyOffset/i.test(this.config.positionTracking))this.createTokenInstance=this.createOffsetOnlyToken;else throw Error(`Invalid <positionTracking> config option: "${this.config.positionTracking}"`);this.hasCustom?(this.addToken=this.addTokenUsingPush,this.handlePayload=this.handlePayloadWithCustom):(this.addToken=this.addTokenUsingMemberAccess,this.handlePayload=this.handlePayloadNoCustom)}),this.TRACE_INIT("Failed Optimization Warnings",()=>{const u=kt(this.canModeBeOptimized,(l,c,f)=>(c===!1&&l.push(f),l),[]);if(r.ensureOptimizations&&!Re(u))throw Error(`Lexer Modes: < ${u.join(", ")} > cannot be optimized.
Disable the "ensureOptimizations" lexer config flag to silently ignore this and run the lexer in an un-optimized mode.
Or inspect the console log for details on how to resolve these issues.`)}),this.TRACE_INIT("clearRegExpParserCache",()=>{Mb()}),this.TRACE_INIT("toFastProperties",()=>{Wy(this)})})}tokenize(e,r=this.defaultMode){if(!Re(this.lexerDefinitionErrors)){const a=j(this.lexerDefinitionErrors,i=>i.message).join(`-----------------------
`);throw new Error(`Unable to Tokenize because Errors detected in definition of Lexer:
`+a)}return this.tokenizeInternal(e,r)}tokenizeInternal(e,r){let n,a,i,o,u,l,c,f,d,p,y,h,T,C,v;const w=e,b=w.length;let N=0,B=0;const ne=this.hasCustom?0:Math.floor(e.length/10),J=new Array(ne),he=[];let Ae=this.trackStartLines?1:void 0,ye=this.trackStartLines?1:void 0;const ue=i_(this.emptyGroups),ot=this.trackStartLines,k=this.config.lineTerminatorsPattern;let _=0,$=[],I=[];const R=[],A=[];Object.freeze(A);let S=!1;const L=s(M=>{if(R.length===1&&M.tokenType.PUSH_MODE===void 0){const Y=this.config.errorMessageProvider.buildUnableToPopLexerModeMessage(M);he.push({offset:M.startOffset,line:M.startLine,column:M.startColumn,length:M.image.length,message:Y})}else{R.pop();const Y=Bn(R);$=this.patternIdxToConfig[Y],I=this.charCodeToPatternIdxToConfig[Y],_=$.length;const q=this.canModeBeOptimized[Y]&&this.config.safeMode===!1;I&&q?S=!0:S=!1}},"pop_mode");function x(M){R.push(M),I=this.charCodeToPatternIdxToConfig[M],$=this.patternIdxToConfig[M],_=$.length,_=$.length;const Y=this.canModeBeOptimized[M]&&this.config.safeMode===!1;I&&Y?S=!0:S=!1}s(x,"push_mode"),x.call(this,r);let O;const z=this.config.recoveryEnabled;for(;N<b;){l=null,d=-1;const M=w.charCodeAt(N);let Y;if(S){const Z=Or(M),ae=I[Z];Y=ae!==void 0?ae:A}else Y=$;const q=Y.length;for(n=0;n<q;n++){O=Y[n];const Z=O.pattern;c=null;const ae=O.short;if(ae!==!1?M===ae&&(d=1,l=Z):O.isCustom===!0?(v=Z.exec(w,N,J,ue),v!==null?(l=v[0],d=l.length,v.payload!==void 0&&(c=v.payload)):l=null):(Z.lastIndex=N,d=this.matchLength(Z,e,N)),d!==-1){if(u=O.longerAlt,u!==void 0){l=e.substring(N,N+d);const Oe=u.length;for(i=0;i<Oe;i++){const pe=$[u[i]],Ee=pe.pattern;if(f=null,pe.isCustom===!0?(v=Ee.exec(w,N,J,ue),v!==null?(o=v[0],v.payload!==void 0&&(f=v.payload)):o=null):(Ee.lastIndex=N,o=this.match(Ee,e,N)),o&&o.length>l.length){l=o,d=o.length,c=f,O=pe;break}}}break}}if(d!==-1){if(p=O.group,p!==void 0&&(l=l!==null?l:e.substring(N,N+d),y=O.tokenTypeIdx,h=this.createTokenInstance(l,N,y,O.tokenType,Ae,ye,d),this.handlePayload(h,c),p===!1?B=this.addToken(J,B,h):ue[p].push(h)),ot===!0&&O.canLineTerminator===!0){let Z=0,ae,Oe;k.lastIndex=0;do l=l!==null?l:e.substring(N,N+d),ae=k.test(l),ae===!0&&(Oe=k.lastIndex-1,Z++);while(ae===!0);Z!==0?(Ae=Ae+Z,ye=d-Oe,this.updateTokenEndLineColumnLocation(h,p,Oe,Z,Ae,ye,d)):ye=this.computeNewColumn(ye,d)}else ye=this.computeNewColumn(ye,d);N=N+d,this.handleModes(O,L,x,h)}else{const Z=N,ae=Ae,Oe=ye;let pe=z===!1;for(;pe===!1&&N<b;)for(N++,a=0;a<_;a++){const Ee=$[a],Ve=Ee.pattern,Le=Ee.short;if(Le!==!1?w.charCodeAt(N)===Le&&(pe=!0):Ee.isCustom===!0?pe=Ve.exec(w,N,J,ue)!==null:(Ve.lastIndex=N,pe=Ve.exec(e)!==null),pe===!0)break}if(T=N-Z,ye=this.computeNewColumn(ye,T),C=this.config.errorMessageProvider.buildUnexpectedCharactersMessage(w,Z,T,ae,Oe,Bn(R)),he.push({offset:Z,line:ae,column:Oe,length:T,message:C}),z===!1)break}}return this.hasCustom||(J.length=B),{tokens:J,groups:ue,errors:he}}handleModes(e,r,n,a){if(e.pop===!0){const i=e.push;r(a),i!==void 0&&n.call(this,i)}else e.push!==void 0&&n.call(this,e.push)}updateTokenEndLineColumnLocation(e,r,n,a,i,o,u){let l,c;r!==void 0&&(l=n===u-1,c=l?-1:0,a===1&&l===!0||(e.endLine=i+c,e.endColumn=o-1+-c))}computeNewColumn(e,r){return e+r}createOffsetOnlyToken(e,r,n,a){return{image:e,startOffset:r,tokenTypeIdx:n,tokenType:a}}createStartOnlyToken(e,r,n,a,i,o){return{image:e,startOffset:r,startLine:i,startColumn:o,tokenTypeIdx:n,tokenType:a}}createFullToken(e,r,n,a,i,o,u){return{image:e,startOffset:r,endOffset:r+u-1,startLine:i,endLine:i,startColumn:o,endColumn:o+u-1,tokenTypeIdx:n,tokenType:a}}addTokenUsingPush(e,r,n){return e.push(n),r}addTokenUsingMemberAccess(e,r,n){return e[r]=n,r++,r}handlePayloadNoCustom(e,r){}handlePayloadWithCustom(e,r){r!==null&&(e.payload=r)}match(e,r,n){return e.test(r)===!0?r.substring(n,e.lastIndex):null}matchLength(e,r,n){return e.test(r)===!0?e.lastIndex-n:-1}},s($i,"Lexer"),$i);pt.SKIPPED="This marks a skipped Token pattern, this means each token identified by it will be consumed and then thrown into oblivion, this can be used to for example to completely ignore whitespace.";pt.NA=/NOT_APPLICABLE/;function Mn(t){return Jy(t)?t.LABEL:t.name}s(Mn,"tokenLabel");function Jy(t){return Ct(t.LABEL)&&t.LABEL!==""}s(Jy,"hasTokenLabel");var A1="parent",Bv="categories",Uv="label",Kv="group",Wv="push_mode",Vv="pop_mode",qv="longer_alt",Hv="line_breaks",Yv="start_chars_hint";function Xa(t){return g_(t)}s(Xa,"createToken");function g_(t){const e=t.pattern,r={};if(r.name=t.name,kr(e)||(r.PATTERN=e),K(t,A1))throw`The parent property is no longer supported.
See: https://github.com/chevrotain/chevrotain/issues/564#issuecomment-349062346 for details.`;return K(t,Bv)&&(r.CATEGORIES=t[Bv]),cl([r]),K(t,Uv)&&(r.LABEL=t[Uv]),K(t,Kv)&&(r.GROUP=t[Kv]),K(t,Vv)&&(r.POP_MODE=t[Vv]),K(t,Wv)&&(r.PUSH_MODE=t[Wv]),K(t,qv)&&(r.LONGER_ALT=t[qv]),K(t,Hv)&&(r.LINE_BREAKS=t[Hv]),K(t,Yv)&&(r.START_CHARS_HINT=t[Yv]),r}s(g_,"createTokenInternal");var Jr=Xa({name:"EOF",pattern:pt.NA});cl([Jr]);function Zu(t,e,r,n,a,i,o,u){return{image:e,startOffset:r,endOffset:n,startLine:a,endLine:i,startColumn:o,endColumn:u,tokenTypeIdx:t.tokenTypeIdx,tokenType:t}}s(Zu,"createTokenInstance");function Zy(t,e){return ul(t,e)}s(Zy,"tokenMatcher");var qa={buildMismatchTokenMessage({expected:t,actual:e,previous:r,ruleName:n}){return`Expecting ${Jy(t)?`--> ${Mn(t)} <--`:`token of type --> ${t.name} <--`} but found --> '${e.image}' <--`},buildNotAllInputParsedMessage({firstRedundant:t,ruleName:e}){return"Redundant input, expecting EOF but found: "+t.image},buildNoViableAltMessage({expectedPathsPerAlt:t,actual:e,previous:r,customUserDescription:n,ruleName:a}){const i="Expecting: ",u=`
but found: '`+Jt(e).image+"'";if(n)return i+n+u;{const l=kt(t,(p,y)=>p.concat(y),[]),c=j(l,p=>`[${j(p,y=>Mn(y)).join(", ")}]`),d=`one of these possible Token sequences:
${j(c,(p,y)=>` ${y+1}. ${p}`).join(`
`)}`;return i+d+u}},buildEarlyExitMessage({expectedIterationPaths:t,actual:e,customUserDescription:r,ruleName:n}){const a="Expecting: ",o=`
but found: '`+Jt(e).image+"'";if(r)return a+r+o;{const l=`expecting at least one iteration which starts with one of these possible Token sequences::
<${j(t,c=>`[${j(c,f=>Mn(f)).join(",")}]`).join(" ,")}>`;return a+l+o}}};Object.freeze(qa);var E1={buildRuleNotFoundError(t,e){return"Invalid grammar, reference to a rule which is not defined: ->"+e.nonTerminalName+`<-
inside top level rule: ->`+t.name+"<-"}},Ln={buildDuplicateFoundError(t,e){function r(f){return f instanceof Se?f.terminalType.name:f instanceof ht?f.nonTerminalName:""}s(r,"getExtraProductionArgument");const n=t.name,a=Jt(e),i=a.idx,o=Kt(a),u=r(a),l=i>0;let c=`->${o}${l?i:""}<- ${u?`with argument: ->${u}<-`:""}
appears more than once (${e.length} times) in the top level rule: ->${n}<-.
For further details see: https://chevrotain.io/docs/FAQ.html#NUMERICAL_SUFFIXES
`;return c=c.replace(/[ \t]+/g," "),c=c.replace(/\s\s+/g,`
`),c},buildNamespaceConflictError(t){return`Namespace conflict found in grammar.
The grammar has both a Terminal(Token) and a Non-Terminal(Rule) named: <${t.name}>.
To resolve this make sure each Terminal and Non-Terminal names are unique
This is easy to accomplish by using the convention that Terminal names start with an uppercase letter
and Non-Terminal names start with a lower case letter.`},buildAlternationPrefixAmbiguityError(t){const e=j(t.prefixPath,a=>Mn(a)).join(", "),r=t.alternation.idx===0?"":t.alternation.idx;return`Ambiguous alternatives: <${t.ambiguityIndices.join(" ,")}> due to common lookahead prefix
in <OR${r}> inside <${t.topLevelRule.name}> Rule,
<${e}> may appears as a prefix path in all these alternatives.
See: https://chevrotain.io/docs/guide/resolving_grammar_errors.html#COMMON_PREFIX
For Further details.`},buildAlternationAmbiguityError(t){const e=t.alternation.idx===0?"":t.alternation.idx,r=t.prefixPath.length===0;let n=`Ambiguous Alternatives Detected: <${t.ambiguityIndices.join(" ,")}> in <OR${e}> inside <${t.topLevelRule.name}> Rule,
`;if(r)n+=`These alternatives are all empty (match no tokens), making them indistinguishable.
Only the last alternative may be empty.
`;else{const a=j(t.prefixPath,i=>Mn(i)).join(", ");n+=`<${a}> may appears as a prefix path in all these alternatives.
`}return n+=`See: https://chevrotain.io/docs/guide/resolving_grammar_errors.html#AMBIGUOUS_ALTERNATIVES
For Further details.`,n},buildEmptyRepetitionError(t){let e=Kt(t.repetition);return t.repetition.idx!==0&&(e+=t.repetition.idx),`The repetition <${e}> within Rule <${t.topLevelRule.name}> can never consume any tokens.
This could lead to an infinite loop.`},buildTokenNameError(t){return"deprecated"},buildEmptyAlternationError(t){return`Ambiguous empty alternative: <${t.emptyChoiceIdx+1}> in <OR${t.alternation.idx}> inside <${t.topLevelRule.name}> Rule.
Only the last alternative may be an empty alternative.`},buildTooManyAlternativesError(t){return`An Alternation cannot have more than 256 alternatives:
<OR${t.alternation.idx}> inside <${t.topLevelRule.name}> Rule.
has ${t.alternation.definition.length+1} alternatives.`},buildLeftRecursionError(t){const e=t.topLevelRule.name,r=j(t.leftRecursionPath,i=>i.name),n=`${e} --> ${r.concat([e]).join(" --> ")}`;return`Left Recursion found in grammar.
rule: <${e}> can be invoked from itself (directly or indirectly)
without consuming any Tokens. The grammar path that causes this is:
${n}
To fix this refactor your grammar to remove the left recursion.
see: https://en.wikipedia.org/wiki/LL_parser#Left_factoring.`},buildInvalidRuleNameError(t){return"deprecated"},buildDuplicateRuleNameError(t){let e;return t.topLevelRule instanceof sl?e=t.topLevelRule.name:e=t.topLevelRule,`Duplicate definition, rule: ->${e}<- is already defined in the grammar: ->${t.grammarName}<-`}};function v_(t,e){const r=new C1(t,e);return r.resolveRefs(),r.errors}s(v_,"resolveGrammar");var Ri,C1=(Ri=class extends ol{constructor(e,r){super(),this.nameToTopRule=e,this.errMsgProvider=r,this.errors=[]}resolveRefs(){V(Ke(this.nameToTopRule),e=>{this.currTopLevel=e,e.accept(this)})}visitNonTerminal(e){const r=this.nameToTopRule[e.nonTerminalName];if(r)e.referencedRule=r;else{const n=this.errMsgProvider.buildRuleNotFoundError(this.currTopLevel,e);this.errors.push({message:n,type:yt.UNRESOLVED_SUBRULE_REF,ruleName:this.currTopLevel.name,unresolvedRefName:e.nonTerminalName})}}},s(Ri,"GastRefResolverVisitor"),Ri),Ai,b1=(Ai=class extends fd{constructor(e,r){super(),this.topProd=e,this.path=r,this.possibleTokTypes=[],this.nextProductionName="",this.nextProductionOccurrence=0,this.found=!1,this.isAtEndOfPath=!1}startWalking(){if(this.found=!1,this.path.ruleStack[0]!==this.topProd.name)throw Error("The path does not start with the walker's top Rule!");return this.ruleStack=it(this.path.ruleStack).reverse(),this.occurrenceStack=it(this.path.occurrenceStack).reverse(),this.ruleStack.pop(),this.occurrenceStack.pop(),this.updateExpectedNext(),this.walk(this.topProd),this.possibleTokTypes}walk(e,r=[]){this.found||super.walk(e,r)}walkProdRef(e,r,n){if(e.referencedRule.name===this.nextProductionName&&e.idx===this.nextProductionOccurrence){const a=r.concat(n);this.updateExpectedNext(),this.walk(e.referencedRule,a)}}updateExpectedNext(){Re(this.ruleStack)?(this.nextProductionName="",this.nextProductionOccurrence=0,this.isAtEndOfPath=!0):(this.nextProductionName=this.ruleStack.pop(),this.nextProductionOccurrence=this.occurrenceStack.pop())}},s(Ai,"AbstractNextPossibleTokensWalker"),Ai),Ei,_1=(Ei=class extends b1{constructor(e,r){super(e,r),this.path=r,this.nextTerminalName="",this.nextTerminalOccurrence=0,this.nextTerminalName=this.path.lastTok.name,this.nextTerminalOccurrence=this.path.lastTokOccurrence}walkTerminal(e,r,n){if(this.isAtEndOfPath&&e.terminalType.name===this.nextTerminalName&&e.idx===this.nextTerminalOccurrence&&!this.found){const a=r.concat(n),i=new bt({definition:a});this.possibleTokTypes=ll(i),this.found=!0}}},s(Ei,"NextAfterTokenWalker"),Ei),Ci,pd=(Ci=class extends fd{constructor(e,r){super(),this.topRule=e,this.occurrence=r,this.result={token:void 0,occurrence:void 0,isEndOfRule:void 0}}startWalking(){return this.walk(this.topRule),this.result}},s(Ci,"AbstractNextTerminalAfterProductionWalker"),Ci),bi,S1=(bi=class extends pd{walkMany(e,r,n){if(e.idx===this.occurrence){const a=Jt(r.concat(n));this.result.isEndOfRule=a===void 0,a instanceof Se&&(this.result.token=a.terminalType,this.result.occurrence=a.idx)}else super.walkMany(e,r,n)}},s(bi,"NextTerminalAfterManyWalker"),bi),_i,Xv=(_i=class extends pd{walkManySep(e,r,n){if(e.idx===this.occurrence){const a=Jt(r.concat(n));this.result.isEndOfRule=a===void 0,a instanceof Se&&(this.result.token=a.terminalType,this.result.occurrence=a.idx)}else super.walkManySep(e,r,n)}},s(_i,"NextTerminalAfterManySepWalker"),_i),Si,w1=(Si=class extends pd{walkAtLeastOne(e,r,n){if(e.idx===this.occurrence){const a=Jt(r.concat(n));this.result.isEndOfRule=a===void 0,a instanceof Se&&(this.result.token=a.terminalType,this.result.occurrence=a.idx)}else super.walkAtLeastOne(e,r,n)}},s(Si,"NextTerminalAfterAtLeastOneWalker"),Si),wi,Jv=(wi=class extends pd{walkAtLeastOneSep(e,r,n){if(e.idx===this.occurrence){const a=Jt(r.concat(n));this.result.isEndOfRule=a===void 0,a instanceof Se&&(this.result.token=a.terminalType,this.result.occurrence=a.idx)}else super.walkAtLeastOneSep(e,r,n)}},s(wi,"NextTerminalAfterAtLeastOneSepWalker"),wi);function gf(t,e,r=[]){r=it(r);let n=[],a=0;function i(u){return u.concat(nt(t,a+1))}s(i,"remainingPathWith");function o(u){const l=gf(i(u),e,r);return n.concat(l)}for(s(o,"getAlternativesForProd");r.length<e&&a<t.length;){const u=t[a];if(u instanceof bt)return o(u.definition);if(u instanceof ht)return o(u.definition);if(u instanceof at)n=o(u.definition);else if(u instanceof Ot){const l=u.definition.concat([new xe({definition:u.definition})]);return o(l)}else if(u instanceof Lt){const l=[new bt({definition:u.definition}),new xe({definition:[new Se({terminalType:u.separator})].concat(u.definition)})];return o(l)}else if(u instanceof _t){const l=u.definition.concat([new xe({definition:[new Se({terminalType:u.separator})].concat(u.definition)})]);n=o(l)}else if(u instanceof xe){const l=u.definition.concat([new xe({definition:u.definition})]);n=o(l)}else{if(u instanceof St)return V(u.definition,l=>{Re(l.definition)===!1&&(n=o(l.definition))}),n;if(u instanceof Se)r.push(u.terminalType);else throw Error("non exhaustive match")}a++}return n.push({p Line truncated
${i.join(`
`).replace(/\n/g,`
`)}`)}},"validateVisitor")};return r.prototype=n,r.prototype.constructor=r,r._RULE_NAMES=e,r}s(rS,"createBaseSemanticVisitorConstructor");function nS(t,e,r){const n=s(function(){},"derivedConstructor");sg(n,t+"BaseSemanticsWithDefaults");const a=Object.create(r.prototype);return V(e,i=>{a[i]=tS}),n.prototype=a,n.prototype.constructor=n,n}s(nS,"createBaseVisitorConstructorWithDefaults");var Gm;(function(t){t[t.REDUNDANT_METHOD=0]="REDUNDANT_METHOD",t[t.MISSING_METHOD=1]="MISSING_METHOD"})(Gm||(Gm={}));function aS(t,e){return iS(t,e)}s(aS,"validateVisitor");function iS(t,e){const r=Bt(e,a=>Gr(t[a])===!1),n=j(r,a=>({msg:`Missing visitor method: <${a}> on ${t.constructor.name} CST Visitor.`,type:Gm.MISSING_METHOD,methodName:a}));return Xu(n)}s(iS,"validateMissingCstMethods");var Ki,j1=(Ki=class{initTreeBuilder(e){if(this.CST_STACK=[],this.outputCst=e.outputCst,this.nodeLocationTracking=K(e,"nodeLocationTracking")?e.nodeLocationTracking:Lr.nodeLocationTracking,!this.outputCst)this.cstInvocationStateUpdate=He,this.cstFinallyStateUpdate=He,this.cstPostTerminal=He,this.cstPostNonTerminal=He,this.cstPostRule=He;else if(/full/i.test(this.nodeLocationTracking))this.recoveryEnabled?(this.setNodeLocationFromToken=Mm,this.setNodeLocationFromNode=Mm,this.cstPostRule=He,this.setInitialNodeLocation=this.setInitialNodeLocationFullRecovery):(this.setNodeLocationFromToken=He,this.setNodeLocationFromNode=He,this.cstPostRule=this.cstPostRuleFull,this.setInitialNodeLocation=this.setInitialNodeLocationFullRegular);else if(/onlyOffset/i.test(this.nodeLocationTracking))this.recoveryEnabled?(this.setNodeLocationFromToken=xm,this.setNodeLocationFromNode=xm,this.cstPostRule=He,this.setInitialNodeLocation=this.setInitialNodeLocationOnlyOffsetRecovery):(this.setNodeLocationFromToken=He,this.setNodeLocationFromNode=He,this.cstPostRule=this.cstPostRuleOnlyOffset,this.setInitialNodeLocation=this.setInitialNodeLocationOnlyOffsetRegular);else if(/none/i.test(this.nodeLocationTracking))this.setNodeLocationFromToken=He,this.setNodeLocationFromNode=He,this.cstPostRule=He,this.setInitialNodeLocation=He;else throw Error(`Invalid <nodeLocationTracking> config option: "${e.nodeLocationTracking}"`)}setInitialNodeLocationOnlyOffsetRecovery(e){e.location={startOffset:NaN,endOffset:NaN}}setInitialNodeLocationOnlyOffsetRegular(e){e.location={startOffset:this.LA(1).startOffset,endOffset:NaN}}setInitialNodeLocationFullRecovery(e){e.location={startOffset:NaN,startLine:NaN,startColumn:NaN,endOffset:NaN,endLine:NaN,endColumn:NaN}}setInitialNodeLocationFullRegular(e){const r=this.LA(1);e.location={startOffset:r.startOffset,startLine:r.startLine,startColumn:r.startColumn,endOffset:NaN,endLine:NaN,endColumn:NaN}}cstInvocationStateUpdate(e){const r={name:e,children:Object.create(null)};this.setInitialNodeLocation(r),this.CST_STACK.push(r)}cstFinallyStateUpdate(){this.CST_STACK.pop()}cstPostRuleFull(e){const r=this.LA(0),n=e.location;n.startOffset<=r.startOffset?(n.endOffset=r.endOffset,n.endLine=r.endLine,n.endColumn=r.endColumn):(n.startOffset=NaN,n.startLine=NaN,n.startColumn=NaN)}cstPostRuleOnlyOffset(e){const r=this.LA(0),n=e.location;n.startOffset<=r.startOffset?n.endOffset=r.endOffset:n.startOffset=NaN}cstPostTerminal(e,r){const n=this.CST_STACK[this.CST_STACK.length-1];Q_(n,r,e),this.setNodeLocationFromToken(n.location,r)}cstPostNonTerminal(e,r){const n=this.CST_STACK[this.CST_STACK.length-1];eS(n,r,e),this.setNodeLocationFromNode(n.location,e.location)}getBaseCstVisitorConstructor(){if(kr(this.baseCstVisitorConstructor)){const e=rS(this.className,Nt(this.gastProductionsCache));return this.baseCstVisitorConstructor=e,e}return this.baseCstVisitorConstructor}getBaseCstVisitorConstructorWithDefaults(){if(kr(this.baseCstVisitorWithDefaultsConstructor)){const e=nS(this.className,Nt(this.gastProductionsCache),this.getBaseCstVisitorConstructor());return this.baseCstVisitorWithDefaultsConstructor=e,e}return this.baseCstVisitorWithDefaultsConstructor}getLastExplicitRuleShortName(){const e=this.RULE_STACK;return e[e.length-1]}getPreviousExplicitRuleShortName(){const e=this.RULE_STACK;return e[e.length-2]}getLastExplicitRuleOccurrenceIndex(){const e=this.RULE_OCCURRENCE_STACK;return e[e.length-1]}},s(Ki,"TreeBuilder"),Ki),Wi,B1=(Wi=class{initLexerAdapter(){this.tokVector=[],this.tokVectorLength=0,this.currIdx=-1}set input(e){if(this.selfAnalysisDone!==!0)throw Error("Missing <performSelfAnalysis> invocation at the end of the Parser's constructor.");this.reset(),this.tokVector=e,this.tokVectorLength=e.length}get input(){return this.tokVector}SKIP_TOKEN(){return this.currIdx<=this.tokVector.length-2?(this.consumeToken(),this.LA(1)):Tf}LA(e){const r=this.currIdx+e;return r<0||this.tokVectorLength<=r?Tf:this.tokVector[r]}consumeToken(){this.currIdx++}exportLexerState(){return this.currIdx}importLexerState(e){this.currIdx=e}resetLexerState(){this.currIdx=-1}moveToTerminatedState(){this.currIdx=this.tokVector.length-1}getLexerPosition(){return this.exportLexerState()}},sLine truncated
See: https://chevrotain.io/docs/changes/BREAKING_CHANGES.html#_6-0-0
For Further details.`);if(se(e)){if(Re(e))throw Error(`A Token Vocabulary cannot be empty.
Note that the first argument for the parser constructor
is no longer a Token vector (since v4.0).`);if(typeof e[0].startOffset=="number")throw Error(`The Parser constructor no longer accepts a token vector as the first argument.
See: https://chevrotain.io/docs/changes/BREAKING_CHANGES.html#_4-0-0
For Further details.`)}if(se(e))this.tokensMap=kt(e,(i,o)=>(i[o.name]=o,i),{});else if(K(e,"modes")&&Yt(Ht(Ke(e.modes)),y_)){const i=Ht(Ke(e.modes)),o=By(i);this.tokensMap=kt(o,(u,l)=>(u[l.name]=l,u),{})}else if(zt(e))this.tokensMap=it(e);else throw new Error("<tokensDictionary> argument must be An Array of Token constructors, A dictionary of Token constructors or an IMultiModeLexerDefinition");this.tokensMap.EOF=Jr;const n=K(e,"modes")?Ht(Ke(e.modes)):Ke(e),a=Yt(n,i=>Re(i.categoryMatches));this.tokenMatcher=a?_u:ul,cl(Ke(this.tokensMap))}defineRule(e,r,n){if(this.selfAnalysisDone)throw Error(`Grammar rule <${e}> may not be defined after the 'performSelfAnalysis' method has been called'
Make sure that all grammar rule definitions are done before 'performSelfAnalysis' is called.`);const a=K(n,"resyncEnabled")?n.resyncEnabled:$f.resyncEnabled,i=K(n,"recoveryValueFunc")?n.recoveryValueFunc:$f.recoveryValueFunc,o=this.ruleShortNameIdx<<M1+nn;this.ruleShortNameIdx++,this.shortRuleNameToFull[o]=e,this.fullRuleNameToShort[e]=o;let u;return this.outputCst===!0?u=s(function(...f){try{this.ruleInvocationStateUpdate(o,e,this.subruleIdx),r.apply(this,f);const d=this.CST_STACK[this.CST_STACK.length-1];return this.cstPostRule(d),d}catch(d){return this.invokeRuleCatch(d,a,i)}finally{this.ruleFinallyStateUpdate()}},"invokeRuleWithTry"):u=s(function(...f){try{return this.ruleInvocationStateUpdate(o,e,this.subruleIdx),r.apply(this,f)}catch(d){return this.invokeRuleCatch(d,a,i)}finally{this.ruleFinallyStateUpdate()}},"invokeRuleWithTryCst"),Object.assign(u,{ruleName:e,originalGrammarAction:r})}invokeRuleCatch(e,r,n){const a=this.RULE_STACK.length===1,i=r&&!this.isBackTracking()&&this.recoveryEnabled;if(Su(e)){const o=e;if(i){const u=this.findReSyncTokenType();if(this.isInCurrentRuleReSyncSet(u))if(o.resyncedTokens=this.reSyncTo(u),this.outputCst){const l=this.CST_STACK[this.CST_STACK.length-1];return l.recoveredNode=!0,l}else return n(e);else{if(this.outputCst){const l=this.CST_STACK[this.CST_STACK.length-1];l.recoveredNode=!0,o.partialCstResult=l}throw o}}else{if(a)return this.moveToTerminatedState(),n(e);throw o}}else throw e}optionInternal(e,r){const n=this.getKeyForAutomaticLookahead(J_,r);return this.optionInternalLogic(e,r,n)}optionInternalLogic(e,r,n){let a=this.getLaFuncFromCache(n),i;if(typeof e!="function"){i=e.DEF;const o=e.GATE;if(o!==void 0){const u=a;a=s(()=>o.call(this)&&u.call(this),"lookAheadFunc")}}else i=e;if(a.call(this)===!0)return i.call(this)}atLeastOneInternal(e,r){const n=this.getKeyForAutomaticLookahead(Lm,e);return this.atLeastOneInternalLogic(e,r,n)}atLeastOneInternalLogic(e,r,n){let a=this.getLaFuncFromCache(n),i;if(typeof r!="function"){i=r.DEF;const o=r.GATE;if(o!==void 0){const u=a;a=s(()=>o.call(this)&&u.call(this),"lookAheadFunc")}}else i=r;if(a.call(this)===!0){let o=this.doSingleRepetition(i);for(;a.call(this)===!0&&o===!0;)o=this.doSingleRepetition(i)}else throw this.raiseEarlyExitException(e,Pe.REPETITION_MANDATORY,r.ERR_MSG);this.attemptInRepetitionRecovery(this.atLeastOneInternal,[e,r],a,Lm,e,w1)}atLeastOneSepFirstInternal(e,r){const n=this.getKeyForAutomaticLookahead(Lc,e);this.atLeastOneSepFirstInternalLogic(e,r,n)}atLeastOneSepFirstInternalLogic(e,r,n){const a=r.DEF,i=r.SEP;if(this.getLaFuncFromCache(n).call(this)===!0){a.call(this);const u=s(()=>this.tokenMatcher(this.LA(1),i),"separatorLookAheadFunc");for(;this.tokenMatcher(this.LA(1),i)===!0;)this.CONSUME(i),a.call(this);this.attemptInRepetitionRecovery(this.repetitionSepSecondInternal,[e,i,u,a,Jv],u,Lc,e,Jv)}else throw this.raiseEarlyExitException(e,Pe.REPETITION_MANDATORY_WITH_SEPARATOR,r.ERR_MSG)}manyInternal(e,r){const n=this.getKeyForAutomaticLookahead(Om,e);return this.manyInternalLogic(e,r,n)}manyInternalLogic(e,r,n){let a=this.getLaFuncFromCache(n),i;if(typeof r!="function"){i=r.DEF;const u=r.GATE;if(u!==void 0){const l=a;a=s(()=>u.call(this)&&l.call(this),"lookaheadFunction")}}else i=r;let o=!0;for(;a.call(this)===!0&&o===!0;)o=this.doSingleRepetition(i);this.attemptInRepetitionRecovery(this.manyInternal,[e,r],a,Om,e,S1,o)}manySepFirstInternal(e,r){const n=this.getKeyForAutomaticLookahead(Dm,e);this.manySepFirstInternalLogic(e,r,n)}manySepFirstInternalLogic(e,r,n){const a=r.DEF,i=r.SEP;if(this.getLaFuncFromCache(n).call(this)===!0){a.call(this);const u=s(()=>this.tokenMatcher(this.LA(1),i),"separatorLookAheadFunc");for(;this.tokenMatcher(this.LA(1),i)===!0;)this.CONSUME(i),a.call(this);this.attemptInRepetitionRecovery(this.repetitionSepSecondInternal,[e,i,u,a,Xv],u,Dm,e,Xv)}}repetitionSepSecondInternal(e,r,n,a,i){for(;n();)this.CONSUME(r),a.call(this);this.attemptInRepetitionRecovery(this.repetitionSepSecondInternal,[e,r,n,a,i],n,Lc,e,i)}doSingleRepetition(e){const r=this.getLexerPosition();return e.call(this),this.getLexerPosition()>r}orInternal(e,r){const n=this.getKeyForAutomaticLookahead(X_,r),a=se(e)?e:e.DEF,o=this.getLaFuncFromCache(n).call(this,a);if(o!==void 0)return a[o].ALT.call(this);this.raiseNoAltException(r,e.ERR_MSG)}ruleFinallyStateUpdate(){if(this.RULE_STACK.pop(),this.RULE_OCCURRENCE_STACK.pop(),this.cstFinallyStateUpdate(),this.RULE_STACK.length===0&&this.isAtEndOfInput()===!1){const e=this.LA(1),r=this.errorMessageProvider.buildNotAllInputParsedMessage({firstRedundant:e,ruleName:this.getCurrRuleFullName()});this.SAVE_ERROR(new O1(r,e))}}subruleInternal(e,r,n){let a;try{const i=n!==void 0?n.ARGS:void 0;return this.subruleIdx=r,a=e.apply(this,i),this.cstPostNonTerminal(a,n!==void 0&&n.LABEL!==void 0?n.LABEL:e.ruleName),a}catch(i){throw this.subruleInternalError(i,n,e.ruleName)}}subruleInternalError(e,r,n){throw Su(e)&&e.partialCstResult!==void 0&&(this.cstPostNonTerminal(e.partialCstRe Line truncated
See: https://chevrotain.io/docs/guide/internals.html#grammar-recording for details`,-1,-1,-1,-1,-1,-1);Object.freeze(oS);var q1={name:`This CSTNode indicates the Parser is in Recording Phase
See: https://chevrotain.io/docs/guide/internals.html#grammar-recording for details`,children:{}},Xi,H1=(Xi=class{initGastRecorder(e){this.recordingProdStack=[],this.RECORDING_PHASE=!1}enableRecording(){this.RECORDING_PHASE=!0,this.TRACE_INIT("Enable Recording",()=>{for(let e=0;e<10;e++){const r=e>0?e:"";this[`CONSUME${r}`]=function(n,a){return this.consumeInternalRecord(n,e,a)},this[`SUBRULE${r}`]=function(n,a){return this.subruleInternalRecord(n,e,a)},this[`OPTION${r}`]=function(n){return this.optionInternalRecord(n,e)},this[`OR${r}`]=function(n){return this.orInternalRecord(n,e)},this[`MANY${r}`]=function(n){this.manyInternalRecord(e,n)},this[`MANY_SEP${r}`]=function(n){this.manySepFirstInternalRecord(e,n)},this[`AT_LEAST_ONE${r}`]=function(n){this.atLeastOneInternalRecord(e,n)},this[`AT_LEAST_ONE_SEP${r}`]=function(n){this.atLeastOneSepFirstInternalRecord(e,n)}}this.consume=function(e,r,n){return this.consumeInternalRecord(r,e,n)},this.subrule=function(e,r,n){return this.subruleInternalRecord(r,e,n)},this.option=function(e,r){return this.optionInternalRecord(r,e)},this.or=function(e,r){return this.orInternalRecord(r,e)},this.many=function(e,r){this.manyInternalRecord(e,r)},this.atLeastOne=function(e,r){this.atLeastOneInternalRecord(e,r)},this.ACTION=this.ACTION_RECORD,this.BACKTRACK=this.BACKTRACK_RECORD,this.LA=this.LA_RECORD})}disableRecording(){this.RECORDING_PHASE=!1,this.TRACE_INIT("Deleting Recording methods",()=>{const e=this;for(let r=0;r<10;r++){const n=r>0?r:"";delete e[`CONSUME${n}`],delete e[`SUBRULE${n}`],delete e[`OPTION${n}`],delete e[`OR${n}`],delete e[`MANY${n}`],delete e[`MANY_SEP${n}`],delete e[`AT_LEAST_ONE${n}`],delete e[`AT_LEAST_ONE_SEP${n}`]}delete e.consume,delete e.subrule,delete e.option,delete e.or,delete e.many,delete e.atLeastOne,delete e.ACTION,delete e.BACKTRACK,delete e.LA})}ACTION_RECORD(e){}BACKTRACK_RECORD(e,r){return()=>!0}LA_RECORD(e){return Tf}topLevelRuleRecord(e,r){try{const n=new sl({definition:[],name:e});return n.name=e,this.recordingProdStack.push(n),r.call(this),this.recordingProdStack.pop(),n}catch(n){if(n.KNOWN_RECORDER_ERROR!==!0)try{n.message=n.message+`
This error was thrown during the "grammar recording phase" For more info see:
https://chevrotain.io/docs/guide/internals.html#grammar-recording`}catch{throw n}throw n}}optionInternalRecord(e,r){return Na.call(this,at,e,r)}atLeastOneInternalRecord(e,r){Na.call(this,Ot,r,e)}atLeastOneSepFirstInternalRecord(e,r){Na.call(this,Lt,r,e,Zv)}manyInternalRecord(e,r){Na.call(this,xe,r,e)}manySepFirstInternalRecord(e,r){Na.call(this,_t,r,e,Zv)}orInternalRecord(e,r){return lS.call(this,e,r)}subruleInternalRecord(e,r,n){if(wu(r),!e||K(e,"ruleName")===!1){const u=new Error(`<SUBRULE${Fm(r)}> argument is invalid expecting a Parser method reference but got: <${JSON.stringify(e)}>
inside top level rule: <${this.recordingProdStack[0].name}>`);throw u.KNOWN_RECORDER_ERROR=!0,u}const a=Bn(this.recordingProdStack),i=e.ruleName,o=new ht({idx:r,nonTerminalName:i,label:n?.LABEL,referencedRule:void 0});return a.definition.push(o),this.outputCst?q1:yd}consumeInternalRecord(e,r,n){if(wu(r),!Xy(e)){const o=new Error(`<CONSUME${Fm(r)}> argument is invalid expecting a TokenType reference but got: <${JSON.stringify(e)}>
inside top level rule: <${this.recordingProdStack[0].name}>`);throw o.KNOWN_RECORDER_ERROR=!0,o}const a=Bn(this.recordingProdStack),i=new Se({idx:r,terminalType:e,label:n?.LABEL});return a.definition.push(i),oS}},s(Xi,"GastRecorder"),Xi);function Na(t,e,r,n=!1){wu(r);const a=Bn(this.recordingProdStack),i=Gr(e)?e:e.DEF,o=new t({definition:[],idx:r});return n&&(o.separator=e.SEP),K(e,"MAX_LOOKAHEAD")&&(o.maxLookahead=e.MAX_LOOKAHEAD),this.recordingProdStack.push(o),i.call(this),a.definition.push(o),this.recordingProdStack.pop(),yd}s(Na,"recordProd");function lS(t,e){wu(e);const r=Bn(this.recordingProdStack),n=se(t)===!1,a=n===!1?t:t.DEF,i=new St({definition:[],idx:e,ignoreAmbiguities:n&&t.IGNORE_AMBIGUITIES===!0});K(t,"MAX_LOOKAHEAD")&&(i.maxLookahead=t.MAX_LOOKAHEAD);const o=Eb(a,u=>Gr(u.GATE));return i.hasPredicates=o,r.definition.push(i),V(a,u=>{const l=new bt({definition:[]});i.definition.push(l),K(u,"IGNORE_AMBIGUITIES")?l.ignoreAmbiguities=u.IGNORE_AMBIGUITIES:K(u,"GATE")&&(l.ignoreAmbiguities=!0),this.recordingProdStack.push(l),u.ALT.call(this),this.recordingProdStack.pop()}),yd}s(lS,"recordOrProd");function Fm(t){return t===0?"":`${t}`}s(Fm,"getIdxSuffix");function wu(t){if(t<0||t>Qv){const e=new Error(`Invalid DSL Method idx value: <${t}>
Idx value must be a none negative value smaller than ${Qv+1}`);throw e.KNOWN_RECORDER_ERROR=!0,e}}s(wu,"assertMethodIdxIsValid");var Ji,Y1=(Ji=class{initPerformanceTracer(e){if(K(e,"traceInitPerf")){const r=e.traceInitPerf,n=typeof r=="number";this.traceInitMaxIdent=n?r:1/0,this.traceInitPerf=n?r>0:r}else this.traceInitMaxIdent=0,this.traceInitPerf=Lr.traceInitPerf;this.traceInitIndent=-1}TRACE_INIT(e,r){if(this.traceInitPerf===!0){this.traceInitIndent++;const n=new Array(this.traceInitIndent+1).join(" ");this.traceInitIndent<this.traceInitMaxIdent&&console.log(`${n}--> <${e}>`);const{time:a,value:i}=Ky(r),o=a>10?console.warn:console.log;return this.traceInitIndent<this.traceInitMaxIdent&&o(`${n}<-- <${e}> time: ${a}ms`),this.traceInitIndent--,i}else return r()}},s(Ji,"PerformanceTracer"),Ji);function uS(t,e){e.forEach(r=>{const n=r.prototype;Object.getOwnPropertyNames(n).forEach(a=>{if(a==="constructor")return;const i=Object.getOwnPropertyDescriptor(n,a);i&&(i.get||i.set)?Object.defineProperty(t.prototype,a,i):t.prototype[a]=r.prototype[a]})})}s(uS,"applyMixins");var Tf=Zu(Jr,"",NaN,NaN,NaN,NaN,NaN,NaN);Object.freeze(Tf);var Lr=Object.freeze({recoveryEnabled:!1,maxLookahead:3,dynamicTokensEnabled:!1,outputCst:!0,errorMessageProvider:qa,nodeLocationTracking:"none",traceInitPerf:!1,skipValidations:!1}),$f=Object.freeze({recoveryValueFunc:s(()=>{},"recoveryValueFunc"),resyncEnabled:!0}),yt;(function(t){t[t.INVALID_RULE_NAME=0]="INVALID_RULE_NAME",t[t.DUPLICATE_RULE_NAME=1]="DUPLICATE_RULE_NAME",t[t.INVALID_RULE_OVERRIDE=2]="INVALID_RULE_OVERRIDE",t[t.DUPLICATE_PRODUCTIONS=3]="DUPLICATE_PRODUCTIONS",t[t.UNRESOLVED_SUBRULE_REF=4]="UNRESOLVED_SUBRULE_REF",t[t.LEFT_RECURSION=5]="LEFT_RECURSION",t[t.NONE_LAST_EMPTY_ALT=6]="NONE_LAST_EMPTY_ALT",t[t.AMBIGUOUS_ALTS=7]="AMBIGUOUS_ALTS",t[t.CONFLICT_TOKENS_RULES_NAMESPACE=8]="CONFLICT_TOKENS_RULES_NAMESPACE",t[t.INVALID_TOKEN_NAME=9]="INVALID_TOKEN_NAME",t[t.NO_NON_EMPTY_LOOKAHEAD=10]="NO_NON_EMPTY_LOOKAHEAD",t[t.AMBIGUOUS_PREFIX_ALTS=11]="AMBIGUOUS_PREFIX_ALTS",t[t.TOO_MANY_ALTS=12]="TOO_MANY_ALTS",t[t.CUSTOM_LOOKAHEAD_VALIDATION=13]="CUSTOM_LOOKAHEAD_VALIDATION"})(yt||(yt={}));function zm(t=void 0){return function(){return t}}s(zm,"EMPTY_ALT");var Fn,og=(Fn=class{static performSelfAnalysis(e){throw Error("The **static** `performSelfAnalysis` method has been deprecated. \nUse the **instance** method with the same name instead.")}performSelfAnalysis(){this.TRACE_INIT("performSelfAnalysis",()=>{let e;this.selfAnalysisDone=!0;const r=this.className;this.TRACE_INIT("toFastProps",()=>{Wy(this)}),this.TRACE_INIT("Grammar Recording",()=>{try{this.enableRecording(),V(this.definedRulesNames,a=>{const o=this[a].originalGrammarAction;let u;this.TRACE_INIT(`${a} Rule`,()=>{u=this.topLevelRuleRecord(a,o)}),this.gastProductionsCache[a]=u})}finally{this.disableRecording()}});let n=[];if(this.TRACE_INIT("Grammar Resolving",()=>{n=z_({rules:Ke(this.gastProductionsCache)}),this.definitionErrors=this.definitionErrors.concat(n)}),this.TRACE_INIT("Grammar Validations",()=>{if(Re(n)&&this.skipValidations===!1){const a=j_({rules:Ke(this.gastProductionsCache),tokenTypes:Ke(this.tokensMap),errMsgProvider:Ln,grammarName:r}),i=S_({lookaheadStrategy:this.lookaheadStrategy,rules:Ke(this.gastProductionsCache),tokenTypes:Ke(this.tokensMap),grammarName:r});this.definitionErrors=this.definitionErrors.concat(a,i)}}),Re(this.definitionErrors)&&(this.recoveryEnabled&&this.TRACE_INIT("computeAllProdsFollows",()=>{const a=Db(Ke(this.gastProductionsCache));this.resyncFollows=a}),this.TRACE_INIT("ComputeLookaheadFunctions",()=>{var a,i;(i=(a=this.lookaheadStrategy).initialize)===null||i===void 0||i.call(a,{rules:Ke(this.gastProductionsCache)}),this.preComputeLookaheadFunctions(Ke(this.gastProductionsCache))})),!Fn.DEFER_DEFINITION_ERRORS_HANDLING&&!Re(this.definitionErrors))throw e=j(this.definitionErrors,a=>a.message),new Error(`Parser Definition Errors detected:
${e.join(`
-------------------------------
`)}`)})}constructor(e,r){this.definitionErrors=[],this.selfAnalysisDone=!1;const n=this;if(n.initErrorHandler(r),n.initLexerAdapter(),n.initLooksAhead(r),n.initRecognizerEngine(e,r),n.initRecoverable(r),n.initTreeBuilder(r),n.initContentAssist(),n.initGastRecorder(r),n.initPerformanceTracer(r),K(r,"ignoredIssues"))throw new Error(`The <ignoredIssues> IParserConfig property has been deprecated.
Please use the <IGNORE_AMBIGUITIES> flag on the relevant DSL method instead.
See: https://chevrotain.io/docs/guide/resolving_grammar_errors.html#IGNORING_AMBIGUITIES
For further details.`);this.skipValidations=K(r,"skipValidations")?r.skipValidations:Lr.skipValidations}},s(Fn,"Parser"),Fn);og.DEFER_DEFINITION_ERRORS_HANDLING=!1;uS(og,[x1,G1,j1,B1,K1,U1,W1,V1,H1,Y1]);var Zi,X1=(Zi=class extends og{constructor(e,r=Lr){const n=it(r);n.outputCst=!1,super(e,n)}},s(Zi,"EmbeddedActionsParser"),Zi);function cS(t,e){for(var r=-1,n=t==null?0:t.length,a=Array(n);++r<n;)a[r]=e(t[r],r,t);return a}s(cS,"arrayMap");var fS=cS;function dS(){this.__data__=[],this.size=0}s(dS,"listCacheClear");var J1=dS;function pS(t,e){return t===e||t!==t&&e!==e}s(pS,"eq");var mS=pS;function hS(t,e){for(var r=t.length;r--;)if(mS(t[r][0],e))return r;return-1}s(hS,"assocIndexOf");var gd=hS,Z1=Array.prototype,Q1=Z1.splice;function yS(t){var e=this.__data__,r=gd(e,t);if(r<0)return!1;var n=e.length-1;return r==n?e.pop():Q1.call(e,r,1),--this.size,!0}s(yS,"listCacheDelete");var eG=yS;function gS(t){var e=this.__data__,r=gd(e,t);return r<0?void 0:e[r][1]}s(gS,"listCacheGet");var tG=gS;function vS(t){return gd(this.__data__,t)>-1}s(vS,"listCacheHas");var rG=vS;function TS(t,e){var r=this.__data__,n=gd(r,t);return n<0?(++this.size,r.push([t,e])):r[n][1]=e,this}s(TS,"listCacheSet");var nG=TS;function ia(t){var e=-1,r=t==null?0:t.length;for(this.clear();++e<r;){var n=t[e];this.set(n[0],n[1])}}s(ia,"ListCache");ia.prototype.clear=J1;ia.prototype.delete=eG;ia.prototype.get=tG;ia.prototype.has=rG;ia.prototype.set=nG;var vd=ia;function $S(){this.__data__=new vd,this.size=0}s($S,"stackClear");var aG=$S;function RS(t){var e=this.__data__,r=e.delete(t);return this.size=e.size,r}s(RS,"stackDelete");var iG=RS;function AS(t){return this.__data__.get(t)}s(AS,"stackGet");var sG=AS;function ES(t){return this.__data__.has(t)}s(ES,"stackHas");var oG=ES,lG=typeof global=="object"&&global&&global.Object===Object&&global,CS=lG,uG=typeof self=="object"&&self&&self.Object===Object&&self,cG=CS||uG||Function("return this")(),Fr=cG,fG=Fr.Symbol,pr=fG,bS=Object.prototype,dG=bS.hasOwnProperty,pG=bS.toString,Ol=pr?pr.toStringTag:void 0;function _S(t){var e=dG.call(t,Ol),r=t[Ol];try{t[Ol]=void 0;var n=!0}catch{}var a=pG.call(t);return n&&(e?t[Ol]=r:delete t[Ol]),a}s(_S,"getRawTag");var mG=_S,hG=Object.prototype,yG=hG.toString;function SS(t){return yG.call(t)}s(SS,"objectToString");var gG=SS,vG="[object Null]",TG="[object Undefined]",eT=pr?pr.toStringTag:void 0;function wS(t){return t==null?t===void 0?TG:vG:eT&&eT in Object(t)?mG(t):gG(t)}s(wS,"baseGetTag");var fl=wS;function IS(t){var e=typeof t;return t!=null&&(e=="object"||e=="function")}s(IS,"isObject");var lg=IS,$G="[object AsyncFunction]",RG="[object Function]",AG="[object GeneratorFunction]",EG="[object Proxy]";function NS(t){if(!lg(t))return!1;var e=fl(t);return e==RG||e==AG||e==$G||e==EG}s(NS,"isFunction");var PS=NS,CG=Fr["__core-js_shared__"],qd=CG,tT=(function(){var t=/[^.]+$/.exec(qd&&qd.keys&&qd.keys.IE_PROTO||"");return t?"Symbol(src)_1."+t:""})();function kS(t){return!!tT&&tT in t}s(kS,"isMasked");var bG=kS,_G=Function.prototype,SG=_G.toString;function OS(t){if(t!=null){try{return SG.call(t)}catch{}try{return t+""}catch{}}return""}s(OS,"toSource");var sa=OS,wG=/[\\^$.*+?()[\]{}|]/g,IG=/^\[object .+?Constructor\]$/,NG=Function.prototype,PG=Object.prototype,kG=NG.toString,OG=PG.hasOwnProperty,LG=RegExp("^"+kG.call(OG).replace(wG,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");function LS(t){if(!lg(t)||bG(t))return!1;var e=PS(t)?LG:IG;return e.test(sa(t))}s(LS,"baseIsNative");var DG=LS;function DS(t,e){return t?.[e]}s(DS,"getValue");var xG=DS;function xS(t,e){var r=xG(t,e);return DG(r)?r:void 0}s(xS,"getNative");var dl=xS,MG=dl(Fr,"Map"),Iu=MG,GG=dl(Object,"create"),Nu=GG;function MS(){this.__data__=Nu?Nu(null):{},this.size=0}s(MS,"hashClear");var FG=MS;function GS(t){var e=this.has(t)&&delete this.__data__[t];return this.size-=e?1:0,e}s(GS,"hashDelete");var zG=GS,jG="__lodash_hash_undefined__",BG=Object.prototype,UG=BG.hasOwnProperty;function FS(t){var e=this.__data__;if(Nu){var r=e[t];return r===jG?void 0:r}return UG.call(e,t)?e[t]:void 0}s(FS,"hashGet");var KG=FS,WG=Object.prototype,VG=WG.hasOwnProperty;function zS(t){var e=this.__data__;return Nu?e[t]!==void 0:VG.call(e,t)}s(zS,"hashHas");var qG=zS,HG="__lodash_hash_undefined__";function jS(t,e){var r=this.__data__;return this.size+=this.has(t)?0:1,r[t]=Nu&&e===void 0?HG:e,this}s(jS,"hashSet");var YG=jS;function oa(t){var e=-1,r=t==null?0:t.length;for(this.clear();++e<r;){var n=t[e];this.set(n[0],n[1])}}s(oa,"Hash");oa.prototype.clear=FG;oa.prototype.delete=zG;oa.prototype.get=KG;oa.prototype.has=qG;oa.prototype.set=YG;var rT=oa;function BS(){this.size=0,this.__data__={hash:new rT,map:new(Iu||vd),string:new rT}}s(BS,"mapCacheClear");var XG=BS;function US(t){var e=typeof t;return e=="string"||e=="number"||e=="symbol"||e=="boolean"?t!=="__proto__":t===null}s(US,"isKeyable");var JG=US;function KS(t,e){var r=t.__data__;return JG(e)?r[typeof e=="string"?"string":"hash"]:r.map}s(KS,"getMapDat Line truncated
<${e}> may appears as a prefix path in all these alternatives.
`;return n=n+`See: https://chevrotain.io/docs/guide/resolving_grammar_errors.html#AMBIGUOUS_ALTERNATIVES
For Further details.`,n}s(uN,"buildAmbiguityError");function cN(t){if(t instanceof ht)return"SUBRULE";if(t instanceof at)return"OPTION";if(t instanceof St)return"OR";if(t instanceof Ot)return"AT_LEAST_ONE";if(t instanceof Lt)return"AT_LEAST_ONE_SEP";if(t instanceof _t)return"MANY_SEP";if(t instanceof xe)return"MANY";if(t instanceof Se)return"CONSUME";throw Error("non exhaustive match")}s(cN,"getProductionDslName");function fN(t,e,r){const n=Gj(e.configs.elements,i=>i.state.transitions),a=Jj(n.filter(i=>i instanceof vg).map(i=>i.tokenType),i=>i.tokenTypeIdx);return{actualToken:r,possibleTokenTypes:a,tokenPath:t}}s(fN,"buildAdaptivePredictError");function dN(t,e){return t.edges[e.tokenTypeIdx]}s(dN,"getExistingTargetState");function pN(t,e,r){const n=new Wm,a=[];for(const o of t.elements){if(r.is(o.alt)===!1)continue;if(o.state.type===tc){a.push(o);continue}const u=o.state.transitions.length;for(let l=0;l<u;l++){const c=o.state.transitions[l],f=mN(c,e);f!==void 0&&n.add({state:f,alt:o.alt,stack:o.stack})}}let i;if(a.length===0&&n.size===1&&(i=n),i===void 0){i=new Wm;for(const o of n.elements)ku(o,i)}if(a.length>0&&!vN(i))for(const o of a)i.add(o);return i}s(pN,"computeReachSet");function mN(t,e){if(t instanceof vg&&Zy(e,t.tokenType))return t.target}s(mN,"getReachableTarget");function hN(t,e){let r;for(const n of t.elements)if(e.is(n.alt)===!0){if(r===void 0)r=n.alt;else if(r!==n.alt)return}return r}s(hN,"getUniqueAlt");function bg(t){return{configs:t,edges:{},isAcceptState:!1,prediction:-1}}s(bg,"newDFAState");function qm(t,e,r,n){return n=_g(t,n),e.edges[r.tokenTypeIdx]=n,n}s(qm,"addDFAEdge");function _g(t,e){if(e===Af)return e;const r=e.configs.key,n=t.states[r];return n!==void 0?n:(e.configs.finalize(),t.states[r]=e,e)}s(_g,"addDFAState");function yN(t){const e=new Wm,r=t.transitions.length;for(let n=0;n<r;n++){const i={state:t.transitions[n].target,alt:n,stack:[]};ku(i,e)}return e}s(yN,"computeStartState");function ku(t,e){const r=t.state;if(r.type===tc){if(t.stack.length>0){const a=[...t.stack],o={state:a.pop(),alt:t.alt,stack:a};ku(o,e)}else e.add(t);return}r.epsilonOnlyTransitions||e.add(t);const n=r.transitions.length;for(let a=0;a<n;a++){const i=r.transitions[a],o=gN(t,i);o!==void 0&&ku(o,e)}}s(ku,"closure");function gN(t,e){if(e instanceof TI)return{state:e.target,alt:t.alt,stack:t.stack};if(e instanceof Tg){const r=[...t.stack,e.followState];return{state:e.target,alt:t.alt,stack:r}}}s(gN,"getEpsilonTarget");function vN(t){for(const e of t.elements)if(e.state.type===tc)return!0;return!1}s(vN,"hasConfigInRuleStopState");function TN(t){for(const e of t.elements)if(e.state.type!==tc)return!1;return!0}s(TN,"allConfigsInRuleStopStates");function $N(t){if(TN(t))return!0;const e=RN(t.elements);return AN(e)&&!EN(e)}s($N,"hasConflictTerminatingPrediction");function RN(t){const e=new Map;for(const r of t){const n=Eg(r,!1);let a=e.get(n);a===void 0&&(a={},e.set(n,a)),a[r.alt]=!0}return e}s(RN,"getConflictingAltSets");function AN(t){for(const e of Array.from(t.values()))if(Object.keys(e).length>1)return!0;return!1}s(AN,"hasConflictingAltSet");function EN(t){for(const e of Array.from(t.values()))if(Object.keys(e).length===1)return!0;return!1}s(EN,"hasStateAssociatedWithOneAlt");Du();var ss,CN=(ss=class{constructor(){this.nodeStack=[]}get current(){return this.nodeStack[this.nodeStack.length-1]??this.rootNode}buildRootNode(e){return this.rootNode=new wg(e),this.rootNode.root=this.rootNode,this.nodeStack=[this.rootNode],this.rootNode}buildCompositeNode(e){const r=new Nd;return r.grammarSource=e,r.root=this.rootNode,this.current.content.push(r),this.nodeStack.push(r),r}buildLeafNode(e,r){const n=new Ef(e.startOffset,e.image.length,Tu(e),e.tokenType,!r);return n.grammarSource=r,n.root=this.rootNode,this.current.content.push(n),n}removeNode(e){const r=e.container;if(r){const n=r.content.indexOf(e);n>=0&&r.content.splice(n,1)}}addHiddenNodes(e){const r=[];for(const i of e){const o=new Ef(i.startOffset,i.image.length,Tu(i),i.tokenType,!0);o.root=this.rootNode,r.push(o)}let n=this.current,a=!1;if(n.content.length>0){n.content.push(...r);return}for(;n.container;){const i=n.container.content.indexOf(n);if(i>0){n.container.content.splice(i,0,...r),a=!0;break}n=n.container}a||this.rootNode.content.unshift(...r)}construct(e){const r=this.current;typeof e.$type=="string"&&!e.$infixName&&(this.current.astNode=e),e.$cstNode=r;const n=this.nodeStack.pop();n?.content.length===0&&this.removeNode(n)}},s(ss,"CstNodeBuilder"),ss),os,Sg=(os=class{get hidden(){return!1}get astNode(){const e=typeof this._astNode?.$type=="string"?this._astNode:this.container?.astNode;if(!e)throw new Error("This node has no associated AST element");return e}set astNode(e){this._astNode=e}get text(){return this.root.fullText.substring(this.offset,this.end)}},s(os,"AbstractCstNode"),os),ls,Ef=(ls=class extends Sg{get offset(){return this._offset}get length(){return this._length}get end(){return this._offset+this._length}get hidden(){return this._hidden}get Line truncated
`;case"r":return"\r";case"t":return" ";case"v":return"\v";case"0":return"\0";default:return c}}s(r,"convertEscapeCharacter");function n(c){return c.charAt(0)==="^"?c.substring(1):c}s(n,"convertID"),t.convertID=n;function a(c){return parseInt(c)}s(a,"convertInt"),t.convertInt=a;function i(c){return BigInt(c)}s(i,"convertBigint"),t.convertBigint=i;function o(c){return new Date(c)}s(o,"convertDate"),t.convertDate=o;function u(c){return Number(c)}s(u,"convertNumber"),t.convertNumber=u;function l(c){return c.toLowerCase()==="true"}s(l,"convertBoolean"),t.convertBoolean=l})(or||(or={}));var $e={};kf($e,$h(Lf()));function Ld(){return new Promise(t=>{typeof setImmediate>"u"?setTimeout(t,0):setImmediate(t)})}s(Ld,"delayNextTick");var Fc=0,zN=10;function Dd(){return Fc=performance.now(),new $e.CancellationTokenSource}s(Dd,"startCancelableOperation");function Mg(t){zN=t}s(Mg,"setInterruptionPeriod");var cr=Symbol("OperationCancelled");function fa(t){return t===cr}s(fa,"isOperationCancelled");async function Ye(t){if(t===$e.CancellationToken.None)return;const e=performance.now();if(e-Fc>=zN&&(Fc=e,await Ld(),Fc=performance.now()),t.isCancellationRequested)throw cr}s(Ye,"interruptAndCheck");var $s,Dr=($s=class{constructor(){this.promise=new Promise((e,r)=>{this.resolve=n=>(e(n),this),this.reject=n=>(r(n),this)})}},s($s,"Deferred"),$s),Hr,_T=(Hr=class{constructor(e,r,n,a){this._uri=e,this._languageId=r,this._version=n,this._content=a,this._lineOffsets=void 0}get uri(){return this._uri}get languageId(){return this._languageId}get version(){return this._version}getText(e){if(e){const r=this.offsetAt(e.start),n=this.offsetAt(e.end);return this._content.substring(r,n)}return this._content}update(e,r){for(const n of e)if(Hr.isIncremental(n)){const a=Fg(n.range),i=this.offsetAt(a.start),o=this.offsetAt(a.end);this._content=this._content.substring(0,i)+n.text+this._content.substring(o,this._content.length);const u=Math.max(a.start.line,0),l=Math.max(a.end.line,0);let c=this._lineOffsets;const f=Hm(n.text,!1,i);if(l-u===f.length)for(let p=0,y=f.length;p<y;p++)c[p+u+1]=f[p];else f.length<1e4?c.splice(u+1,l-u,...f):this._lineOffsets=c=c.slice(0,u+1).concat(f,c.slice(l+1));const d=n.text.length-(o-i);if(d!==0)for(let p=u+1+f.length,y=c.length;p<y;p++)c[p]=c[p]+d}else if(Hr.isFull(n))this._content=n.text,this._lineOffsets=void 0;else throw new Error("Unknown change event received");this._version=r}getLineOffsets(){return this._lineOffsets===void 0&&(this._lineOffsets=Hm(this._content,!0)),this._lineOffsets}positionAt(e){e=Math.max(Math.min(e,this._content.length),0);const r=this.getLineOffsets();let n=0,a=r.length;if(a===0)return{line:0,character:e};for(;n<a;){const o=Math.floor((n+a)/2);r[o]>e?a=o:n=o+1}const i=n-1;return e=this.ensureBeforeEOL(e,r[i]),{line:i,character:e-r[i]}}offsetAt(e){const r=this.getLineOffsets();if(e.line>=r.length)return this._content.length;if(e.line<0)return 0;const n=r[e.line];if(e.character<=0)return n;const a=e.line+1<r.length?r[e.line+1]:this._content.length,i=Math.min(n+e.character,a);return this.ensureBeforeEOL(i,n)}ensureBeforeEOL(e,r){for(;e>r&&Gg(this._content.charCodeAt(e-1));)e--;return e}get lineCount(){return this.getLineOffsets().length}static isIncremental(e){const r=e;return r!=null&&typeof r.text=="string"&&r.range!==void 0&&(r.rangeLength===void 0||typeof r.rangeLength=="number")}static isFull(e){const r=e;return r!=null&&typeof r.text=="string"&&r.range===void 0&&r.rangeLength===void 0}},s(Hr,"FullTextDocument"),Hr),_f;(function(t){function e(a,i,o,u){return new _T(a,i,o,u)}s(e,"create"),t.create=e;function r(a,i,o){if(a instanceof _T)return a.update(i,o),a;throw new Error("TextDocument.update: document must be created by TextDocument.create")}s(r,"update"),t.update=r;function n(a,i){const o=a.getText(),u=Sf(i.map(jN),(f,d)=>{const p=f.range.start.line-d.range.start.line;return p===0?f.range.start.character-d.range.start.character:p});let l=0;const c=[];for(const f of u){const d=a.offsetAt(f.range.start);if(d<l)throw new Error("Overlapping edit");d>l&&c.push(o.substring(l,d)),f.newText.length&&c.push(f.newText),l=a.offsetAt(f.range.end)}return c.push(o.substr(l)),c.join("")}s(n,"applyEdits"),t.applyEdits=n})(_f||(_f={}));function Sf(t,e){if(t.length<=1)return t;const r=t.length/2|0,n=t.slice(0,r),a=t.slice(r);Sf(n,e),Sf(a,e);let i=0,o=0,u=0;for(;i<n.length&&o<a.length;)e(n[i],a[o])<=0?t[u++]=n[i++]:t[u++]=a[o++];for(;i<n.length;)t[u++]=n[i++];for(;o<a.length;)t[u++]=a[o++];return t}s(Sf,"mergeSort");function Hm(t,e,r=0){const n=e?[r]:[];for(let a=0;a<t.length;a++){const i=t.charCodeAt(a);Gg(i)&&(i===13&&a+1<t.length&&t.charCodeAt(a+1)===10&&a++,n.push(r+a+1))}return n}s(Hm,"computeLineOffsets");function Gg(t){return t===13||t===10}s(Gg,"isEOL");function Fg(t){const e=t.start,r=t.end;return e.line>r.line||e.line===r.line&&e.character>r.character?{start:r,end:e}:t}s(Fg,"getWellformedRange");function jN(t){const e=Fg(t.range);return e!==t.range?{newText:t.newText,range:e}:t}s(jN,"getWellfo Line truncated
${r}`),this.inline?`{${e}}`:e}toMarkdown(e){return e?.renderTag?.(this)??this.toMarkdownDefault(e)}toMarkdownDefault(e){const r=this.content.toMarkdown(e);if(this.inline){const i=vP(this.name,r,e??{});if(typeof i=="string")return i}let n="";e?.tag==="italic"||e?.tag===void 0?n="*":e?.tag==="bold"?n="**":e?.tag==="bold-italic"&&(n="***");let a=`${n}@${this.name}${n}`;return this.content.inlines.length===1?a=`${a} — ${r}`:this.content.inlines.length>1&&(a=`${a}
${r}`),this.inline?`{${a}}`:a}},s(Qs,"JSDocTagImpl"),Qs);function vP(t,e,r){if(t==="linkplain"||t==="linkcode"||t==="link"){const n=e.indexOf(" ");let a=e;if(n>0){const o=Pf(e,n);a=e.substring(o),e=e.substring(0,n)}return(t==="linkcode"||t==="link"&&r.link==="code")&&(a=`\`${a}\``),r.renderLink?.(e,a)??TP(e,a)}}s(vP,"renderInlineTag");function TP(t,e){try{return It.parse(t,!0),`[${e}](${t})`}catch{return t}}s(TP,"renderLinkDefault");var eo,Jm=(eo=class{constructor(e,r){this.inlines=e,this.range=r}toString(){let e="";for(let r=0;r<this.inlines.length;r++){const n=this.inlines[r],a=this.inlines[r+1];e+=n.toString(),a&&a.range.start.line>n.range.start.line&&(e+=`
`)}return e}toMarkdown(e){let r="";for(let n=0;n<this.inlines.length;n++){const a=this.inlines[n],i=this.inlines[n+1];r+=a.toMarkdown(e),i&&i.range.start.line>a.range.start.line&&(r+=`
`)}return r}},s(eo,"JSDocTextImpl"),eo),to,$P=(to=class{constructor(e,r){this.text=e,this.range=r}toString(){return this.text}toMarkdown(){return this.text}},s(to,"JSDocLineImpl"),to);function Zm(t){return t.endsWith(`
`)?`
`:`
`}s(Zm,"fillNewlines");var ro,RP=(ro=class{constructor(e){this.indexManager=e.shared.workspace.IndexManager,this.commentProvider=e.documentation.CommentProvider}getDocumentation(e){const r=this.commentProvider.getComment(e);if(r&&Xg(r))return Yg(r).toMarkdown({renderLink:s((a,i)=>this.documentationLinkRenderer(e,a,i),"renderLink"),renderTag:s(a=>this.documentationTagRenderer(e,a),"renderTag")})}documentationLinkRenderer(e,r,n){const a=this.findNameInLocalSymbols(e,r)??this.findNameInGlobalScope(e,r);if(a&&a.nameSegment){const i=a.nameSegment.range.start.line+1,o=a.nameSegment.range.start.character+1,u=a.documentUri.with({fragment:`L${i},${o}`});return`[${n}](${u.toString()})`}else return}documentationTagRenderer(e,r){}findNameInLocalSymbols(e,r){const a=Vt(e).localSymbols;if(!a)return;let i=e;do{const u=a.getStream(i).find(l=>l.name===r);if(u)return u;i=i.$container}while(i)}findNameInGlobalScope(e,r){return this.indexManager.allElements().find(a=>a.name===r)}},s(ro,"JSDocDocumentationProvider"),ro),no,AP=(no=class{constructor(e){this.grammarConfig=()=>e.parser.GrammarConfig}getComment(e){return Kg(e)?e.$comment:Zh(e.$cstNode,this.grammarConfig().multilineCommentRules)?.text}},s(no,"DefaultCommentProvider"),no),ao,EP=(ao=class{constructor(e){this.syncParser=e.parser.LangiumParser}parse(e,r){return Promise.resolve(this.syncParser.parse(e))}},s(ao,"DefaultAsyncParser"),ao),io,gB=(io=class{constructor(e){this.threadCount=8,this.terminationDelay=200,this.workerPool=[],this.queue=[],this.hydrator=e.serializer.Hydrator}initializeWorkers(){for(;this.workerPool.length<this.threadCount;){const e=this.createWorker();e.onReady(()=>{if(this.queue.length>0){const r=this.queue.shift();r&&(e.lock(),r.resolve(e))}}),this.workerPool.push(e)}}async parse(e,r){const n=await this.acquireParserWorker(r),a=new Dr;let i;const o=r.onCancellationRequested(()=>{i=setTimeout(()=>{this.terminateWorker(n)},this.terminationDelay)});return n.parse(e).then(u=>{const l=this.hydrator.hydrate(u);a.resolve(l)}).catch(u=>{a.reject(u)}).finally(()=>{o.dispose(),clearTimeout(i)}),a.promise}terminateWorker(e){e.terminate();const r=this.workerPool.indexOf(e);r>=0&&this.workerPool.splice(r,1)}async acquireParserWorker(e){this.initializeWorkers();for(const n of this.workerPool)if(n.ready)return n.lock(),n;const r=new Dr;return e.onCancellationRequested(()=>{const n=this.queue.indexOf(r);n>=0&&this.queue.splice(n,1),r.reject(cr)}),this.queue.push(r),r.promise}},s(io,"AbstractThreadedAsyncParser"),io),so,vB=(so=class{get ready(){return this._ready}get onReady(){return this.onReadyEmitter.event}constructor(e,r,n,a){this.onReadyEmitter=new Gd.Emitter,this.deferred=new Dr,this._ready=!0,this._parsing=!1,this.sendMessage=e,this._terminate=a,r(i=>{const o=i;this.deferred.resolve(o),this.unlock()}),n(i=>{this.deferred.reject(i),this.unlock()})}terminate(){this.deferred.reject(cr),this._terminate()}lock(){this._ready=!1}unlock(){this._parsing=!1,this._ready=!0,this.onReadyEmitter.fire()}parse(e){if(this._parsing)throw new Error("Parser worker is busy");return this._parsing=!0,this.deferred=new Dr,this.sendMessage(e),this.deferred.promise}},s(so,"ParserWorker"),so),oo,CP=(oo=class{constructor(){this.previousTokenSource=new $e.CancellationTokenSource,this.writeQueue=[],this.readQueue=[],this.done=!0}write(e){this.cancelWrite();const r=Dd();return this.previousTokenSource=r,this.enqueue(this.writeQueue,e,r.token)}read(e){return this.enqueue(this.readQueue,e)}enqueue(e,r,n=$e.CancellationToken.None){const a=new Dr,i={action:r,deferred:a,cancellationToken:n};return e.push(i),this.performNextOperation(),a.promise}async performNextOperation(){if(!this.done)return;const e=[];if(this.writeQueue.length>0)e.push(this.writeQueue.shift());else if(this.readQueue.length>0)e.push(...this.readQueue.splice(0,this.readQueue.length));else return;this.done=!1,await Promise.all(e.map(async({action:r,deferred:n,cancellationToken:a})=>{try{const i=await Promise.resolve().then(()=>r(a));n.resolve(i)}catch(i){fa(i)?n.resolve(void 0):n.reject(i)}})),this.done=!0,this.performNextOperation()}cancelWrite(){this.previousTokenSource.cancel()}},s(oo,"DefaultWorkspaceLock"),oo),lo,bP=(lo=class{constructor(e){this.grammarElementIdMap=new wf,this.tokenTypeIdMap=new wf,this.grammar=e.Grammar,this.lexer=e.parser.Lexer,this.linker=e.references.Linker}dehydrate(e){return{lexerErrors:e.lexerErrors,lexerReport:e.lexerReport?this.dehydrateLexerReport(e.lexerReport):void 0,parserErrors:e.parserErrors.map(r=>({...r,message:r.message})),value:this.dehydrateAstNode(e.value,this.createDehyrationContext(e.value))}}dehydrateLexerReport(e){return e}createDehyrationContext(e){const r=new Map,n=new Map;for(const a of qt(e))r.set(a,{});if(e.$cstNode)for(const a of Jo(e.$cstNode))n.set(a,{});return{astNodes:r,cstNodes:n}}dehydrateAstNode(e,r){const n=r.astNodes.get(e);n.$type=e.$type,n.$containerIndex=e.$containerIndex,n.$containerProperty=e.$containerProperty,e.$cstNode!==void 0&&(n.$cstNode=this.dehydrateCstN Line truncated
`.includes(e[r-1])}matchWhitespace(e,r,n,a){this.whitespaceRegExp.lastIndex=r;const i=this.whitespaceRegExp.exec(e);return{currIndentLevel:i?.[0].length??0,prevIndentLevel:this.indentationStack.at(-1),match:i}}createIndentationTokenInstance(e,r,n,a){const i=this.getLineNumber(r,a);return Zu(e,n,a,a+n.length,i,i,1,n.length)}getLineNumber(e,r){return e.substring(0,r).split(/\r\n|\r|\n/).length}indentMatcher(e,r,n,a){if(!this.isStartOfLine(e,r))return null;const{currIndentLevel:i,prevIndentLevel:o,match:u}=this.matchWhitespace(e,r,n,a);return i<=o?null:(this.indentationStack.push(i),u)}dedentMatcher(e,r,n,a){if(!this.isStartOfLine(e,r))return null;const{currIndentLevel:i,prevIndentLevel:o,match:u}=this.matchWhitespace(e,r,n,a);if(i>=o)return null;const l=this.indentationStack.lastIndexOf(i);if(l===-1)return this.diagnostics.push({severity:"error",message:`Invalid dedent level ${i} at offset: ${r}. Current indentation stack: ${this.indentationStack}`,offset:r,length:u?.[0]?.length??0,line:this.getLineNumber(e,r),column:1}),null;const c=this.indentationStack.length-l-1,f=e.substring(0,r).match(/[\r\n]+$/)?.[0].length??1;for(let d=0;d<c;d++){const p=this.createIndentationTokenInstance(this.dedentTokenType,e,"",r-(f-1));n.push(p),this.indentationStack.pop()}return null}buildTerminalToken(e){const r=super.buildTerminalToken(e),{indentTokenName:n,dedentTokenName:a,whitespaceTokenName:i}=this.options;return r.name===n?this.indentTokenType:r.name===a?this.dedentTokenType:r.name===i?Xa({name:i,pattern:this.whitespaceRegExp,group:pt.SKIPPED}):r}flushRemainingDedents(e){const r=[];for(;this.indentationStack.length>1;)r.push(this.createIndentationTokenInstance(this.dedentTokenType,e,"",e.length)),this.indentationStack.pop();return this.indentationStack=[0],r}},s(uo,"IndentationAwareTokenBuilder"),uo),co,TB=(co=class extends Hg{constructor(e){if(super(e),e.parser.TokenBuilder instanceof SP)this.indentationTokenBuilder=e.parser.TokenBuilder;else throw new Error("IndentationAwareLexer requires an accompanying IndentationAwareTokenBuilder")}tokenize(e,r=qg){const n=super.tokenize(e),a=n.report;r?.mode==="full"&&n.tokens.push(...a.remainingDedents),a.remainingDedents=[];const{indentTokenType:i,dedentTokenType:o}=this.indentationTokenBuilder,u=i.tokenTypeIdx,l=o.tokenTypeIdx,c=[],f=n.tokens.length-1;for(let d=0;d<f;d++){const p=n.tokens[d],y=n.tokens[d+1];if(p.tokenTypeIdx===u&&y.tokenTypeIdx===l){d++;continue}c.push(p)}return f>=0&&c.push(n.tokens[f]),n.tokens=c,n}},s(co,"IndentationAwareLexer"),co),nv={};en(nv,{AstUtils:()=>_h,BiMap:()=>wf,Cancellation:()=>$e,ContextCache:()=>Md,CstUtils:()=>Eh,DONE_RESULT:()=>ct,Deferred:()=>Dr,Disposable:()=>Gn,DisposableCache:()=>xd,DocumentCache:()=>XN,EMPTY_STREAM:()=>Vo,ErrorWithLocation:()=>Uf,GrammarUtils:()=>ry,MultiMap:()=>xr,OperationCancelled:()=>cr,Reduction:()=>vu,RegExpUtils:()=>ay,SimpleCache:()=>Bg,StreamImpl:()=>ur,TreeStreamImpl:()=>qo,URI:()=>It,UriTrie:()=>zg,UriUtils:()=>dt,WorkspaceCache:()=>Ug,assertCondition:()=>ny,assertUnreachable:()=>tn,delayNextTick:()=>Ld,interruptAndCheck:()=>Ye,isOperationCancelled:()=>fa,loadGrammarFromJson:()=>Ze,setInterruptionPeriod:()=>Mg,startCancelableOperation:()=>Dd,stream:()=>de});kf(nv,Gd);var fo,wP=(fo=class{stat(e){throw new Error("No file system is available.")}statSync(e){throw new Error("No file system is available.")}async exists(){return!1}existsSync(){return!1}readBinary(){throw new Error("No file system is available.")}readBinarySync(){throw new Error("No file system is available.")}readFile(){throw new Error("No file system is available.")}readFileSync(){throw new Error("No file system is available.")}async readDirectory(){return[]}readDirectorySync(){return[]}},s(fo,"EmptyFileSystemProvider"),fo),st={fileSystemProvider:s(()=>new wP,"fileSystemProvider")},$B={Grammar:s(()=>{},"Grammar"),LanguageMetaData:s(()=>({caseInsensitive:!1,fileExtensions:[".langium"],languageId:"langium"}),"LanguageMetaData")},RB={AstReflection:s(()=>new qh,"AstReflection")};function IP(){const t=re(Je(st),RB),e=re(Xe({shared:t}),$B);return t.ServiceRegistry.register(e),e}s(IP,"createMinimalGrammarServices");function Ze(t){const e=IP(),r=e.serializer.JsonSerializer.deserialize(t);return e.shared.workspace.LangiumDocumentFactory.fromModel(r,It.parse(`memory:/${r.name??"grammar"}.langium`)),r}s(Ze,"loadGrammarFromJson");kf(z$,nv);var po,AB=(po=class{constructor(e){this.activeCategories=new Set,this.allCategories=new Set(["validating","parsing","linking"]),this.activeCategories=e??new Set(this.allCategories),this.records=new xr}isActive(e){return this.activeCategories.has(e)}start(...e){e?e.forEach(r=>this.activeCategories.add(r)):this.activeCategories=new Set(this.allCategories)}stop(...e){e?e.forEach(r=>this.activeCategories.delete(r)):this.activeCategories.clear()}createTask(e,r){if(!this.isActive(e))throw new Error(`Category "${e}" is not active.`);return console.log(`Creating profiling task for '${e}.${r}'.`),new NP(n=>this.records.add(e,this.dumpLine truncated
`)}}},s(yo,"AbstractMermaidValueConverter"),yo),go,pl=(go=class extends vr{runCustomConverter(e,r,n){}},s(go,"CommonValueConverter"),go),vo,$t=(vo=class extends Od{constructor(e){super(),this.keywords=new Set(e)}buildKeywordTokens(e,r,n){const a=super.buildKeywordTokens(e,r,n);return a.forEach(i=>{this.keywords.has(i.name)&&i.PATTERN!==void 0&&(i.PATTERN=new RegExp(i.PATTERN.toString()+"(?:(?=%%)|(?!\\S))"))}),a}},s(vo,"AbstractMermaidTokenBuilder"),vo),To;To=class extends $t{},s(To,"CommonTokenBuilder");var $o,zU=($o=class extends $t{constructor(){super(["radar-beta"])}},s($o,"RadarTokenBuilder"),$o),kP={parser:{TokenBuilder:s(()=>new zU,"TokenBuilder"),ValueConverter:s(()=>new pl,"ValueConverter")}};function OP(t=st){const e=re(Je(t),Tt),r=re(Xe({shared:e}),wU,kP);return e.ServiceRegistry.register(r),{shared:e,Radar:r}}s(OP,"createRadarServices");var Ro,jU=(Ro=class extends $t{constructor(){super(["railroad-beta"])}},s(Ro,"RailroadTokenBuilder"),Ro),w$=s(t=>{const e=t.slice(1,-1);let r="";for(let n=0;n<e.length;n++){const a=e[n];if(a==="\\"&&n+1<e.length){n++;const i=e[n];switch(i){case"n":r+=`
`;break;case"r":r+="\r";break;case"t":r+=" ";break;default:r+=i}continue}r+=a}return r},"decodeEscapedString"),Ao,BU=(Ao=class extends vr{runConverter(e,r,n){const a=super.runConverter(e,r,n);if(e.name==="TITLE"&&typeof a=="string"){const i=a.trim();if(i.startsWith('"')&&i.endsWith('"')||i.startsWith("'")&&i.endsWith("'"))return w$(i)}return a}runCustomConverter(e,r,n){if(e.name==="RR_STRING")return w$(r)}},s(Ao,"RailroadValueConverter"),Ao),LP={parser:{TokenBuilder:s(()=>new jU,"TokenBuilder"),ValueConverter:s(()=>new BU,"ValueConverter")}};function DP(t=st){const e=re(Je(t),Tt),r=re(Xe({shared:e}),PU,LP);return e.ServiceRegistry.register(r),{shared:e,Railroad:r}}s(DP,"createRailroadServices");var Eo,UU=(Eo=class extends $t{constructor(){super(["railroad-ebnf-beta"])}},s(Eo,"RailroadEbnfTokenBuilder"),Eo),I$=s(t=>{const e=t.slice(1,-1);let r="";for(let n=0;n<e.length;n++){const a=e[n];if(a==="\\"&&n+1<e.length){n++;const i=e[n];switch(i){case"n":r+=`
`;break;case"r":r+="\r";break;case"t":r+=" ";break;default:r+=i}continue}r+=a}return r},"decodeEscapedString"),Co,KU=(Co=class extends vr{runConverter(e,r,n){const a=super.runConverter(e,r,n);if(e.name==="TITLE"&&typeof a=="string"){const i=a.trim();if(i.startsWith('"')&&i.endsWith('"')||i.startsWith("'")&&i.endsWith("'"))return I$(i)}return a}runCustomConverter(e,r,n){if(e.name==="EBNF_STRING")return I$(r);if(e.name==="EBNF_SPECIAL_SEQUENCE")return r.slice(1,-1).trim()}},s(Co,"RailroadEbnfValueConverter"),Co),xP={parser:{TokenBuilder:s(()=>new UU,"TokenBuilder"),ValueConverter:s(()=>new KU,"ValueConverter")}};function MP(t=st){const e=re(Je(t),Tt),r=re(Xe({shared:e}),NU,xP);return e.ServiceRegistry.register(r),{shared:e,RailroadEbnf:r}}s(MP,"createRailroadEbnfServices");var bo,WU=(bo=class extends $t{constructor(){super(["railroad-abnf-beta"])}},s(bo,"RailroadAbnfTokenBuilder"),bo),_o,VU=(_o=class extends vr{runConverter(e,r,n){const a=super.runConverter(e,r,n);if(e.name==="TITLE"&&typeof a=="string"){const i=a.trim();if(i.startsWith('"')&&i.endsWith('"')||i.startsWith("'")&&i.endsWith("'"))return i.slice(1,-1)}return a}runCustomConverter(e,r,n){if(e.name==="ABNF_STRING")return r.slice(1,-1)}},s(_o,"RailroadAbnfValueConverter"),_o),GP={parser:{TokenBuilder:s(()=>new WU,"TokenBuilder"),ValueConverter:s(()=>new VU,"ValueConverter")}};function FP(t=st){const e=re(Je(t),Tt),r=re(Xe({shared:e}),IU,GP);return e.ServiceRegistry.register(r),{shared:e,RailroadAbnf:r}}s(FP,"createRailroadAbnfServices");var So,qU=(So=class extends $t{constructor(){super(["railroad-peg-beta"])}},s(So,"RailroadPegTokenBuilder"),So),N$=s(t=>{const e=t.slice(1,-1);let r="";for(let n=0;n<e.length;n++){const a=e[n];if(a==="\\"&&n+1<e.length){n++;const i=e[n];switch(i){case"n":r+=`
`;break;case"r":r+="\r";break;case"t":r+=" ";break;default:r+=i}continue}r+=a}return r},"decodeEscapedString"),wo,HU=(wo=class extends vr{runConverter(e,r,n){const a=super.runConverter(e,r,n);if(e.name==="TITLE"&&typeof a=="string"){const i=a.trim();if(i.startsWith('"')&&i.endsWith('"')||i.startsWith("'")&&i.endsWith("'"))return N$(i)}return a}runCustomConverter(e,r,n){if(e.name==="PEG_STRING")return N$(r)}},s(wo,"RailroadPegValueConverter"),wo),zP={parser:{TokenBuilder:s(()=>new qU,"TokenBuilder"),ValueConverter:s(()=>new HU,"ValueConverter")}};function jP(t=st){const e=re(Je(t),Tt),r=re(Xe({shared:e}),kU,zP);return e.ServiceRegistry.register(r),{shared:e,RailroadPeg:r}}s(jP,"createRailroadPegServices");var Io,YU=(Io=class extends $t{constructor(){super(["treemap"])}},s(Io,"TreemapTokenBuilder"),Io),XU=/classDef\s+([A-Z_a-z]\w+)(?:\s+([^\n\r;]*))?;?/,No,JU=(No=class extends vr{runCustomConverter(e,r,n){if(e.name==="NUMBER2")return parseFloat(r.replace(/,/g,""));if(e.name==="SEPARATOR")return r.substring(1,r.length-1);if(e.name==="STRING2")return r.substring(1,r.length-1);if(e.name==="INDENTATION")return r.length;if(e.name==="ClassDef"){if(typeof r!="string")return r;const a=XU.exec(r);if(a)return{$type:"ClassDefStatement",className:a[1],styleText:a[2]||void 0}}}},s(No,"TreemapValueConverter"),No);function BP(t){const e=t.validation.TreemapValidator,r=t.validation.ValidationRegistry;if(r){const n={Treemap:e.checkSingleRoot.bind(e)};r.register(n,e)}}s(BP,"registerValidationChecks");var Po,ZU=(Po=class{checkSingleRoot(e,r){let n;for(const a of e.TreemapRows)a.item&&(n===void 0&&a.indent===void 0?n=0:a.indent===void 0?r("error","Multiple root nodes are not allowed in a treemap.",{node:a,property:"item"}):n!==void 0&&n>=parseInt(a.indent,10)&&r("error","Multiple root nodes are not allowed in a treemap.",{node:a,property:"item"}))}},s(Po,"TreemapValidator"),Po),UP={parser:{TokenBuilder:s(()=>new YU,"TokenBuilder"),ValueConverter:s(()=>new JU,"ValueConverter")},validation:{TreemapValidator:s(()=>new ZU,"TreemapValidator")}};function KP(t=st){const e=re(Je(t),Tt),r=re(Xe({shared:e}),OU,UP);return e.ServiceRegistry.register(r),BP(r),{shared:e,Treemap:r}}s(KP,"createTreemapServices");var ko,QU=(ko=class extends vr{runCustomConverter(e,r,n){if(e.name.toUpperCase()==="LINK_LABEL")return r.substring(1).trim()}},s(ko,"WardleyValueConverter"),ko),WP={parser:{ValueConverter:s(()=>new QU,"ValueConverter")}};function VP(t=st){const e=re(Je(t),Tt),r=re(Xe({shared:e}),DU,WP);return e.ServiceRegistry.register(r),{shared:e,Wardley:r}}s(VP,"createWardleyServices");var Oo,eK=(Oo=class extends $t{constructor(){super(["cynefin-beta"])}},s(Oo,"CynefinTokenBuilder"),Oo),qP={parser:{TokenBuilder:s(()=>new eK,"TokenBuilder"),ValueConverter:s(()=>new pl,"ValueConverter")}};function HP(t=st){const e=re(Je(t),Tt),r=re(Xe({shared:e}),AU,qP);return e.ServiceRegistry.register(r),{shared:e,Cynefin:r}}s(HP,"createCynefinServices");var Lo,tK=(Lo=class extends $t{constructor(){super(["gitGraph"])}},s(Lo,"GitGraphTokenBuilder"),Lo),YP={parser:{TokenBuilder:s(()=>new tK,"TokenBuilder"),ValueConverter:s(()=>new pl,"ValueConverter")}};function XP(t=st){const e=re(Je(t),Tt),r=re(Xe({shared:e}),CU,YP);return e.ServiceRegistry.register(r),{shared:e,GitGraph:r}}s(XP,"createGitGraphServices");var Do,rK=(Do=class extends $t{constructor(){super(["info","showInfo"])}},s(Do,"InfoTokenBuilder"),Do),JP={parser:{TokenBuilder:s(()=>new rK,"TokenBuilder"),ValueConverter:s(()=>new pl,"ValueConverter")}};function ZP(t=st){const e=re(Je(t),Tt),r=re(Xe({shared:e}),bU,JP);return e.ServiceRegistry.register(r),{shared:e,Info:r}}s(ZP,"createInfoServices");var xo,nK=(xo=class extends $t{constructor(){super(["packet"])}},s(xo,"PacketTokenBuilder"),xo),QP={parser:{TokenBuilder:s(()=>new nK,"TokenBuilder"),ValueConverter:s(()=>new pl,"ValueConverter")}};function ek(t=st){const e=re(Je(t),Tt),r=re(Xe({shared:e}),_U,QP);return e.ServiceRegistry.register(r),{shared:e,Packet:r}}s(ek,"createPacketServices");var Mo,aK=(Mo=class extends $t{constructor(){super(["pie","showData"])}},s(Mo,"PieTokenBuilder"),Mo),Go,iK=(Go=class extends vr{runCustomConverter(e,r,n){if(e.name==="PIE_SECTION_LABEL")return r.replace(/"/g,"").trim()}},s(Go,"PieValueConverter"),Go),tk={parser:{TokenBuilder:s(()=>new aK,"TokenBuilder"),ValueConverter:s(()=>new iK,"ValueConverter")}};function rk(t=st){const e=re(Je(t),Tt),r=re(Xe({shared:e}),SU,tk);return e.ServiceRegistry.register(r),{shared:e,Pie:r}}s(rk,"createPieServices");var Fo,sK=(Fo=class extends vr{runCustomConverter(e,r,n){if(e.name==="INDENTATION")return r?.length||0;if(e.name==="QUOTED_NAME")return r.substring(1,r.length-1);if(e.name==="BARE_NAME")return r.replace(/[\t ]+$/,"");if(e.name==="CLASS_ANNOTATION")return r.trim().substring(3).trim();if(e.name==="ICON_ANNOTATION"){const a=r.trim();return a.substring(5,a.length-1)}if(e.name==="DESC_ANNOTATION")return r.trim().substring(2).trim()}},s(Fo,"TreeViewValueConverter"),Fo),zo,oK=(zo=class extends Line truncated
`),n=e.parserErrors.map(a=>{const i=a.token.startLine!==void 0&&!isNaN(a.token.startLine)?a.token.startLine:"?",o=a.token.startColumn!==void 0&&!isNaN(a.token.startColumn)?a.token.startColumn:"?";return`Parse error on line ${i}, column ${o}: ${a.message}`}).join(`
`);super(`Parsing failed: ${r} ${n}`),this.result=e}},s(Wo,"MermaidParseError"),Wo);const hK=Object.freeze(Object.defineProperty({__proto__:null,InfoModule:JP,createInfoServices:ZP},Symbol.toStringTag,{value:"Module"})),yK=Object.freeze(Object.defineProperty({__proto__:null,PacketModule:QP,createPacketServices:ek},Symbol.toStringTag,{value:"Module"})),gK=Object.freeze(Object.defineProperty({__proto__:null,PieModule:tk,createPieServices:rk},Symbol.toStringTag,{value:"Module"})),vK=Object.freeze(Object.defineProperty({__proto__:null,TreeViewModule:nk,createTreeViewServices:ak},Symbol.toStringTag,{value:"Module"})),TK=Object.freeze(Object.defineProperty({__proto__:null,ArchitectureModule:ik,createArchitectureServices:sk},Symbol.toStringTag,{value:"Module"})),$K=Object.freeze(Object.defineProperty({__proto__:null,GitGraphModule:YP,createGitGraphServices:XP},Symbol.toStringTag,{value:"Module"})),RK=Object.freeze(Object.defineProperty({__proto__:null,EventModelingModule:lk,createEventModelingServices:uk},Symbol.toStringTag,{value:"Module"})),AK=Object.freeze(Object.defineProperty({__proto__:null,RadarModule:kP,createRadarServices:OP},Symbol.toStringTag,{value:"Module"})),EK=Object.freeze(Object.defineProperty({__proto__:null,RailroadModule:LP,createRailroadServices:DP},Symbol.toStringTag,{value:"Module"})),CK=Object.freeze(Object.defineProperty({__proto__:null,RailroadEbnfModule:xP,createRailroadEbnfServices:MP},Symbol.toStringTag,{value:"Module"})),bK=Object.freeze(Object.defineProperty({__proto__:null,RailroadAbnfModule:GP,createRailroadAbnfServices:FP},Symbol.toStringTag,{value:"Module"})),_K=Object.freeze(Object.defineProperty({__proto__:null,RailroadPegModule:zP,createRailroadPegServices:jP},Symbol.toStringTag,{value:"Module"})),SK=Object.freeze(Object.defineProperty({__proto__:null,TreemapModule:UP,createTreemapServices:KP},Symbol.toStringTag,{value:"Module"})),wK=Object.freeze(Object.defineProperty({__proto__:null,WardleyModule:WP,createWardleyServices:VP},Symbol.toStringTag,{value:"Module"})),IK=Object.freeze(Object.defineProperty({__proto__:null,CynefinModule:qP,createCynefinServices:HP},Symbol.toStringTag,{value:"Module"}));export{mK as M,MP as a,FP as b,DP as c,jP as d,NB as i,pK as p};
@@ -0,0 +1,62 @@
import{p as xt}from"./chunk-JWPE2WC7.js";import{s as gt,g as $t,o as bt,n as wt,a as Ct,b as vt,_ as s,l as O,D as Dt,d as kt,p as At,A as U,y as Q,B as Tt,E as ot}from"./mermaid.core.js";import{p as Bt}from"./cynefin-OW5HDTMX.js";import"./index.js";var rt=s(()=>({domains:new Map,transitions:[]}),"createDefaultData"),H=rt(),St=s(()=>H.domains,"getDomains"),Mt=s(()=>H.transitions,"getTransitions"),zt=s(t=>{if(t)for(const e of t){const n=e.domain,a=(e.items??[]).map(c=>({label:c.label}));H.domains.set(n,{name:n,items:a})}},"setDomains"),Lt=s(t=>{t&&(H.transitions=t.filter(e=>e.from===e.to?(O.warn(`Cynefin: self-loop transition on domain "${e.from}" is not meaningful and will be skipped.`),!1):!0).map(e=>({from:e.from,to:e.to,label:e.label||void 0})))},"setTransitions"),Nt=s(()=>U({...Tt.cynefin,...Q().cynefin}),"getConfig"),Pt=s(()=>{At(),H=rt()},"clear"),Y={getDomains:St,getTransitions:Mt,setDomains:zt,setTransitions:Lt,getConfig:Nt,clear:Pt,setAccTitle:vt,getAccTitle:Ct,setDiagramTitle:wt,getDiagramTitle:bt,getAccDescription:$t,setAccDescription:gt},It=s(t=>{xt(t,Y),Y.setDomains(t.domains),Y.setTransitions(t.transitions)},"populate"),Wt={parse:s(async t=>{const e=await Bt("cynefin",t);O.debug(e),It(e)},"parse")};function V(t){let e=t+1831565813|0;return e=Math.imul(e^e>>>15,e|1),e^=e+Math.imul(e^e>>>7,e|61),((e^e>>>14)>>>0)/4294967296}s(V,"seededRandom");function st(t){let e=0;for(let n=0;n<t.length;n++){const a=t.charCodeAt(n);e=(e<<5)-e+a,e|=0}return e}s(st,"hashString");function it(t,e){return typeof t=="number"&&Number.isFinite(t)&&t!==0?t:st(e)}s(it,"resolveSeed");function ct(t,e,n,a){const c=t/2,m=a??t*.015,v=7,W=e/v,d=[];for(let o=0;o<=v;o++){const p=V(n+o*17)*m*2-m;d.push({x:c+p,y:o*W})}let D=`M${d[0].x},${d[0].y}`;for(let o=0;o<d.length-1;o++){const p=d[o],i=d[o+1],f=(p.y+i.y)/2,b=o%2===0?1:-1,h=m*1.5*b*V(n+o*31+7),R=p.x+h,_=f,E=i.x-h;D+=` C${R},${_} ${E},${f} ${i.x},${i.y}`}return D}s(ct,"generateFoldPath");function lt(t,e,n,a){const c=e/2,m=a??e*.015,v=7,W=t/v,d=[];for(let o=0;o<=v;o++){const p=V(n+o*23)*m*2-m;d.push({x:o*W,y:c+p})}let D=`M${d[0].x},${d[0].y}`;for(let o=0;o<d.length-1;o++){const p=d[o],i=d[o+1],f=(p.x+i.x)/2,b=o%2===0?1:-1,h=m*1.5*b*V(n+o*37+11),R=f,_=p.y+h,E=f,z=i.y-h;D+=` C${R},${_} ${E},${z} ${i.x},${i.y}`}return D}s(lt,"generateHorizontalBoundary");function dt(t,e){const n=t/2,a=e*.5,c=e,m=t*.03;return[`M${n},${a}`,`C${n+m},${a+(c-a)*.2}`,`${n-m*1.5},${a+(c-a)*.55}`,`${n+m*.5},${a+(c-a)*.75}`,`C${n-m},${a+(c-a)*.85}`,`${n+m*.3},${a+(c-a)*.95}`,`${n},${c}`].join(" ")}s(dt,"generateCliffPath");function ft(t,e,n,a){return[`M${t-n},${e}`,`A${n},${a} 0 1,1 ${t+n},${e}`,`A${n},${a} 0 1,1 ${t-n},${e}`,"Z"].join(" ")}s(ft,"generateConfusionPath");var at={complex:{model:"Probe → Sense → Respond",practice:"Emergent Practices"},complicated:{model:"Sense → Analyse → Respond",practice:"Good Practices"},clear:{model:"Sense → Categorise → Respond",practice:"Best Practices"},chaotic:{model:"Act → Sense → Respond",practice:"Novel Practices"},confusion:{model:"",practice:"Disorder"}},Rt=s((t,e)=>{const n=t/2,a=e/2;return{complex:{cx:n/2,cy:a/2,x:0,y:0,w:n,h:a},complicated:{cx:n+n/2,cy:a/2,x:n,y:0,w:n,h:a},chaotic:{cx:n/2,cy:a+a/2,x:0,y:a,w:n,h:a},clear:{cx:n+n/2,cy:a+a/2,x:n,y:a,w:n,h:a},confusion:{cx:n,cy:a,x:n*.7,y:a*.7,w:n*.6,h:a*.6}}},"getDomainLayouts"),_t=s(()=>{const t=ot(),e=Q();return U(t,e.themeVariables).cynefin},"getCynefinDomainColors"),q=3,Et=s((t,e,n,a)=>{const c=a.db,m=c.getDomains(),v=c.getTransitions(),W=c.getDiagramTitle(),d=c.getAccTitle(),D=c.getAccDescription(),o=c.getConfig(),p=_t();O.debug("Rendering Cynefin diagram");const i=o.width,f=o.height,b=o.padding,h=o.showDomainDescriptions,R=o.boundaryAmplitude,_=i+b*2,E=f+b*2,z={complex:p.complexBg,complicated:p.complicatedBg,clear:p.clearBg,chaotic:p.chaoticBg,confusion:p.confusionBg},k=Dt(e);kt(k,E,_,o.useMaxWidth??!0),k.attr("viewBox",`0 0 ${_} ${E}`),d&&k.append("title").text(d),D&&k.append("desc").text(D);const A=k.append("g").attr("transform",`translate(${b}, ${b})`),F=Rt(i,f),Z=it(o.seed,e),mt=A.append("g").attr("class","cynefin-backgrounds"),X=["complex","complicated","chaotic","clear"];for(const l of X){const r=F[l];mt.append("rect").attr("class","cynefinDomain").attr("x",r.x).attr("y",r.y).attr("width",r.w).attr("height",r.h).attr("fill",z[l]).attr("fill-opacity",.4).attr("stroke","none")}const j=A.append("g").attr("class","cynefin-boundaries");j.append("path").attr("class","cynefinBoundary").attr("d",ct(i,f,Z,R)).attr("fill","none"),j.append("path").attr("class","cynefinBoundary").attr("d",lt(i,f,Z+100,R)).attr("fill","none"),j.append("path").attr("class","cynefinCliff").attr("d",dt(i,f)).attr("fill","none");const pt=i*.15,yt=f*.15;A.append("path").attr("class","cynefinConfusion").attr("d",ft(i/2,f/2,pt,yt)).attr("fill",z.confusion).attr("fill-opacity",.5);const J=A.append("g").attr("class","cynefin-labels");for(const l of X){const r=F[l];J.append("text").attr("class","cynefinDomainLabel").attr("x"Line truncated
.cynefinDomain {
stroke: none;
}
.cynefinDomainLabel {
font-size: ${t.domainFontSize}px;
font-weight: bold;
fill: ${t.labelColor};
}
.cynefinSubtitle {
font-size: ${t.itemFontSize-1}px;
fill: ${t.textColor};
font-style: italic;
}
.cynefinItem {
fill-opacity: 0.95;
stroke: ${t.boundaryColor};
stroke-width: 1;
}
.cynefinItemText {
font-size: ${t.itemFontSize}px;
fill: ${t.textColor};
}
.cynefinItemOverflow {
fill-opacity: 0.6;
stroke: ${t.boundaryColor};
stroke-width: 1;
stroke-dasharray: 3 2;
}
.cynefinBoundary {
stroke: ${t.boundaryColor};
stroke-width: ${t.boundaryWidth};
stroke-dasharray: 6 3;
}
.cynefinCliff {
stroke: ${t.cliffColor};
stroke-width: ${t.cliffWidth};
}
.cynefinConfusion {
stroke: ${t.boundaryColor};
stroke-width: 1.5;
stroke-dasharray: 4 2;
}
.cynefinArrowLine {
stroke: ${t.arrowColor};
stroke-width: ${t.arrowWidth};
fill: none;
}
.cynefinArrowHead {
fill: ${t.arrowColor};
stroke: none;
}
.cynefinArrowLabel {
font-size: ${t.itemFontSize-1}px;
fill: ${t.textColor};
}
.cynefinTitle {
font-size: ${t.domainFontSize+2}px;
font-weight: bold;
fill: ${t.labelColor};
}
`},"styles"),Gt=Ht,qt={parser:Wt,db:Y,renderer:Ft,styles:Gt};export{qt as diagram};
Loaded 100 of 181 files, more files were not shown because too many files have changed in this diff. Show more