mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-09 22:08:38 +03:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
867bbdeb66 | ||
|
|
5f5521d2e6 | ||
|
|
3ecd042ef0 | ||
|
|
f57a670ef8 | ||
|
|
b5db9fcd52 | ||
|
|
ca17292768 |
@@ -577,11 +577,6 @@ Give nanobot its own email account. It polls **IMAP** for incoming mail and repl
|
|||||||
> - `allowFrom`: Add your email address. Use `["*"]` to accept emails from anyone.
|
> - `allowFrom`: Add your email address. Use `["*"]` to accept emails from anyone.
|
||||||
> - `smtpUseTls` and `smtpUseSsl` default to `true` / `false` respectively, which is correct for Gmail (port 587 + STARTTLS). No need to set them explicitly.
|
> - `smtpUseTls` and `smtpUseSsl` default to `true` / `false` respectively, which is correct for Gmail (port 587 + STARTTLS). No need to set them explicitly.
|
||||||
> - Set `"autoReplyEnabled": false` if you only want to read/analyze emails without sending automatic replies.
|
> - Set `"autoReplyEnabled": false` if you only want to read/analyze emails without sending automatic replies.
|
||||||
> - `postAction`: Optional post-processing for processed emails: `"delete"` or `"move"` (default `null`).
|
|
||||||
> This runs only after an accepted email is successfully delivered to the AI pipeline.
|
|
||||||
> - `postActionMoveMailbox`: Destination mailbox used when `postAction` is `"move"` (for example `"Processed"` or `"[Gmail]/Trash"`).
|
|
||||||
> - `postActionIgnoreSkipped`: If `true` (default), skipped emails are ignored for post-action and not moved/deleted.
|
|
||||||
> - `postActionExpunge`: When `true`, the channel performs a full mailbox cleanup after processing emails (default `false`). Enable only on very old IMAP servers that lack modern UIDPLUS support. Note that this will expunge **all** messages marked as deleted in the mailbox, including ones not handled by the agent. Leaving this off is safe for all modern IMAP servers.
|
|
||||||
> - `allowedAttachmentTypes`: Save inbound attachments matching these MIME types — `["*"]` for all, e.g. `["application/pdf", "image/*"]` (default `[]` = disabled).
|
> - `allowedAttachmentTypes`: Save inbound attachments matching these MIME types — `["*"]` for all, e.g. `["application/pdf", "image/*"]` (default `[]` = disabled).
|
||||||
> - `maxAttachmentSize`: Max size per attachment in bytes (default `2000000` / 2MB).
|
> - `maxAttachmentSize`: Max size per attachment in bytes (default `2000000` / 2MB).
|
||||||
> - `maxAttachmentsPerEmail`: Max attachments to save per email (default `5`).
|
> - `maxAttachmentsPerEmail`: Max attachments to save per email (default `5`).
|
||||||
@@ -602,10 +597,6 @@ Give nanobot its own email account. It polls **IMAP** for incoming mail and repl
|
|||||||
"smtpPassword": "your-app-password",
|
"smtpPassword": "your-app-password",
|
||||||
"fromAddress": "my-nanobot@gmail.com",
|
"fromAddress": "my-nanobot@gmail.com",
|
||||||
"allowFrom": ["your-real-email@gmail.com"],
|
"allowFrom": ["your-real-email@gmail.com"],
|
||||||
"postAction": "move",
|
|
||||||
"postActionMoveMailbox": "[Gmail]/Trash",
|
|
||||||
"postActionIgnoreSkipped": true,
|
|
||||||
"postActionExpunge": false,
|
|
||||||
"allowedAttachmentTypes": ["application/pdf", "image/*"]
|
"allowedAttachmentTypes": ["application/pdf", "image/*"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-206
@@ -8,7 +8,6 @@ import re
|
|||||||
import smtplib
|
import smtplib
|
||||||
import ssl
|
import ssl
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from dataclasses import dataclass
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from email import policy
|
from email import policy
|
||||||
from email.header import decode_header, make_header
|
from email.header import decode_header, make_header
|
||||||
@@ -17,7 +16,7 @@ from email.parser import BytesParser
|
|||||||
from email.utils import parseaddr
|
from email.utils import parseaddr
|
||||||
from fnmatch import fnmatch
|
from fnmatch import fnmatch
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Literal
|
from typing import Any
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
@@ -54,10 +53,6 @@ class EmailConfig(Base):
|
|||||||
auto_reply_enabled: bool = True
|
auto_reply_enabled: bool = True
|
||||||
poll_interval_seconds: int = 30
|
poll_interval_seconds: int = 30
|
||||||
mark_seen: bool = True
|
mark_seen: bool = True
|
||||||
post_action: Literal["delete", "move"] | None = None
|
|
||||||
post_action_move_mailbox: str | None = None
|
|
||||||
post_action_expunge: bool = False
|
|
||||||
post_action_ignore_skipped: bool = True
|
|
||||||
max_body_chars: int = 12000
|
max_body_chars: int = 12000
|
||||||
subject_prefix: str = "Re: "
|
subject_prefix: str = "Re: "
|
||||||
allow_from: list[str] = Field(default_factory=list)
|
allow_from: list[str] = Field(default_factory=list)
|
||||||
@@ -72,13 +67,6 @@ class EmailConfig(Base):
|
|||||||
max_attachments_per_email: int = 5
|
max_attachments_per_email: int = 5
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class _ServerFeatures:
|
|
||||||
move: bool
|
|
||||||
uidplus: bool
|
|
||||||
uid_store: bool | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class EmailChannel(BaseChannel):
|
class EmailChannel(BaseChannel):
|
||||||
"""
|
"""
|
||||||
Email channel.
|
Email channel.
|
||||||
@@ -162,9 +150,7 @@ class EmailChannel(BaseChannel):
|
|||||||
poll_seconds = max(5, int(self.config.poll_interval_seconds))
|
poll_seconds = max(5, int(self.config.poll_interval_seconds))
|
||||||
while self._running:
|
while self._running:
|
||||||
try:
|
try:
|
||||||
inbound_items, skipped_uids = await asyncio.to_thread(self._fetch_new_messages)
|
inbound_items = await asyncio.to_thread(self._fetch_new_messages)
|
||||||
should_apply_post_action = self._should_apply_post_action()
|
|
||||||
post_actions_uids: set[str] = set()
|
|
||||||
for item in inbound_items:
|
for item in inbound_items:
|
||||||
sender = item["sender"]
|
sender = item["sender"]
|
||||||
subject = item.get("subject", "")
|
subject = item.get("subject", "")
|
||||||
@@ -175,27 +161,13 @@ class EmailChannel(BaseChannel):
|
|||||||
if message_id:
|
if message_id:
|
||||||
self._last_message_id_by_chat[sender] = message_id
|
self._last_message_id_by_chat[sender] = message_id
|
||||||
|
|
||||||
try:
|
await self._handle_message(
|
||||||
await self._handle_message(
|
sender_id=sender,
|
||||||
sender_id=sender,
|
chat_id=sender,
|
||||||
chat_id=sender,
|
content=item["content"],
|
||||||
content=item["content"],
|
media=item.get("media") or None,
|
||||||
media=item.get("media") or None,
|
metadata=item.get("metadata", {}),
|
||||||
metadata=item.get("metadata", {}),
|
)
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
self.logger.exception("Error delivering email from {}", sender)
|
|
||||||
continue
|
|
||||||
|
|
||||||
uid = str((item.get("metadata") or {}).get("uid") or "")
|
|
||||||
if uid and should_apply_post_action:
|
|
||||||
post_actions_uids.add(uid)
|
|
||||||
|
|
||||||
if should_apply_post_action and not self.config.post_action_ignore_skipped:
|
|
||||||
post_actions_uids.update(skipped_uids)
|
|
||||||
|
|
||||||
if post_actions_uids:
|
|
||||||
await asyncio.to_thread(self._apply_post_actions_batch, sorted(post_actions_uids))
|
|
||||||
except Exception:
|
except Exception:
|
||||||
self.logger.exception("Polling error")
|
self.logger.exception("Polling error")
|
||||||
|
|
||||||
@@ -323,9 +295,6 @@ class EmailChannel(BaseChannel):
|
|||||||
if not self.config.smtp_password:
|
if not self.config.smtp_password:
|
||||||
missing.append("smtp_password")
|
missing.append("smtp_password")
|
||||||
|
|
||||||
if self.config.post_action == "move" and not (self.config.post_action_move_mailbox or "").strip():
|
|
||||||
missing.append("post_action_move_mailbox")
|
|
||||||
|
|
||||||
if missing:
|
if missing:
|
||||||
self.logger.error("Channel not configured, missing: {}", ', '.join(missing))
|
self.logger.error("Channel not configured, missing: {}", ', '.join(missing))
|
||||||
return False
|
return False
|
||||||
@@ -349,8 +318,8 @@ class EmailChannel(BaseChannel):
|
|||||||
smtp.login(self.config.smtp_username, self.config.smtp_password)
|
smtp.login(self.config.smtp_username, self.config.smtp_password)
|
||||||
smtp.send_message(msg)
|
smtp.send_message(msg)
|
||||||
|
|
||||||
def _fetch_new_messages(self) -> tuple[list[dict[str, Any]], set[str]]:
|
def _fetch_new_messages(self) -> list[dict[str, Any]]:
|
||||||
"""Poll IMAP and return parsed unread messages plus skipped message UIDs."""
|
"""Poll IMAP and return parsed unread messages."""
|
||||||
return self._fetch_messages(
|
return self._fetch_messages(
|
||||||
search_criteria=("UNSEEN",),
|
search_criteria=("UNSEEN",),
|
||||||
mark_seen=self.config.mark_seen,
|
mark_seen=self.config.mark_seen,
|
||||||
@@ -372,7 +341,7 @@ class EmailChannel(BaseChannel):
|
|||||||
if end_date <= start_date:
|
if end_date <= start_date:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
messages, _ = self._fetch_messages(
|
return self._fetch_messages(
|
||||||
search_criteria=(
|
search_criteria=(
|
||||||
"SINCE",
|
"SINCE",
|
||||||
self._format_imap_date(start_date),
|
self._format_imap_date(start_date),
|
||||||
@@ -383,7 +352,6 @@ class EmailChannel(BaseChannel):
|
|||||||
dedupe=False,
|
dedupe=False,
|
||||||
limit=max(1, int(limit)),
|
limit=max(1, int(limit)),
|
||||||
)
|
)
|
||||||
return messages
|
|
||||||
|
|
||||||
def _fetch_messages(
|
def _fetch_messages(
|
||||||
self,
|
self,
|
||||||
@@ -391,9 +359,8 @@ class EmailChannel(BaseChannel):
|
|||||||
mark_seen: bool,
|
mark_seen: bool,
|
||||||
dedupe: bool,
|
dedupe: bool,
|
||||||
limit: int,
|
limit: int,
|
||||||
) -> tuple[list[dict[str, Any]], set[str]]:
|
) -> list[dict[str, Any]]:
|
||||||
messages: list[dict[str, Any]] = []
|
messages: list[dict[str, Any]] = []
|
||||||
skipped_uids: set[str] = set()
|
|
||||||
cycle_uids: set[str] = set()
|
cycle_uids: set[str] = set()
|
||||||
|
|
||||||
for attempt in range(2):
|
for attempt in range(2):
|
||||||
@@ -404,16 +371,15 @@ class EmailChannel(BaseChannel):
|
|||||||
dedupe,
|
dedupe,
|
||||||
limit,
|
limit,
|
||||||
messages,
|
messages,
|
||||||
skipped_uids,
|
|
||||||
cycle_uids,
|
cycle_uids,
|
||||||
)
|
)
|
||||||
return messages, skipped_uids
|
return messages
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
if attempt == 1 or not self._is_stale_imap_error(exc):
|
if attempt == 1 or not self._is_stale_imap_error(exc):
|
||||||
raise
|
raise
|
||||||
self.logger.warning("IMAP connection went stale, retrying once: {}", exc)
|
self.logger.warning("IMAP connection went stale, retrying once: {}", exc)
|
||||||
|
|
||||||
return messages, skipped_uids
|
return messages
|
||||||
|
|
||||||
def _fetch_messages_once(
|
def _fetch_messages_once(
|
||||||
self,
|
self,
|
||||||
@@ -422,17 +388,29 @@ class EmailChannel(BaseChannel):
|
|||||||
dedupe: bool,
|
dedupe: bool,
|
||||||
limit: int,
|
limit: int,
|
||||||
messages: list[dict[str, Any]],
|
messages: list[dict[str, Any]],
|
||||||
skipped_uids: set[str],
|
|
||||||
cycle_uids: set[str],
|
cycle_uids: set[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Fetch messages by arbitrary IMAP search criteria."""
|
"""Fetch messages by arbitrary IMAP search criteria."""
|
||||||
mailbox = self.config.imap_mailbox or "INBOX"
|
mailbox = self.config.imap_mailbox or "INBOX"
|
||||||
|
|
||||||
client = self._open_imap_client(mailbox=mailbox, missing_mailbox_ok=True)
|
if self.config.imap_use_ssl:
|
||||||
if client is None:
|
client = imaplib.IMAP4_SSL(self.config.imap_host, self.config.imap_port)
|
||||||
return messages
|
else:
|
||||||
|
client = imaplib.IMAP4(self.config.imap_host, self.config.imap_port)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
client.login(self.config.imap_username, self.config.imap_password)
|
||||||
|
try:
|
||||||
|
status, _ = client.select(mailbox)
|
||||||
|
except Exception as exc:
|
||||||
|
if self._is_missing_mailbox_error(exc):
|
||||||
|
self.logger.warning("Mailbox unavailable, skipping poll for {}: {}", mailbox, exc)
|
||||||
|
return messages
|
||||||
|
raise
|
||||||
|
if status != "OK":
|
||||||
|
self.logger.warning("Mailbox select returned {}, skipping poll for {}", status, mailbox)
|
||||||
|
return messages
|
||||||
|
|
||||||
status, data = client.search(None, *search_criteria)
|
status, data = client.search(None, *search_criteria)
|
||||||
if status != "OK" or not data:
|
if status != "OK" or not data:
|
||||||
return messages
|
return messages
|
||||||
@@ -464,8 +442,6 @@ class EmailChannel(BaseChannel):
|
|||||||
self._remember_processed_uid(uid, dedupe, cycle_uids)
|
self._remember_processed_uid(uid, dedupe, cycle_uids)
|
||||||
if mark_seen:
|
if mark_seen:
|
||||||
client.store(imap_id, "+FLAGS", "\\Seen")
|
client.store(imap_id, "+FLAGS", "\\Seen")
|
||||||
if uid:
|
|
||||||
skipped_uids.add(uid)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# --- Anti-spoofing: verify Authentication-Results ---
|
# --- Anti-spoofing: verify Authentication-Results ---
|
||||||
@@ -477,8 +453,6 @@ class EmailChannel(BaseChannel):
|
|||||||
sender,
|
sender,
|
||||||
)
|
)
|
||||||
self._remember_processed_uid(uid, dedupe, cycle_uids)
|
self._remember_processed_uid(uid, dedupe, cycle_uids)
|
||||||
if uid:
|
|
||||||
skipped_uids.add(uid)
|
|
||||||
continue
|
continue
|
||||||
if self.config.verify_dkim and not dkim_pass:
|
if self.config.verify_dkim and not dkim_pass:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
@@ -487,16 +461,12 @@ class EmailChannel(BaseChannel):
|
|||||||
sender,
|
sender,
|
||||||
)
|
)
|
||||||
self._remember_processed_uid(uid, dedupe, cycle_uids)
|
self._remember_processed_uid(uid, dedupe, cycle_uids)
|
||||||
if uid:
|
|
||||||
skipped_uids.add(uid)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not self.is_allowed(sender):
|
if not self.is_allowed(sender):
|
||||||
self._remember_processed_uid(uid, dedupe, cycle_uids)
|
self._remember_processed_uid(uid, dedupe, cycle_uids)
|
||||||
if mark_seen:
|
if mark_seen:
|
||||||
client.store(imap_id, "+FLAGS", "\\Seen")
|
client.store(imap_id, "+FLAGS", "\\Seen")
|
||||||
if uid:
|
|
||||||
skipped_uids.add(uid)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
subject = self._decode_header_value(parsed.get("Subject", ""))
|
subject = self._decode_header_value(parsed.get("Subject", ""))
|
||||||
@@ -553,39 +523,8 @@ class EmailChannel(BaseChannel):
|
|||||||
if mark_seen:
|
if mark_seen:
|
||||||
client.store(imap_id, "+FLAGS", "\\Seen")
|
client.store(imap_id, "+FLAGS", "\\Seen")
|
||||||
finally:
|
finally:
|
||||||
self._close_imap_client(client)
|
with suppress(Exception):
|
||||||
|
client.logout()
|
||||||
def _open_imap_client(self, mailbox: str, *, missing_mailbox_ok: bool = False) -> Any | None:
|
|
||||||
if self.config.imap_use_ssl:
|
|
||||||
client: Any = imaplib.IMAP4_SSL(self.config.imap_host, self.config.imap_port)
|
|
||||||
else:
|
|
||||||
client = imaplib.IMAP4(self.config.imap_host, self.config.imap_port)
|
|
||||||
|
|
||||||
try:
|
|
||||||
client.login(self.config.imap_username, self.config.imap_password)
|
|
||||||
try:
|
|
||||||
status, _ = client.select(mailbox)
|
|
||||||
except Exception as exc:
|
|
||||||
if missing_mailbox_ok and self._is_missing_mailbox_error(exc):
|
|
||||||
self.logger.warning("Mailbox unavailable, skipping poll for {}: {}", mailbox, exc)
|
|
||||||
self._close_imap_client(client)
|
|
||||||
return None
|
|
||||||
raise
|
|
||||||
|
|
||||||
if status != "OK":
|
|
||||||
self.logger.warning("Mailbox select returned {}, skipping poll for {}", status, mailbox)
|
|
||||||
self._close_imap_client(client)
|
|
||||||
return None
|
|
||||||
except Exception:
|
|
||||||
self._close_imap_client(client)
|
|
||||||
raise
|
|
||||||
|
|
||||||
return client
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _close_imap_client(client: Any) -> None:
|
|
||||||
with suppress(Exception):
|
|
||||||
client.logout()
|
|
||||||
|
|
||||||
def _collect_self_addresses(self) -> set[str]:
|
def _collect_self_addresses(self) -> set[str]:
|
||||||
"""Return normalized email addresses owned by this channel instance."""
|
"""Return normalized email addresses owned by this channel instance."""
|
||||||
@@ -631,118 +570,6 @@ class EmailChannel(BaseChannel):
|
|||||||
# Evict a random half to cap memory; mark_seen is the primary dedup
|
# Evict a random half to cap memory; mark_seen is the primary dedup
|
||||||
self._processed_uids = set(list(self._processed_uids)[len(self._processed_uids) // 2:])
|
self._processed_uids = set(list(self._processed_uids)[len(self._processed_uids) // 2:])
|
||||||
|
|
||||||
def _should_apply_post_action(self) -> bool:
|
|
||||||
return self.config.post_action in {"delete", "move"}
|
|
||||||
|
|
||||||
def _apply_post_actions_batch(self, post_actions_uids: list[str]) -> None:
|
|
||||||
if not self._should_apply_post_action() or not post_actions_uids:
|
|
||||||
return
|
|
||||||
|
|
||||||
mailbox = self.config.imap_mailbox or "INBOX"
|
|
||||||
client = self._open_imap_client(mailbox=mailbox)
|
|
||||||
if client is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
features = self._server_features(client)
|
|
||||||
# Apply all post-actions in one IMAP session. `features` also carries
|
|
||||||
# session-learned behavior (e.g. UID STORE support) so later UIDs can
|
|
||||||
# skip known-broken paths.
|
|
||||||
for uid in post_actions_uids:
|
|
||||||
if uid:
|
|
||||||
self._apply_post_action(client, uid, features)
|
|
||||||
finally:
|
|
||||||
self._close_imap_client(client)
|
|
||||||
|
|
||||||
def _apply_post_action(
|
|
||||||
self,
|
|
||||||
client: Any,
|
|
||||||
uid: str,
|
|
||||||
features: _ServerFeatures,
|
|
||||||
) -> None:
|
|
||||||
action = self.config.post_action
|
|
||||||
|
|
||||||
if action == "delete":
|
|
||||||
if not self._uid_store_deleted(client, uid, features):
|
|
||||||
return
|
|
||||||
self._uid_expunge_or_fallback(client, uid, features)
|
|
||||||
return
|
|
||||||
|
|
||||||
if action == "move":
|
|
||||||
target = (self.config.post_action_move_mailbox or "").strip()
|
|
||||||
if features.move:
|
|
||||||
status, _ = client.uid("MOVE", uid, target)
|
|
||||||
if status != "OK":
|
|
||||||
self.logger.warning("Post-action move failed (UID MOVE) for UID {} to mailbox {}", uid, target)
|
|
||||||
return
|
|
||||||
|
|
||||||
status, _ = client.uid("COPY", uid, target)
|
|
||||||
if status != "OK":
|
|
||||||
self.logger.warning("Post-action move failed (UID COPY) for UID {} to mailbox {}", uid, target)
|
|
||||||
return
|
|
||||||
if not self._uid_store_deleted(client, uid, features):
|
|
||||||
return
|
|
||||||
self._uid_expunge_or_fallback(client, uid, features)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _server_features(client: Any) -> _ServerFeatures:
|
|
||||||
caps: set[str] = set()
|
|
||||||
with suppress(Exception):
|
|
||||||
status, data = client.capability()
|
|
||||||
if status == "OK" and data:
|
|
||||||
for raw in data:
|
|
||||||
if isinstance(raw, (bytes, bytearray)):
|
|
||||||
caps.update(token.upper() for token in raw.decode("utf-8", errors="ignore").split())
|
|
||||||
elif isinstance(raw, str):
|
|
||||||
caps.update(token.upper() for token in raw.split())
|
|
||||||
return _ServerFeatures(move="MOVE" in caps, uidplus="UIDPLUS" in caps)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _lookup_imap_id_by_uid(client: Any, uid: str) -> bytes | None:
|
|
||||||
# IMAP exposes two message identifiers: UID (stable) and sequence number
|
|
||||||
# (session-local). We target by UID first, but some servers may reject
|
|
||||||
# UID STORE. In that case we resolve the current sequence number for the
|
|
||||||
# UID and retry with STORE using that sequence id.
|
|
||||||
status, data = client.search(None, "UID", uid)
|
|
||||||
if status != "OK" or not data or not data[0]:
|
|
||||||
return None
|
|
||||||
return data[0].split()[0]
|
|
||||||
|
|
||||||
def _uid_store_deleted(self, client: Any, uid: str, features: _ServerFeatures) -> bool:
|
|
||||||
# Optimistic path: try UID STORE first because UID is stable and avoids
|
|
||||||
# sequence-number lookup. If this fails once for the session, remember it
|
|
||||||
# and use the sequence STORE fallback directly for remaining UIDs.
|
|
||||||
if features.uid_store is not False:
|
|
||||||
status, _ = client.uid("STORE", uid, "+FLAGS", "(\\Deleted)")
|
|
||||||
if status == "OK":
|
|
||||||
features.uid_store = True
|
|
||||||
return True
|
|
||||||
features.uid_store = False
|
|
||||||
|
|
||||||
# Compatibility fallback for servers where UID STORE is unavailable or
|
|
||||||
# unreliable: resolve the current sequence number from UID and use STORE.
|
|
||||||
imap_id = self._lookup_imap_id_by_uid(client, uid)
|
|
||||||
if not imap_id:
|
|
||||||
self.logger.warning("Post-action skipped: UID {} not found", uid)
|
|
||||||
return False
|
|
||||||
|
|
||||||
status, _ = client.store(imap_id, "+FLAGS", "\\Deleted")
|
|
||||||
if status != "OK":
|
|
||||||
self.logger.warning("Post-action failed: could not mark UID {} as deleted", uid)
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _uid_expunge_or_fallback(self, client: Any, uid: str, features: _ServerFeatures) -> None:
|
|
||||||
# Prefer UID-scoped expunge when supported to avoid expunging unrelated
|
|
||||||
# messages already marked \Deleted in the selected mailbox.
|
|
||||||
if features.uidplus:
|
|
||||||
status, _ = client.uid("EXPUNGE", uid)
|
|
||||||
if status == "OK":
|
|
||||||
return
|
|
||||||
self.logger.warning("UID EXPUNGE failed for UID {}, falling back to EXPUNGE", uid)
|
|
||||||
if self.config.post_action_expunge:
|
|
||||||
client.expunge()
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _is_stale_imap_error(cls, exc: Exception) -> bool:
|
def _is_stale_imap_error(cls, exc: Exception) -> bool:
|
||||||
message = str(exc).lower()
|
message = str(exc).lower()
|
||||||
|
|||||||
+34
-180
@@ -878,13 +878,12 @@ def _run_gateway(
|
|||||||
health_server_enabled: bool = True,
|
health_server_enabled: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Shared gateway runtime; ``open_browser_url`` opens a tab once channels are up."""
|
"""Shared gateway runtime; ``open_browser_url`` opens a tab once channels are up."""
|
||||||
from nanobot.agent.tools.cron import CronTool
|
|
||||||
from nanobot.agent.tools.message import MessageTool
|
from nanobot.agent.tools.message import MessageTool
|
||||||
from nanobot.bus.queue import MessageBus
|
from nanobot.bus.queue import MessageBus
|
||||||
from nanobot.bus.runtime_events import RuntimeEventBus
|
from nanobot.bus.runtime_events import RuntimeEventBus
|
||||||
from nanobot.channels.manager import ChannelManager
|
from nanobot.channels.manager import ChannelManager
|
||||||
|
from nanobot.cron.executor import CronJobExecutor
|
||||||
from nanobot.cron.service import CronService
|
from nanobot.cron.service import CronService
|
||||||
from nanobot.cron.types import CronJob
|
|
||||||
from nanobot.providers.factory import build_provider_snapshot, load_provider_snapshot
|
from nanobot.providers.factory import build_provider_snapshot, load_provider_snapshot
|
||||||
from nanobot.providers.image_generation import image_gen_provider_configs
|
from nanobot.providers.image_generation import image_gen_provider_configs
|
||||||
from nanobot.session.manager import SessionManager
|
from nanobot.session.manager import SessionManager
|
||||||
@@ -976,174 +975,44 @@ def _run_gateway(
|
|||||||
if isinstance(message_tool, MessageTool):
|
if isinstance(message_tool, MessageTool):
|
||||||
message_tool.set_send_callback(_deliver_to_channel)
|
message_tool.set_send_callback(_deliver_to_channel)
|
||||||
|
|
||||||
# Set cron callback (needs agent)
|
hb_cfg = config.gateway.heartbeat
|
||||||
async def on_cron_job(job: CronJob) -> str | None:
|
|
||||||
"""Execute a cron job through the agent."""
|
|
||||||
async def _silent(*_args, **_kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Dream is an internal job — run directly, not through the agent loop.
|
def _get_channel(channel_name: str) -> Any | None:
|
||||||
if job.name == "dream":
|
try:
|
||||||
from nanobot.agent.memory import MemoryStore
|
return channels.channels.get(channel_name)
|
||||||
|
except NameError:
|
||||||
dream_session_key = MemoryStore.dream_session_key
|
|
||||||
build_dream_commit_message = MemoryStore.build_dream_commit_message
|
|
||||||
prune_dream_sessions = MemoryStore.prune_dream_sessions
|
|
||||||
|
|
||||||
store = agent.context.memory
|
|
||||||
resp = None
|
|
||||||
try:
|
|
||||||
result = store.build_dream_prompt()
|
|
||||||
if result is None:
|
|
||||||
logger.info("Dream: nothing to process")
|
|
||||||
return None
|
|
||||||
prompt, last_cursor = result
|
|
||||||
key = dream_session_key()
|
|
||||||
resp = await agent.process_direct(
|
|
||||||
prompt,
|
|
||||||
session_key=key,
|
|
||||||
ephemeral=True,
|
|
||||||
tools=store.build_dream_tools(),
|
|
||||||
on_progress=_silent,
|
|
||||||
)
|
|
||||||
if MemoryStore.dream_run_completed(resp):
|
|
||||||
store.set_last_dream_cursor(last_cursor)
|
|
||||||
logger.info("Dream cron job completed, cursor advanced to {}", last_cursor)
|
|
||||||
else:
|
|
||||||
logger.warning(
|
|
||||||
"Dream cron job did not complete; cursor remains at {}",
|
|
||||||
store.get_last_dream_cursor(),
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Dream cron job failed")
|
|
||||||
finally:
|
|
||||||
if store.git.is_initialized():
|
|
||||||
msg = build_dream_commit_message(
|
|
||||||
"dream: periodic memory consolidation", resp,
|
|
||||||
)
|
|
||||||
sha = store.git.auto_commit(msg)
|
|
||||||
if sha:
|
|
||||||
logger.info("Dream commit: {}", sha)
|
|
||||||
store.compact_history()
|
|
||||||
prune_dream_sessions(agent.sessions.sessions_dir)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Heartbeat is a system job that checks HEARTBEAT.md for active tasks.
|
def _pick_heartbeat_target() -> tuple[str, str]:
|
||||||
if job.name == "heartbeat":
|
"""Pick a routable channel/chat target for heartbeat-triggered messages."""
|
||||||
heartbeat_file = config.workspace_path / "HEARTBEAT.md"
|
|
||||||
try:
|
|
||||||
content = heartbeat_file.read_text(encoding="utf-8")
|
|
||||||
except OSError:
|
|
||||||
logger.debug("Heartbeat: HEARTBEAT.md missing")
|
|
||||||
return None
|
|
||||||
if not _heartbeat_has_active_tasks(content):
|
|
||||||
logger.debug("Heartbeat: HEARTBEAT.md has no active tasks")
|
|
||||||
return None
|
|
||||||
|
|
||||||
channel, chat_id = _pick_heartbeat_target()
|
|
||||||
if channel == "cli":
|
|
||||||
return None
|
|
||||||
|
|
||||||
prompt = (
|
|
||||||
_HEARTBEAT_PREAMBLE
|
|
||||||
+ f"Review the following HEARTBEAT.md and report any active tasks:\n\n{content}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Internal check: funnel all output through the post-run gate so the
|
|
||||||
# turn can't deliver directly via the message tool and skip it.
|
|
||||||
suppress_token = None
|
|
||||||
if isinstance(message_tool, MessageTool):
|
|
||||||
suppress_token = message_tool.set_suppress_delivery(True)
|
|
||||||
try:
|
|
||||||
resp = await agent.process_direct(
|
|
||||||
prompt,
|
|
||||||
session_key="heartbeat",
|
|
||||||
channel=channel,
|
|
||||||
chat_id=chat_id,
|
|
||||||
on_progress=_silent,
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
if isinstance(message_tool, MessageTool) and suppress_token is not None:
|
|
||||||
message_tool.reset_suppress_delivery(suppress_token)
|
|
||||||
response = resp.content if resp else ""
|
|
||||||
|
|
||||||
# Keep a small tail of heartbeat history so the loop stays bounded.
|
|
||||||
session = agent.sessions.get_or_create("heartbeat")
|
|
||||||
session.retain_recent_legal_suffix(hb_cfg.keep_recent_messages)
|
|
||||||
agent.sessions.save(session)
|
|
||||||
|
|
||||||
if not response:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Fail closed: stay silent on evaluator failure instead of notifying.
|
|
||||||
should_notify = await evaluate_response(
|
|
||||||
response, prompt, agent.provider, agent.model,
|
|
||||||
default_notify=False,
|
|
||||||
)
|
|
||||||
if should_notify:
|
|
||||||
logger.info("Heartbeat: completed, delivering response")
|
|
||||||
await _deliver_to_channel(
|
|
||||||
OutboundMessage(channel=channel, chat_id=chat_id, content=response),
|
|
||||||
record=True,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.info("Heartbeat: silenced by post-run evaluation")
|
|
||||||
return response
|
|
||||||
|
|
||||||
reminder_note = (
|
|
||||||
"The scheduled time has arrived. Deliver this reminder to the user now, "
|
|
||||||
"as a brief and natural message in their language. Speak directly to them — "
|
|
||||||
"do not narrate progress, summarize, include user IDs, or add status reports "
|
|
||||||
"like 'Done' or 'Reminded'.\n\n"
|
|
||||||
f"Reminder: {job.payload.message}"
|
|
||||||
)
|
|
||||||
|
|
||||||
cron_tool = agent.tools.get("cron")
|
|
||||||
cron_token = None
|
|
||||||
if isinstance(cron_tool, CronTool):
|
|
||||||
cron_token = cron_tool.set_cron_context(True)
|
|
||||||
|
|
||||||
message_record_token = None
|
|
||||||
if isinstance(message_tool, MessageTool):
|
|
||||||
message_record_token = message_tool.set_record_channel_delivery(True)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resp = await agent.process_direct(
|
enabled = set(channels.enabled_channels)
|
||||||
reminder_note,
|
except NameError:
|
||||||
session_key=f"cron:{job.id}",
|
return "cli", "direct"
|
||||||
channel=job.payload.channel or "cli",
|
for item in session_manager.list_sessions():
|
||||||
chat_id=job.payload.to or "direct",
|
key = item.get("key") or ""
|
||||||
on_progress=_silent,
|
if ":" not in key:
|
||||||
)
|
continue
|
||||||
finally:
|
channel, chat_id = key.split(":", 1)
|
||||||
if isinstance(cron_tool, CronTool) and cron_token is not None:
|
if channel in {"cli", "system"}:
|
||||||
cron_tool.reset_cron_context(cron_token)
|
continue
|
||||||
if isinstance(message_tool, MessageTool) and message_record_token is not None:
|
if channel in enabled and chat_id:
|
||||||
message_tool.reset_record_channel_delivery(message_record_token)
|
return channel, chat_id
|
||||||
|
return "cli", "direct"
|
||||||
|
|
||||||
response = resp.content if resp else ""
|
cron_executor = CronJobExecutor(
|
||||||
|
agent=agent,
|
||||||
if job.payload.deliver and isinstance(message_tool, MessageTool) and message_tool._sent_in_turn:
|
bus=bus,
|
||||||
return response
|
deliver_to_channel=_deliver_to_channel,
|
||||||
|
get_channel=_get_channel,
|
||||||
if job.payload.deliver and job.payload.to and response:
|
evaluate_response=evaluate_response,
|
||||||
should_notify = await evaluate_response(
|
heartbeat_workspace=config.workspace_path,
|
||||||
response, reminder_note, agent.provider, agent.model,
|
heartbeat_preamble=_HEARTBEAT_PREAMBLE,
|
||||||
)
|
heartbeat_has_active_tasks=_heartbeat_has_active_tasks,
|
||||||
if should_notify:
|
pick_heartbeat_target=_pick_heartbeat_target,
|
||||||
await _deliver_to_channel(
|
heartbeat_keep_recent_messages=hb_cfg.keep_recent_messages,
|
||||||
OutboundMessage(
|
)
|
||||||
channel=job.payload.channel or "cli",
|
cron.on_job = cron_executor.run
|
||||||
chat_id=job.payload.to,
|
|
||||||
content=response,
|
|
||||||
metadata=dict(job.payload.channel_meta),
|
|
||||||
),
|
|
||||||
record=True,
|
|
||||||
session_key=job.payload.session_key,
|
|
||||||
)
|
|
||||||
return response
|
|
||||||
|
|
||||||
cron.on_job = on_cron_job
|
|
||||||
|
|
||||||
def _webui_runtime_model_name() -> str | None:
|
def _webui_runtime_model_name() -> str | None:
|
||||||
model = getattr(agent, "model", None)
|
model = getattr(agent, "model", None)
|
||||||
@@ -1164,20 +1033,6 @@ def _run_gateway(
|
|||||||
webui_runtime_capabilities=webui_runtime_capabilities,
|
webui_runtime_capabilities=webui_runtime_capabilities,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _pick_heartbeat_target() -> tuple[str, str]:
|
|
||||||
"""Pick a routable channel/chat target for heartbeat-triggered messages."""
|
|
||||||
enabled = set(channels.enabled_channels)
|
|
||||||
for item in session_manager.list_sessions():
|
|
||||||
key = item.get("key") or ""
|
|
||||||
if ":" not in key:
|
|
||||||
continue
|
|
||||||
channel, chat_id = key.split(":", 1)
|
|
||||||
if channel in {"cli", "system"}:
|
|
||||||
continue
|
|
||||||
if channel in enabled and chat_id:
|
|
||||||
return channel, chat_id
|
|
||||||
return "cli", "direct"
|
|
||||||
|
|
||||||
if channels.enabled_channels:
|
if channels.enabled_channels:
|
||||||
console.print(f"[green]✓[/green] Channels enabled: {', '.join(channels.enabled_channels)}")
|
console.print(f"[green]✓[/green] Channels enabled: {', '.join(channels.enabled_channels)}")
|
||||||
else:
|
else:
|
||||||
@@ -1187,7 +1042,6 @@ def _run_gateway(
|
|||||||
if cron_status["jobs"] > 0:
|
if cron_status["jobs"] > 0:
|
||||||
console.print(f"[green]✓[/green] Cron: {cron_status['jobs']} scheduled jobs")
|
console.print(f"[green]✓[/green] Cron: {cron_status['jobs']} scheduled jobs")
|
||||||
|
|
||||||
hb_cfg = config.gateway.heartbeat
|
|
||||||
if hb_cfg.enabled:
|
if hb_cfg.enabled:
|
||||||
console.print(f"[green]✓[/green] Heartbeat: every {hb_cfg.interval_s}s")
|
console.print(f"[green]✓[/green] Heartbeat: every {hb_cfg.interval_s}s")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -0,0 +1,354 @@
|
|||||||
|
"""Cron job execution for the gateway runtime."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Protocol
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
import nanobot.utils.evaluator as evaluator
|
||||||
|
from nanobot.agent.tools.cron import CronTool
|
||||||
|
from nanobot.agent.tools.message import MessageTool
|
||||||
|
from nanobot.bus.events import OutboundMessage
|
||||||
|
from nanobot.bus.queue import MessageBus
|
||||||
|
from nanobot.cron.types import CronJob
|
||||||
|
|
||||||
|
|
||||||
|
class DeliverToChannel(Protocol):
|
||||||
|
def __call__(
|
||||||
|
self,
|
||||||
|
msg: OutboundMessage,
|
||||||
|
*,
|
||||||
|
record: bool = False,
|
||||||
|
session_key: str | None = None,
|
||||||
|
) -> Awaitable[None]: ...
|
||||||
|
|
||||||
|
|
||||||
|
ChannelLookup = Callable[[str], Any | None]
|
||||||
|
EvaluateResponse = Callable[..., Awaitable[bool]]
|
||||||
|
HeartbeatTaskDetector = Callable[[str], bool]
|
||||||
|
HeartbeatTargetPicker = Callable[[], tuple[str, str]]
|
||||||
|
|
||||||
|
|
||||||
|
class _CronStreamBuffer:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
channel: str,
|
||||||
|
chat_id: str,
|
||||||
|
channel_meta: dict[str, Any],
|
||||||
|
base_id: str,
|
||||||
|
) -> None:
|
||||||
|
self.channel = channel
|
||||||
|
self.chat_id = chat_id
|
||||||
|
self.channel_meta = channel_meta
|
||||||
|
self.base_id = base_id
|
||||||
|
self.segment = 0
|
||||||
|
self.events: list[OutboundMessage] = []
|
||||||
|
self.has_delta = False
|
||||||
|
|
||||||
|
def _stream_id(self) -> str:
|
||||||
|
return f"{self.base_id}:{self.segment}"
|
||||||
|
|
||||||
|
async def on_stream(self, delta: str) -> None:
|
||||||
|
meta = dict(self.channel_meta)
|
||||||
|
meta["_stream_delta"] = True
|
||||||
|
meta["_stream_id"] = self._stream_id()
|
||||||
|
self.events.append(OutboundMessage(
|
||||||
|
channel=self.channel,
|
||||||
|
chat_id=self.chat_id,
|
||||||
|
content=delta,
|
||||||
|
metadata=meta,
|
||||||
|
))
|
||||||
|
if delta:
|
||||||
|
self.has_delta = True
|
||||||
|
|
||||||
|
async def on_stream_end(self, *, resuming: bool = False) -> None:
|
||||||
|
meta = dict(self.channel_meta)
|
||||||
|
meta["_stream_end"] = True
|
||||||
|
meta["_resuming"] = resuming
|
||||||
|
meta["_stream_id"] = self._stream_id()
|
||||||
|
self.events.append(OutboundMessage(
|
||||||
|
channel=self.channel,
|
||||||
|
chat_id=self.chat_id,
|
||||||
|
content="",
|
||||||
|
metadata=meta,
|
||||||
|
))
|
||||||
|
self.segment += 1
|
||||||
|
|
||||||
|
async def publish(self, bus: MessageBus) -> None:
|
||||||
|
for event in self.events:
|
||||||
|
await bus.publish_outbound(event)
|
||||||
|
|
||||||
|
|
||||||
|
class CronJobExecutor:
|
||||||
|
"""Runs scheduled cron jobs through the agent and optional channel delivery."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
agent: Any,
|
||||||
|
bus: MessageBus,
|
||||||
|
deliver_to_channel: DeliverToChannel,
|
||||||
|
get_channel: ChannelLookup | None = None,
|
||||||
|
evaluate_response: EvaluateResponse | None = None,
|
||||||
|
heartbeat_workspace: Path | None = None,
|
||||||
|
heartbeat_preamble: str = "",
|
||||||
|
heartbeat_has_active_tasks: HeartbeatTaskDetector | None = None,
|
||||||
|
pick_heartbeat_target: HeartbeatTargetPicker | None = None,
|
||||||
|
heartbeat_keep_recent_messages: int = 8,
|
||||||
|
) -> None:
|
||||||
|
self.agent = agent
|
||||||
|
self.bus = bus
|
||||||
|
self.deliver_to_channel = deliver_to_channel
|
||||||
|
self.get_channel = get_channel or (lambda _channel: None)
|
||||||
|
self.evaluate_response = evaluate_response or evaluator.evaluate_response
|
||||||
|
self.heartbeat_workspace = heartbeat_workspace
|
||||||
|
self.heartbeat_preamble = heartbeat_preamble
|
||||||
|
self.heartbeat_has_active_tasks = heartbeat_has_active_tasks
|
||||||
|
self.pick_heartbeat_target = pick_heartbeat_target
|
||||||
|
self.heartbeat_keep_recent_messages = heartbeat_keep_recent_messages
|
||||||
|
|
||||||
|
async def run(self, job: CronJob) -> str | None:
|
||||||
|
if job.name == "dream":
|
||||||
|
return await self._run_dream()
|
||||||
|
if job.name == "heartbeat":
|
||||||
|
return await self._run_heartbeat()
|
||||||
|
|
||||||
|
return await self._run_agent_turn(job)
|
||||||
|
|
||||||
|
async def _run_dream(self) -> None:
|
||||||
|
from nanobot.agent.memory import MemoryStore
|
||||||
|
|
||||||
|
dream_session_key = MemoryStore.dream_session_key
|
||||||
|
build_dream_commit_message = MemoryStore.build_dream_commit_message
|
||||||
|
prune_dream_sessions = MemoryStore.prune_dream_sessions
|
||||||
|
|
||||||
|
store = self.agent.context.memory
|
||||||
|
resp = None
|
||||||
|
try:
|
||||||
|
result = store.build_dream_prompt()
|
||||||
|
if result is None:
|
||||||
|
logger.info("Dream: nothing to process")
|
||||||
|
return None
|
||||||
|
prompt, last_cursor = result
|
||||||
|
resp = await self.agent.process_direct(
|
||||||
|
prompt,
|
||||||
|
session_key=dream_session_key(),
|
||||||
|
ephemeral=True,
|
||||||
|
tools=store.build_dream_tools(),
|
||||||
|
on_progress=self._silent,
|
||||||
|
)
|
||||||
|
if MemoryStore.dream_run_completed(resp):
|
||||||
|
store.set_last_dream_cursor(last_cursor)
|
||||||
|
logger.info("Dream cron job completed, cursor advanced to {}", last_cursor)
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"Dream cron job did not complete; cursor remains at {}",
|
||||||
|
store.get_last_dream_cursor(),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Dream cron job failed")
|
||||||
|
finally:
|
||||||
|
if store.git.is_initialized():
|
||||||
|
msg = build_dream_commit_message(
|
||||||
|
"dream: periodic memory consolidation", resp,
|
||||||
|
)
|
||||||
|
sha = store.git.auto_commit(msg)
|
||||||
|
if sha:
|
||||||
|
logger.info("Dream commit: {}", sha)
|
||||||
|
store.compact_history()
|
||||||
|
prune_dream_sessions(self.agent.sessions.sessions_dir)
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _run_heartbeat(self) -> str | None:
|
||||||
|
if (
|
||||||
|
self.heartbeat_workspace is None
|
||||||
|
or self.heartbeat_has_active_tasks is None
|
||||||
|
or self.pick_heartbeat_target is None
|
||||||
|
):
|
||||||
|
logger.warning("Heartbeat cron job skipped: executor is not configured for heartbeat")
|
||||||
|
return None
|
||||||
|
|
||||||
|
heartbeat_file = self.heartbeat_workspace / "HEARTBEAT.md"
|
||||||
|
try:
|
||||||
|
content = heartbeat_file.read_text(encoding="utf-8")
|
||||||
|
except OSError:
|
||||||
|
logger.debug("Heartbeat: HEARTBEAT.md missing")
|
||||||
|
return None
|
||||||
|
if not self.heartbeat_has_active_tasks(content):
|
||||||
|
logger.debug("Heartbeat: HEARTBEAT.md has no active tasks")
|
||||||
|
return None
|
||||||
|
|
||||||
|
channel, chat_id = self.pick_heartbeat_target()
|
||||||
|
if channel == "cli":
|
||||||
|
return None
|
||||||
|
|
||||||
|
prompt = (
|
||||||
|
self.heartbeat_preamble
|
||||||
|
+ f"Review the following HEARTBEAT.md and report any active tasks:\n\n{content}"
|
||||||
|
)
|
||||||
|
|
||||||
|
message_tool = self._tool("message")
|
||||||
|
suppress_token = None
|
||||||
|
if isinstance(message_tool, MessageTool):
|
||||||
|
suppress_token = message_tool.set_suppress_delivery(True)
|
||||||
|
try:
|
||||||
|
resp = await self.agent.process_direct(
|
||||||
|
prompt,
|
||||||
|
session_key="heartbeat",
|
||||||
|
channel=channel,
|
||||||
|
chat_id=chat_id,
|
||||||
|
on_progress=self._silent,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
if isinstance(message_tool, MessageTool) and suppress_token is not None:
|
||||||
|
message_tool.reset_suppress_delivery(suppress_token)
|
||||||
|
response = resp.content if resp else ""
|
||||||
|
|
||||||
|
session = self.agent.sessions.get_or_create("heartbeat")
|
||||||
|
session.retain_recent_legal_suffix(self.heartbeat_keep_recent_messages)
|
||||||
|
self.agent.sessions.save(session)
|
||||||
|
|
||||||
|
if not response:
|
||||||
|
return None
|
||||||
|
|
||||||
|
should_notify = await self.evaluate_response(
|
||||||
|
response, prompt, self.agent.provider, self.agent.model,
|
||||||
|
default_notify=False,
|
||||||
|
)
|
||||||
|
if should_notify:
|
||||||
|
logger.info("Heartbeat: completed, delivering response")
|
||||||
|
await self.deliver_to_channel(
|
||||||
|
OutboundMessage(channel=channel, chat_id=chat_id, content=response),
|
||||||
|
record=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.info("Heartbeat: silenced by post-run evaluation")
|
||||||
|
return response
|
||||||
|
|
||||||
|
async def _run_agent_turn(self, job: CronJob) -> str | None:
|
||||||
|
reminder_note = self._reminder_note(job)
|
||||||
|
cron_tool = self._tool("cron")
|
||||||
|
cron_token = None
|
||||||
|
if isinstance(cron_tool, CronTool):
|
||||||
|
cron_token = cron_tool.set_cron_context(True)
|
||||||
|
|
||||||
|
message_tool = self._tool("message")
|
||||||
|
message_record_token = None
|
||||||
|
if isinstance(message_tool, MessageTool):
|
||||||
|
message_record_token = message_tool.set_record_channel_delivery(True)
|
||||||
|
|
||||||
|
channel_name = job.payload.channel or "cli"
|
||||||
|
chat_id = job.payload.to or "direct"
|
||||||
|
stream = self._stream_buffer(job, channel_name=channel_name, chat_id=chat_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = await self.agent.process_direct(
|
||||||
|
reminder_note,
|
||||||
|
session_key=f"cron:{job.id}",
|
||||||
|
channel=channel_name,
|
||||||
|
chat_id=chat_id,
|
||||||
|
on_progress=self._silent,
|
||||||
|
on_stream=stream.on_stream if stream else None,
|
||||||
|
on_stream_end=stream.on_stream_end if stream else None,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
if isinstance(cron_tool, CronTool) and cron_token is not None:
|
||||||
|
cron_tool.reset_cron_context(cron_token)
|
||||||
|
if isinstance(message_tool, MessageTool) and message_record_token is not None:
|
||||||
|
message_tool.reset_record_channel_delivery(message_record_token)
|
||||||
|
|
||||||
|
response = resp.content if resp else ""
|
||||||
|
|
||||||
|
if job.payload.deliver and isinstance(message_tool, MessageTool) and message_tool._sent_in_turn:
|
||||||
|
await self._publish_turn_end_if_needed(job, channel_name=channel_name, chat_id=chat_id)
|
||||||
|
return response
|
||||||
|
|
||||||
|
delivered = False
|
||||||
|
if job.payload.deliver and job.payload.to and response:
|
||||||
|
should_notify = await self.evaluate_response(
|
||||||
|
response, reminder_note, self.agent.provider, self.agent.model,
|
||||||
|
)
|
||||||
|
if should_notify:
|
||||||
|
meta = dict(job.payload.channel_meta)
|
||||||
|
if stream and stream.has_delta:
|
||||||
|
await stream.publish(self.bus)
|
||||||
|
meta["_streamed"] = True
|
||||||
|
await self.deliver_to_channel(
|
||||||
|
OutboundMessage(
|
||||||
|
channel=channel_name,
|
||||||
|
chat_id=chat_id,
|
||||||
|
content=response,
|
||||||
|
metadata=meta,
|
||||||
|
),
|
||||||
|
record=True,
|
||||||
|
session_key=job.payload.session_key,
|
||||||
|
)
|
||||||
|
delivered = True
|
||||||
|
|
||||||
|
if delivered:
|
||||||
|
await self._publish_turn_end_if_needed(job, channel_name=channel_name, chat_id=chat_id)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def _tool(self, name: str) -> Any | None:
|
||||||
|
tools = getattr(self.agent, "tools", {})
|
||||||
|
if hasattr(tools, "get"):
|
||||||
|
return tools.get(name)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _stream_buffer(
|
||||||
|
self,
|
||||||
|
job: CronJob,
|
||||||
|
*,
|
||||||
|
channel_name: str,
|
||||||
|
chat_id: str,
|
||||||
|
) -> _CronStreamBuffer | None:
|
||||||
|
target_channel = self.get_channel(channel_name)
|
||||||
|
wants_stream = bool(
|
||||||
|
job.payload.deliver
|
||||||
|
and job.payload.to
|
||||||
|
and target_channel is not None
|
||||||
|
and target_channel.supports_streaming
|
||||||
|
)
|
||||||
|
if not wants_stream:
|
||||||
|
return None
|
||||||
|
return _CronStreamBuffer(
|
||||||
|
channel=channel_name,
|
||||||
|
chat_id=chat_id,
|
||||||
|
channel_meta=job.payload.channel_meta,
|
||||||
|
base_id=f"cron:{job.id}:{time.time_ns()}",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _publish_turn_end_if_needed(
|
||||||
|
self,
|
||||||
|
job: CronJob,
|
||||||
|
*,
|
||||||
|
channel_name: str,
|
||||||
|
chat_id: str,
|
||||||
|
) -> None:
|
||||||
|
if channel_name != "websocket" or not job.payload.to:
|
||||||
|
return
|
||||||
|
await self.bus.publish_outbound(OutboundMessage(
|
||||||
|
channel=channel_name,
|
||||||
|
chat_id=chat_id,
|
||||||
|
content="",
|
||||||
|
metadata={**job.payload.channel_meta, "_turn_end": True},
|
||||||
|
))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _silent(*_args: Any, **_kwargs: Any) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _reminder_note(job: CronJob) -> str:
|
||||||
|
return (
|
||||||
|
"The scheduled time has arrived. Deliver this reminder to the user now, "
|
||||||
|
"as a brief and natural message in their language. Speak directly to them — "
|
||||||
|
"do not narrate progress, summarize, include user IDs, or add status reports "
|
||||||
|
"like 'Done' or 'Reminded'.\n\n"
|
||||||
|
f"Reminder: {job.payload.message}"
|
||||||
|
)
|
||||||
@@ -79,466 +79,17 @@ def test_fetch_new_messages_parses_unseen_and_marks_seen(monkeypatch) -> None:
|
|||||||
monkeypatch.setattr("nanobot.channels.email.imaplib.IMAP4_SSL", lambda _h, _p: fake)
|
monkeypatch.setattr("nanobot.channels.email.imaplib.IMAP4_SSL", lambda _h, _p: fake)
|
||||||
|
|
||||||
channel = EmailChannel(_make_config(), MessageBus())
|
channel = EmailChannel(_make_config(), MessageBus())
|
||||||
items, skipped_uids = channel._fetch_new_messages()
|
items = channel._fetch_new_messages()
|
||||||
|
|
||||||
assert len(items) == 1
|
assert len(items) == 1
|
||||||
assert items[0]["sender"] == "alice@example.com"
|
assert items[0]["sender"] == "alice@example.com"
|
||||||
assert items[0]["subject"] == "Invoice"
|
assert items[0]["subject"] == "Invoice"
|
||||||
assert "Please pay" in items[0]["content"]
|
assert "Please pay" in items[0]["content"]
|
||||||
assert fake.store_calls == [(b"1", "+FLAGS", "\\Seen")]
|
assert fake.store_calls == [(b"1", "+FLAGS", "\\Seen")]
|
||||||
assert skipped_uids == set()
|
|
||||||
|
|
||||||
# Same UID should be deduped in-process.
|
# Same UID should be deduped in-process.
|
||||||
items_again, skipped_again = channel._fetch_new_messages()
|
items_again = channel._fetch_new_messages()
|
||||||
assert items_again == []
|
assert items_again == []
|
||||||
assert skipped_again == set()
|
|
||||||
|
|
||||||
|
|
||||||
def test_fetch_new_messages_returns_accepted_and_skipped_uids(monkeypatch) -> None:
|
|
||||||
raw = _make_raw_email(subject="Invoice", body="Please pay")
|
|
||||||
|
|
||||||
class FakeIMAP:
|
|
||||||
def login(self, _user: str, _pw: str):
|
|
||||||
return "OK", [b"logged in"]
|
|
||||||
|
|
||||||
def select(self, _mailbox: str):
|
|
||||||
return "OK", [b"1"]
|
|
||||||
|
|
||||||
def search(self, *_args):
|
|
||||||
return "OK", [b"1"]
|
|
||||||
|
|
||||||
def fetch(self, _imap_id: bytes, _parts: str):
|
|
||||||
return "OK", [(b"1 (UID 123 BODY[] {200})", raw), b")"]
|
|
||||||
|
|
||||||
def store(self, _imap_id: bytes, _op: str, _flags: str):
|
|
||||||
return "OK", [b""]
|
|
||||||
|
|
||||||
def logout(self):
|
|
||||||
return "BYE", [b""]
|
|
||||||
|
|
||||||
monkeypatch.setattr("nanobot.channels.email.imaplib.IMAP4_SSL", lambda _h, _p: FakeIMAP())
|
|
||||||
|
|
||||||
channel = EmailChannel(_make_config(post_action="delete"), MessageBus())
|
|
||||||
items, skipped_uids = channel._fetch_new_messages()
|
|
||||||
|
|
||||||
assert len(items) == 1
|
|
||||||
assert items[0]["metadata"]["uid"] == "123"
|
|
||||||
assert skipped_uids == set()
|
|
||||||
|
|
||||||
|
|
||||||
def test_fetch_new_messages_rejected_returns_skipped_uid(monkeypatch) -> None:
|
|
||||||
raw = _make_raw_email(from_addr="Nanobot <bot@example.com>", subject="Loop test")
|
|
||||||
|
|
||||||
class FakeIMAP:
|
|
||||||
def login(self, _user: str, _pw: str):
|
|
||||||
return "OK", [b"logged in"]
|
|
||||||
|
|
||||||
def select(self, _mailbox: str):
|
|
||||||
return "OK", [b"1"]
|
|
||||||
|
|
||||||
def search(self, *_args):
|
|
||||||
return "OK", [b"1"]
|
|
||||||
|
|
||||||
def fetch(self, _imap_id: bytes, _parts: str):
|
|
||||||
return "OK", [(b"1 (UID 123 BODY[] {200})", raw), b")"]
|
|
||||||
|
|
||||||
def store(self, _imap_id: bytes, _op: str, _flags: str):
|
|
||||||
return "OK", [b""]
|
|
||||||
|
|
||||||
def logout(self):
|
|
||||||
return "BYE", [b""]
|
|
||||||
|
|
||||||
monkeypatch.setattr("nanobot.channels.email.imaplib.IMAP4_SSL", lambda _h, _p: FakeIMAP())
|
|
||||||
|
|
||||||
channel_skip = EmailChannel(
|
|
||||||
_make_config(from_address="bot@example.com", post_action="delete", post_action_ignore_skipped=True),
|
|
||||||
MessageBus(),
|
|
||||||
)
|
|
||||||
assert channel_skip._fetch_new_messages() == ([], {"123"})
|
|
||||||
|
|
||||||
channel_apply = EmailChannel(
|
|
||||||
_make_config(from_address="bot@example.com", post_action="delete", post_action_ignore_skipped=False),
|
|
||||||
MessageBus(),
|
|
||||||
)
|
|
||||||
items, skipped_uids = channel_apply._fetch_new_messages()
|
|
||||||
assert items == []
|
|
||||||
assert skipped_uids == {"123"}
|
|
||||||
|
|
||||||
|
|
||||||
def test_apply_post_actions_batch_delete_uses_one_connection(monkeypatch) -> None:
|
|
||||||
raw = _make_raw_email(subject="Invoice", body="Please pay")
|
|
||||||
|
|
||||||
class FakeIMAP:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.search_calls: list[tuple] = []
|
|
||||||
self.uid_calls: list[tuple] = []
|
|
||||||
self.store_calls: list[tuple[bytes, str, str]] = []
|
|
||||||
self.expunge_calls = 0
|
|
||||||
|
|
||||||
def login(self, _user: str, _pw: str):
|
|
||||||
return "OK", [b"logged in"]
|
|
||||||
|
|
||||||
def select(self, _mailbox: str):
|
|
||||||
return "OK", [b"1"]
|
|
||||||
|
|
||||||
def search(self, *_args):
|
|
||||||
self.search_calls.append(_args)
|
|
||||||
if len(_args) >= 3 and _args[1] == "UID":
|
|
||||||
return "OK", [b"1"]
|
|
||||||
return "OK", [b"1"]
|
|
||||||
|
|
||||||
def capability(self):
|
|
||||||
return "OK", [b"IMAP4rev1 UIDPLUS"]
|
|
||||||
|
|
||||||
def uid(self, command: str, *args):
|
|
||||||
self.uid_calls.append((command, *args))
|
|
||||||
if command == "STORE":
|
|
||||||
return "OK", [b""]
|
|
||||||
if command == "EXPUNGE":
|
|
||||||
return "OK", [b""]
|
|
||||||
return "BAD", [b""]
|
|
||||||
|
|
||||||
def fetch(self, _imap_id: bytes, _parts: str):
|
|
||||||
return "OK", [(b"1 (UID 123 BODY[] {200})", raw), b")"]
|
|
||||||
|
|
||||||
def store(self, imap_id: bytes, op: str, flags: str):
|
|
||||||
self.store_calls.append((imap_id, op, flags))
|
|
||||||
return "OK", [b""]
|
|
||||||
|
|
||||||
def expunge(self):
|
|
||||||
self.expunge_calls += 1
|
|
||||||
return "OK", [b""]
|
|
||||||
|
|
||||||
def logout(self):
|
|
||||||
return "BYE", [b""]
|
|
||||||
|
|
||||||
fake = FakeIMAP()
|
|
||||||
monkeypatch.setattr("nanobot.channels.email.imaplib.IMAP4_SSL", lambda _h, _p: fake)
|
|
||||||
|
|
||||||
channel = EmailChannel(_make_config(post_action="delete"), MessageBus())
|
|
||||||
channel._apply_post_actions_batch(["123", "124"])
|
|
||||||
|
|
||||||
assert fake.store_calls == []
|
|
||||||
assert fake.expunge_calls == 0
|
|
||||||
assert fake.search_calls == []
|
|
||||||
assert fake.uid_calls == [
|
|
||||||
("STORE", "123", "+FLAGS", "(\\Deleted)"),
|
|
||||||
("EXPUNGE", "123"),
|
|
||||||
("STORE", "124", "+FLAGS", "(\\Deleted)"),
|
|
||||||
("EXPUNGE", "124"),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_apply_post_actions_batch_move_copies_then_deletes(monkeypatch) -> None:
|
|
||||||
class FakeIMAP:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.uid_calls: list[tuple] = []
|
|
||||||
self.store_calls: list[tuple[bytes, str, str]] = []
|
|
||||||
self.expunge_calls = 0
|
|
||||||
|
|
||||||
def login(self, _user: str, _pw: str):
|
|
||||||
return "OK", [b"logged in"]
|
|
||||||
|
|
||||||
def select(self, _mailbox: str):
|
|
||||||
return "OK", [b"1"]
|
|
||||||
|
|
||||||
def search(self, *_args):
|
|
||||||
return "OK", [b"1"]
|
|
||||||
|
|
||||||
def capability(self):
|
|
||||||
return "OK", [b"IMAP4rev1 UIDPLUS"]
|
|
||||||
|
|
||||||
def uid(self, command: str, *args):
|
|
||||||
self.uid_calls.append((command, *args))
|
|
||||||
if command == "COPY":
|
|
||||||
return "OK", [b""]
|
|
||||||
if command == "STORE":
|
|
||||||
return "OK", [b""]
|
|
||||||
if command == "EXPUNGE":
|
|
||||||
return "OK", [b""]
|
|
||||||
return "BAD", [b""]
|
|
||||||
|
|
||||||
def store(self, imap_id: bytes, op: str, flags: str):
|
|
||||||
self.store_calls.append((imap_id, op, flags))
|
|
||||||
return "OK", [b""]
|
|
||||||
|
|
||||||
def expunge(self):
|
|
||||||
self.expunge_calls += 1
|
|
||||||
return "OK", [b""]
|
|
||||||
|
|
||||||
def logout(self):
|
|
||||||
return "BYE", [b""]
|
|
||||||
|
|
||||||
fake = FakeIMAP()
|
|
||||||
monkeypatch.setattr("nanobot.channels.email.imaplib.IMAP4_SSL", lambda _h, _p: fake)
|
|
||||||
|
|
||||||
channel = EmailChannel(
|
|
||||||
_make_config(post_action="move", post_action_move_mailbox="Processed"),
|
|
||||||
MessageBus(),
|
|
||||||
)
|
|
||||||
channel._apply_post_actions_batch(["123"])
|
|
||||||
|
|
||||||
assert fake.uid_calls == [
|
|
||||||
("COPY", "123", "Processed"),
|
|
||||||
("STORE", "123", "+FLAGS", "(\\Deleted)"),
|
|
||||||
("EXPUNGE", "123"),
|
|
||||||
]
|
|
||||||
assert fake.store_calls == []
|
|
||||||
assert fake.expunge_calls == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_apply_post_actions_batch_move_prefers_uid_move_when_supported(monkeypatch) -> None:
|
|
||||||
class FakeIMAP:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.uid_calls: list[tuple] = []
|
|
||||||
|
|
||||||
def login(self, _user: str, _pw: str):
|
|
||||||
return "OK", [b"logged in"]
|
|
||||||
|
|
||||||
def select(self, _mailbox: str):
|
|
||||||
return "OK", [b"1"]
|
|
||||||
|
|
||||||
def capability(self):
|
|
||||||
return "OK", [b"IMAP4rev1 UIDPLUS MOVE"]
|
|
||||||
|
|
||||||
def uid(self, command: str, *args):
|
|
||||||
self.uid_calls.append((command, *args))
|
|
||||||
if command == "MOVE":
|
|
||||||
return "OK", [b""]
|
|
||||||
return "BAD", [b""]
|
|
||||||
|
|
||||||
def logout(self):
|
|
||||||
return "BYE", [b""]
|
|
||||||
|
|
||||||
fake = FakeIMAP()
|
|
||||||
monkeypatch.setattr("nanobot.channels.email.imaplib.IMAP4_SSL", lambda _h, _p: fake)
|
|
||||||
|
|
||||||
channel = EmailChannel(
|
|
||||||
_make_config(post_action="move", post_action_move_mailbox="Processed"),
|
|
||||||
MessageBus(),
|
|
||||||
)
|
|
||||||
channel._apply_post_actions_batch(["123"])
|
|
||||||
|
|
||||||
assert fake.uid_calls == [("MOVE", "123", "Processed")]
|
|
||||||
|
|
||||||
|
|
||||||
def test_apply_post_actions_batch_fallback_caches_uid_store_failure(monkeypatch) -> None:
|
|
||||||
class FakeIMAP:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.uid_calls: list[tuple] = []
|
|
||||||
self.search_calls: list[tuple] = []
|
|
||||||
self.store_calls: list[tuple[bytes, str, str]] = []
|
|
||||||
self.expunge_calls = 0
|
|
||||||
|
|
||||||
def login(self, _user: str, _pw: str):
|
|
||||||
return "OK", [b"logged in"]
|
|
||||||
|
|
||||||
def select(self, _mailbox: str):
|
|
||||||
return "OK", [b"2"]
|
|
||||||
|
|
||||||
def capability(self):
|
|
||||||
return "OK", [b"IMAP4rev1"]
|
|
||||||
|
|
||||||
def uid(self, command: str, *args):
|
|
||||||
self.uid_calls.append((command, *args))
|
|
||||||
if command == "STORE":
|
|
||||||
return "NO", [b"unsupported"]
|
|
||||||
return "BAD", [b""]
|
|
||||||
|
|
||||||
def search(self, *_args):
|
|
||||||
self.search_calls.append(_args)
|
|
||||||
if _args == (None, "UID", "123"):
|
|
||||||
return "OK", [b"1"]
|
|
||||||
if _args == (None, "UID", "124"):
|
|
||||||
return "OK", [b"2"]
|
|
||||||
return "NO", [b""]
|
|
||||||
|
|
||||||
def store(self, imap_id: bytes, op: str, flags: str):
|
|
||||||
self.store_calls.append((imap_id, op, flags))
|
|
||||||
return "OK", [b""]
|
|
||||||
|
|
||||||
def expunge(self):
|
|
||||||
self.expunge_calls += 1
|
|
||||||
return "OK", [b""]
|
|
||||||
|
|
||||||
def logout(self):
|
|
||||||
return "BYE", [b""]
|
|
||||||
|
|
||||||
fake = FakeIMAP()
|
|
||||||
monkeypatch.setattr("nanobot.channels.email.imaplib.IMAP4_SSL", lambda _h, _p: fake)
|
|
||||||
|
|
||||||
channel = EmailChannel(_make_config(post_action="delete"), MessageBus())
|
|
||||||
channel._apply_post_actions_batch(["123", "124"])
|
|
||||||
|
|
||||||
# UID STORE should be attempted only once, then cached as unsupported.
|
|
||||||
assert [call for call in fake.uid_calls if call[0] == "STORE"] == [("STORE", "123", "+FLAGS", "(\\Deleted)")]
|
|
||||||
assert fake.search_calls == [(None, "UID", "123"), (None, "UID", "124")]
|
|
||||||
assert fake.store_calls == [(b"1", "+FLAGS", "\\Deleted"), (b"2", "+FLAGS", "\\Deleted")]
|
|
||||||
# With post_action_expunge=False (default), no broad expunge is called
|
|
||||||
assert fake.expunge_calls == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_apply_post_actions_batch_delete_with_post_action_expunge_true_no_uidplus(monkeypatch) -> None:
|
|
||||||
"""When post_action_expunge=True and UIDPLUS is unsupported, broad expunge IS called."""
|
|
||||||
class FakeIMAP:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.uid_calls: list[tuple] = []
|
|
||||||
self.store_calls: list[tuple[bytes, str, str]] = []
|
|
||||||
self.expunge_calls = 0
|
|
||||||
|
|
||||||
def login(self, _user: str, _pw: str):
|
|
||||||
return "OK", [b"logged in"]
|
|
||||||
|
|
||||||
def select(self, _mailbox: str):
|
|
||||||
return "OK", [b"2"]
|
|
||||||
|
|
||||||
def capability(self):
|
|
||||||
return "OK", [b"IMAP4rev1"]
|
|
||||||
|
|
||||||
def uid(self, command: str, *args):
|
|
||||||
self.uid_calls.append((command, *args))
|
|
||||||
if command == "STORE":
|
|
||||||
return "NO", [b"unsupported"]
|
|
||||||
return "BAD", [b""]
|
|
||||||
|
|
||||||
def search(self, *_args):
|
|
||||||
uid_to_seq = {"123": b"1", "124": b"2"}
|
|
||||||
uid = _args[-1]
|
|
||||||
seq = uid_to_seq.get(uid, b"")
|
|
||||||
return "OK", [seq]
|
|
||||||
|
|
||||||
def store(self, imap_id: bytes, op: str, flags: str):
|
|
||||||
self.store_calls.append((imap_id, op, flags))
|
|
||||||
return "OK", [b""]
|
|
||||||
|
|
||||||
def expunge(self):
|
|
||||||
self.expunge_calls += 1
|
|
||||||
return "OK", [b""]
|
|
||||||
|
|
||||||
def logout(self):
|
|
||||||
return "BYE", [b""]
|
|
||||||
|
|
||||||
fake = FakeIMAP()
|
|
||||||
monkeypatch.setattr("nanobot.channels.email.imaplib.IMAP4_SSL", lambda _h, _p: fake)
|
|
||||||
|
|
||||||
channel = EmailChannel(_make_config(post_action="delete", post_action_expunge=True), MessageBus())
|
|
||||||
channel._apply_post_actions_batch(["123", "124"])
|
|
||||||
|
|
||||||
assert fake.store_calls == [(b"1", "+FLAGS", "\\Deleted"), (b"2", "+FLAGS", "\\Deleted")]
|
|
||||||
# Broad expunge called because post_action_expunge=True
|
|
||||||
assert fake.expunge_calls == 2
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_start_applies_post_action_only_after_delivery(monkeypatch) -> None:
|
|
||||||
calls: list[str] = []
|
|
||||||
|
|
||||||
channel = EmailChannel(_make_config(post_action="delete"), MessageBus())
|
|
||||||
|
|
||||||
fetched = ([
|
|
||||||
{
|
|
||||||
"sender": "alice@example.com",
|
|
||||||
"subject": "Hi",
|
|
||||||
"message_id": "<m1@example.com>",
|
|
||||||
"content": "hello",
|
|
||||||
"metadata": {"uid": "123"},
|
|
||||||
}
|
|
||||||
], [])
|
|
||||||
|
|
||||||
def _fake_fetch():
|
|
||||||
channel._running = False
|
|
||||||
return fetched
|
|
||||||
|
|
||||||
async def _fake_handle_message(**_kwargs):
|
|
||||||
calls.append("delivered")
|
|
||||||
|
|
||||||
def _fake_batch(actions):
|
|
||||||
assert calls == ["delivered"]
|
|
||||||
assert actions == ["123"]
|
|
||||||
calls.append("post_action")
|
|
||||||
|
|
||||||
monkeypatch.setattr(channel, "_fetch_new_messages", _fake_fetch)
|
|
||||||
monkeypatch.setattr(channel, "_handle_message", _fake_handle_message)
|
|
||||||
monkeypatch.setattr(channel, "_apply_post_actions_batch", _fake_batch)
|
|
||||||
|
|
||||||
await channel.start()
|
|
||||||
assert calls == ["delivered", "post_action"]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_start_skips_post_action_when_delivery_fails(monkeypatch) -> None:
|
|
||||||
called = {"post_action": False}
|
|
||||||
|
|
||||||
channel = EmailChannel(_make_config(post_action="delete"), MessageBus())
|
|
||||||
|
|
||||||
fetched = ([
|
|
||||||
{
|
|
||||||
"sender": "alice@example.com",
|
|
||||||
"subject": "Hi",
|
|
||||||
"message_id": "<m1@example.com>",
|
|
||||||
"content": "hello",
|
|
||||||
"metadata": {"uid": "123"},
|
|
||||||
}
|
|
||||||
], [])
|
|
||||||
|
|
||||||
def _fake_fetch():
|
|
||||||
channel._running = False
|
|
||||||
return fetched
|
|
||||||
|
|
||||||
async def _fake_handle_message(**_kwargs):
|
|
||||||
raise RuntimeError("delivery failed")
|
|
||||||
|
|
||||||
def _fake_batch(_actions):
|
|
||||||
called["post_action"] = True
|
|
||||||
|
|
||||||
monkeypatch.setattr(channel, "_fetch_new_messages", _fake_fetch)
|
|
||||||
monkeypatch.setattr(channel, "_handle_message", _fake_handle_message)
|
|
||||||
monkeypatch.setattr(channel, "_apply_post_actions_batch", _fake_batch)
|
|
||||||
|
|
||||||
await channel.start()
|
|
||||||
assert called["post_action"] is False
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_start_keeps_post_actions_for_successful_emails_when_later_delivery_fails(monkeypatch) -> None:
|
|
||||||
called_actions: list[str] = []
|
|
||||||
|
|
||||||
channel = EmailChannel(_make_config(post_action="delete"), MessageBus())
|
|
||||||
|
|
||||||
fetched = ([
|
|
||||||
{
|
|
||||||
"sender": "alice@example.com",
|
|
||||||
"subject": "First",
|
|
||||||
"message_id": "<m1@example.com>",
|
|
||||||
"content": "ok",
|
|
||||||
"metadata": {"uid": "123"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"sender": "bob@example.com",
|
|
||||||
"subject": "Second",
|
|
||||||
"message_id": "<m2@example.com>",
|
|
||||||
"content": "fail",
|
|
||||||
"metadata": {"uid": "124"},
|
|
||||||
},
|
|
||||||
], [])
|
|
||||||
|
|
||||||
def _fake_fetch():
|
|
||||||
channel._running = False
|
|
||||||
return fetched
|
|
||||||
|
|
||||||
async def _fake_handle_message(**kwargs):
|
|
||||||
if kwargs["chat_id"] == "bob@example.com":
|
|
||||||
raise RuntimeError("delivery failed")
|
|
||||||
|
|
||||||
def _fake_batch(actions):
|
|
||||||
called_actions.extend(actions)
|
|
||||||
|
|
||||||
monkeypatch.setattr(channel, "_fetch_new_messages", _fake_fetch)
|
|
||||||
monkeypatch.setattr(channel, "_handle_message", _fake_handle_message)
|
|
||||||
monkeypatch.setattr(channel, "_apply_post_actions_batch", _fake_batch)
|
|
||||||
|
|
||||||
await channel.start()
|
|
||||||
assert called_actions == ["123"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_fetch_new_messages_skips_self_sent_email_and_marks_seen(monkeypatch) -> None:
|
def test_fetch_new_messages_skips_self_sent_email_and_marks_seen(monkeypatch) -> None:
|
||||||
@@ -571,16 +122,14 @@ def test_fetch_new_messages_skips_self_sent_email_and_marks_seen(monkeypatch) ->
|
|||||||
monkeypatch.setattr("nanobot.channels.email.imaplib.IMAP4_SSL", lambda _h, _p: fake)
|
monkeypatch.setattr("nanobot.channels.email.imaplib.IMAP4_SSL", lambda _h, _p: fake)
|
||||||
|
|
||||||
channel = EmailChannel(_make_config(from_address="bot@example.com"), MessageBus())
|
channel = EmailChannel(_make_config(from_address="bot@example.com"), MessageBus())
|
||||||
items, skipped_uids = channel._fetch_new_messages()
|
items = channel._fetch_new_messages()
|
||||||
|
|
||||||
assert items == []
|
assert items == []
|
||||||
assert skipped_uids == {"123"}
|
|
||||||
assert fake.store_calls == [(b"1", "+FLAGS", "\\Seen")]
|
assert fake.store_calls == [(b"1", "+FLAGS", "\\Seen")]
|
||||||
|
|
||||||
# Same UID should still be deduped after being ignored.
|
# Same UID should still be deduped after being ignored.
|
||||||
items_again, skipped_again = channel._fetch_new_messages()
|
items_again = channel._fetch_new_messages()
|
||||||
assert items_again == []
|
assert items_again == []
|
||||||
assert skipped_again == set()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -640,7 +189,7 @@ def test_fetch_new_messages_skips_self_sent_across_identity_sources(
|
|||||||
monkeypatch.setattr("nanobot.channels.email.imaplib.IMAP4_SSL", lambda _h, _p: fake)
|
monkeypatch.setattr("nanobot.channels.email.imaplib.IMAP4_SSL", lambda _h, _p: fake)
|
||||||
|
|
||||||
channel = EmailChannel(_make_config(**config_override), MessageBus())
|
channel = EmailChannel(_make_config(**config_override), MessageBus())
|
||||||
items, _ = channel._fetch_new_messages()
|
items = channel._fetch_new_messages()
|
||||||
|
|
||||||
assert items == []
|
assert items == []
|
||||||
assert fake.store_calls == [(b"1", "+FLAGS", "\\Seen")]
|
assert fake.store_calls == [(b"1", "+FLAGS", "\\Seen")]
|
||||||
@@ -688,7 +237,7 @@ def test_fetch_new_messages_retries_once_when_imap_connection_goes_stale(monkeyp
|
|||||||
monkeypatch.setattr("nanobot.channels.email.imaplib.IMAP4_SSL", _factory)
|
monkeypatch.setattr("nanobot.channels.email.imaplib.IMAP4_SSL", _factory)
|
||||||
|
|
||||||
channel = EmailChannel(_make_config(), MessageBus())
|
channel = EmailChannel(_make_config(), MessageBus())
|
||||||
items, _ = channel._fetch_new_messages()
|
items = channel._fetch_new_messages()
|
||||||
|
|
||||||
assert len(items) == 1
|
assert len(items) == 1
|
||||||
assert len(fake_instances) == 2
|
assert len(fake_instances) == 2
|
||||||
@@ -734,7 +283,7 @@ def test_fetch_new_messages_keeps_messages_collected_before_stale_retry(monkeypa
|
|||||||
monkeypatch.setattr("nanobot.channels.email.imaplib.IMAP4_SSL", lambda _h, _p: FlakyIMAP())
|
monkeypatch.setattr("nanobot.channels.email.imaplib.IMAP4_SSL", lambda _h, _p: FlakyIMAP())
|
||||||
|
|
||||||
channel = EmailChannel(_make_config(), MessageBus())
|
channel = EmailChannel(_make_config(), MessageBus())
|
||||||
items, _ = channel._fetch_new_messages()
|
items = channel._fetch_new_messages()
|
||||||
|
|
||||||
assert [item["subject"] for item in items] == ["First", "Second"]
|
assert [item["subject"] for item in items] == ["First", "Second"]
|
||||||
|
|
||||||
@@ -757,12 +306,7 @@ def test_fetch_new_messages_skips_missing_mailbox(monkeypatch) -> None:
|
|||||||
|
|
||||||
channel = EmailChannel(_make_config(), MessageBus())
|
channel = EmailChannel(_make_config(), MessageBus())
|
||||||
|
|
||||||
assert channel._fetch_new_messages() == ([], set())
|
assert channel._fetch_new_messages() == []
|
||||||
|
|
||||||
|
|
||||||
def test_validate_config_requires_move_mailbox_for_move_post_action() -> None:
|
|
||||||
channel = EmailChannel(_make_config(post_action="move", post_action_move_mailbox=None), MessageBus())
|
|
||||||
assert channel._validate_config() is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_extract_text_body_falls_back_to_html() -> None:
|
def test_extract_text_body_falls_back_to_html() -> None:
|
||||||
@@ -1118,7 +662,7 @@ def test_spoofed_email_rejected_when_verify_enabled(monkeypatch) -> None:
|
|||||||
|
|
||||||
cfg = _make_config(verify_dkim=True, verify_spf=True)
|
cfg = _make_config(verify_dkim=True, verify_spf=True)
|
||||||
channel = EmailChannel(cfg, MessageBus())
|
channel = EmailChannel(cfg, MessageBus())
|
||||||
items, _ = channel._fetch_new_messages()
|
items = channel._fetch_new_messages()
|
||||||
|
|
||||||
assert len(items) == 0, "Spoofed email without auth headers should be rejected"
|
assert len(items) == 0, "Spoofed email without auth headers should be rejected"
|
||||||
|
|
||||||
@@ -1135,7 +679,7 @@ def test_email_with_valid_auth_results_accepted(monkeypatch) -> None:
|
|||||||
|
|
||||||
cfg = _make_config(verify_dkim=True, verify_spf=True)
|
cfg = _make_config(verify_dkim=True, verify_spf=True)
|
||||||
channel = EmailChannel(cfg, MessageBus())
|
channel = EmailChannel(cfg, MessageBus())
|
||||||
items, _ = channel._fetch_new_messages()
|
items = channel._fetch_new_messages()
|
||||||
|
|
||||||
assert len(items) == 1
|
assert len(items) == 1
|
||||||
assert items[0]["sender"] == "alice@example.com"
|
assert items[0]["sender"] == "alice@example.com"
|
||||||
@@ -1154,7 +698,7 @@ def test_email_with_partial_auth_rejected(monkeypatch) -> None:
|
|||||||
|
|
||||||
cfg = _make_config(verify_dkim=True, verify_spf=True)
|
cfg = _make_config(verify_dkim=True, verify_spf=True)
|
||||||
channel = EmailChannel(cfg, MessageBus())
|
channel = EmailChannel(cfg, MessageBus())
|
||||||
items, _ = channel._fetch_new_messages()
|
items = channel._fetch_new_messages()
|
||||||
|
|
||||||
assert len(items) == 0, "Email with dkim=fail should be rejected"
|
assert len(items) == 0, "Email with dkim=fail should be rejected"
|
||||||
|
|
||||||
@@ -1167,7 +711,7 @@ def test_backward_compat_verify_disabled(monkeypatch) -> None:
|
|||||||
|
|
||||||
cfg = _make_config(verify_dkim=False, verify_spf=False)
|
cfg = _make_config(verify_dkim=False, verify_spf=False)
|
||||||
channel = EmailChannel(cfg, MessageBus())
|
channel = EmailChannel(cfg, MessageBus())
|
||||||
items, _ = channel._fetch_new_messages()
|
items = channel._fetch_new_messages()
|
||||||
|
|
||||||
assert len(items) == 1, "With verification disabled, emails should be accepted as before"
|
assert len(items) == 1, "With verification disabled, emails should be accepted as before"
|
||||||
|
|
||||||
@@ -1180,7 +724,7 @@ def test_email_content_tagged_with_email_context(monkeypatch) -> None:
|
|||||||
|
|
||||||
cfg = _make_config(verify_dkim=False, verify_spf=False)
|
cfg = _make_config(verify_dkim=False, verify_spf=False)
|
||||||
channel = EmailChannel(cfg, MessageBus())
|
channel = EmailChannel(cfg, MessageBus())
|
||||||
items, _ = channel._fetch_new_messages()
|
items = channel._fetch_new_messages()
|
||||||
|
|
||||||
assert len(items) == 1
|
assert len(items) == 1
|
||||||
assert items[0]["content"].startswith("[EMAIL-CONTEXT]"), (
|
assert items[0]["content"].startswith("[EMAIL-CONTEXT]"), (
|
||||||
@@ -1292,7 +836,7 @@ def test_fetch_new_messages_ignores_unauthorized_sender_before_attachments(monke
|
|||||||
)
|
)
|
||||||
channel = EmailChannel(cfg, MessageBus())
|
channel = EmailChannel(cfg, MessageBus())
|
||||||
|
|
||||||
assert channel._fetch_new_messages() == ([], {"500"})
|
assert channel._fetch_new_messages() == []
|
||||||
assert called["attachments"] is False
|
assert called["attachments"] is False
|
||||||
assert fake.store_calls == [(b"1", "+FLAGS", "\\Seen")]
|
assert fake.store_calls == [(b"1", "+FLAGS", "\\Seen")]
|
||||||
|
|
||||||
@@ -1307,7 +851,7 @@ def test_extract_attachments_saves_pdf(tmp_path, monkeypatch) -> None:
|
|||||||
|
|
||||||
cfg = _make_config(allowed_attachment_types=["application/pdf"], verify_dkim=False, verify_spf=False)
|
cfg = _make_config(allowed_attachment_types=["application/pdf"], verify_dkim=False, verify_spf=False)
|
||||||
channel = EmailChannel(cfg, MessageBus())
|
channel = EmailChannel(cfg, MessageBus())
|
||||||
items, _ = channel._fetch_new_messages()
|
items = channel._fetch_new_messages()
|
||||||
|
|
||||||
assert len(items) == 1
|
assert len(items) == 1
|
||||||
assert len(items[0]["media"]) == 1
|
assert len(items[0]["media"]) == 1
|
||||||
@@ -1327,7 +871,7 @@ def test_extract_attachments_disabled_by_default(monkeypatch) -> None:
|
|||||||
cfg = _make_config(verify_dkim=False, verify_spf=False)
|
cfg = _make_config(verify_dkim=False, verify_spf=False)
|
||||||
assert cfg.allowed_attachment_types == []
|
assert cfg.allowed_attachment_types == []
|
||||||
channel = EmailChannel(cfg, MessageBus())
|
channel = EmailChannel(cfg, MessageBus())
|
||||||
items, _ = channel._fetch_new_messages()
|
items = channel._fetch_new_messages()
|
||||||
|
|
||||||
assert len(items) == 1
|
assert len(items) == 1
|
||||||
assert items[0]["media"] == []
|
assert items[0]["media"] == []
|
||||||
@@ -1352,7 +896,7 @@ def test_extract_attachments_mime_type_filter(tmp_path, monkeypatch) -> None:
|
|||||||
verify_spf=False,
|
verify_spf=False,
|
||||||
)
|
)
|
||||||
channel = EmailChannel(cfg, MessageBus())
|
channel = EmailChannel(cfg, MessageBus())
|
||||||
items, _ = channel._fetch_new_messages()
|
items = channel._fetch_new_messages()
|
||||||
|
|
||||||
assert len(items) == 1
|
assert len(items) == 1
|
||||||
assert items[0]["media"] == []
|
assert items[0]["media"] == []
|
||||||
@@ -1376,7 +920,7 @@ def test_extract_attachments_empty_allowed_types_rejects_all(tmp_path, monkeypat
|
|||||||
verify_spf=False,
|
verify_spf=False,
|
||||||
)
|
)
|
||||||
channel = EmailChannel(cfg, MessageBus())
|
channel = EmailChannel(cfg, MessageBus())
|
||||||
items, _ = channel._fetch_new_messages()
|
items = channel._fetch_new_messages()
|
||||||
|
|
||||||
assert len(items) == 1
|
assert len(items) == 1
|
||||||
assert items[0]["media"] == []
|
assert items[0]["media"] == []
|
||||||
@@ -1400,7 +944,7 @@ def test_extract_attachments_wildcard_pattern(tmp_path, monkeypatch) -> None:
|
|||||||
verify_spf=False,
|
verify_spf=False,
|
||||||
)
|
)
|
||||||
channel = EmailChannel(cfg, MessageBus())
|
channel = EmailChannel(cfg, MessageBus())
|
||||||
items, _ = channel._fetch_new_messages()
|
items = channel._fetch_new_messages()
|
||||||
|
|
||||||
assert len(items) == 1
|
assert len(items) == 1
|
||||||
assert len(items[0]["media"]) == 1
|
assert len(items[0]["media"]) == 1
|
||||||
@@ -1423,7 +967,7 @@ def test_extract_attachments_size_limit(tmp_path, monkeypatch) -> None:
|
|||||||
verify_spf=False,
|
verify_spf=False,
|
||||||
)
|
)
|
||||||
channel = EmailChannel(cfg, MessageBus())
|
channel = EmailChannel(cfg, MessageBus())
|
||||||
items, _ = channel._fetch_new_messages()
|
items = channel._fetch_new_messages()
|
||||||
|
|
||||||
assert len(items) == 1
|
assert len(items) == 1
|
||||||
assert items[0]["media"] == []
|
assert items[0]["media"] == []
|
||||||
@@ -1459,7 +1003,7 @@ def test_extract_attachments_max_count(tmp_path, monkeypatch) -> None:
|
|||||||
verify_spf=False,
|
verify_spf=False,
|
||||||
)
|
)
|
||||||
channel = EmailChannel(cfg, MessageBus())
|
channel = EmailChannel(cfg, MessageBus())
|
||||||
items, _ = channel._fetch_new_messages()
|
items = channel._fetch_new_messages()
|
||||||
|
|
||||||
assert len(items) == 1
|
assert len(items) == 1
|
||||||
assert len(items[0]["media"]) == 2
|
assert len(items[0]["media"]) == 2
|
||||||
@@ -1477,7 +1021,7 @@ def test_extract_attachments_sanitizes_filename(tmp_path, monkeypatch) -> None:
|
|||||||
|
|
||||||
cfg = _make_config(allowed_attachment_types=["*"], verify_dkim=False, verify_spf=False)
|
cfg = _make_config(allowed_attachment_types=["*"], verify_dkim=False, verify_spf=False)
|
||||||
channel = EmailChannel(cfg, MessageBus())
|
channel = EmailChannel(cfg, MessageBus())
|
||||||
items, _ = channel._fetch_new_messages()
|
items = channel._fetch_new_messages()
|
||||||
|
|
||||||
assert len(items) == 1
|
assert len(items) == 1
|
||||||
assert len(items[0]["media"]) == 1
|
assert len(items[0]["media"]) == 1
|
||||||
|
|||||||
@@ -1421,6 +1421,262 @@ def test_gateway_cron_job_suppresses_intermediate_progress(
|
|||||||
bus.publish_outbound.assert_not_awaited()
|
bus.publish_outbound.assert_not_awaited()
|
||||||
|
|
||||||
|
|
||||||
|
def test_gateway_cron_job_streams_when_channel_supports_it(
|
||||||
|
monkeypatch, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
|
"""Cron jobs on streaming channels must emit deltas with stream_id and turn_end."""
|
||||||
|
config_file = tmp_path / "instance" / "config.json"
|
||||||
|
config_file.parent.mkdir(parents=True)
|
||||||
|
config_file.write_text("{}")
|
||||||
|
|
||||||
|
config = Config()
|
||||||
|
config.agents.defaults.workspace = str(tmp_path / "config-workspace")
|
||||||
|
bus = MagicMock()
|
||||||
|
bus.publish_outbound = AsyncMock()
|
||||||
|
seen: dict[str, object] = {}
|
||||||
|
|
||||||
|
monkeypatch.setattr("nanobot.config.loader.set_config_path", lambda _path: None)
|
||||||
|
monkeypatch.setattr("nanobot.config.loader.load_config", lambda _path=None: config)
|
||||||
|
monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None)
|
||||||
|
monkeypatch.setattr("nanobot.providers.factory.make_provider", lambda _config: _fake_provider())
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"nanobot.providers.factory.build_provider_snapshot",
|
||||||
|
lambda _config: _test_provider_snapshot(object(), _config),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"nanobot.providers.factory.load_provider_snapshot",
|
||||||
|
lambda _config_path=None: _test_provider_snapshot(object(), config),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr("nanobot.bus.queue.MessageBus", lambda: bus)
|
||||||
|
monkeypatch.setattr("nanobot.session.manager.SessionManager", lambda _workspace: object())
|
||||||
|
|
||||||
|
async def _always_notify(*_args, **_kwargs) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
class _FakeStreamingChannel:
|
||||||
|
supports_streaming = True
|
||||||
|
|
||||||
|
class _FakeChannelManager:
|
||||||
|
def __init__(self, *_args, **_kwargs) -> None:
|
||||||
|
self.channels = {"websocket": _FakeStreamingChannel()}
|
||||||
|
self.enabled_channels = ["websocket"]
|
||||||
|
|
||||||
|
async def start_all(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def stop_all(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class _FakeCron:
|
||||||
|
def __init__(self, _store_path: Path) -> None:
|
||||||
|
self.on_job = None
|
||||||
|
seen["cron"] = self
|
||||||
|
|
||||||
|
def status(self):
|
||||||
|
return {"enabled": True, "jobs": 0, "next_wake_at_ms": None}
|
||||||
|
|
||||||
|
def register_system_job(self, job):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class _FakeAgentLoop:
|
||||||
|
@classmethod
|
||||||
|
def from_config(cls, config, bus=None, **extra):
|
||||||
|
return cls(**extra)
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
self.model = "test-model"
|
||||||
|
self.provider = object()
|
||||||
|
self.tools = {}
|
||||||
|
self.dream = MagicMock()
|
||||||
|
self.sessions = MagicMock()
|
||||||
|
|
||||||
|
async def process_direct(self, *_args, on_stream=None, on_stream_end=None, **_kwargs):
|
||||||
|
seen["on_stream"] = on_stream
|
||||||
|
seen["on_stream_end"] = on_stream_end
|
||||||
|
if on_stream:
|
||||||
|
await on_stream("Hello")
|
||||||
|
await on_stream(" world")
|
||||||
|
if on_stream_end:
|
||||||
|
await on_stream_end(resuming=False)
|
||||||
|
return OutboundMessage(
|
||||||
|
channel="websocket",
|
||||||
|
chat_id="user-1",
|
||||||
|
content="Hello world",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def close_mcp(self) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def run(self) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
monkeypatch.setattr("nanobot.cron.service.CronService", _FakeCron)
|
||||||
|
monkeypatch.setattr("nanobot.cli.commands.AgentLoop", _FakeAgentLoop)
|
||||||
|
monkeypatch.setattr("nanobot.channels.manager.ChannelManager", _FakeChannelManager)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"nanobot.cli.commands.evaluate_response",
|
||||||
|
_always_notify,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = runner.invoke(app, ["gateway", "--config", str(config_file)])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
cron = seen["cron"]
|
||||||
|
job = CronJob(
|
||||||
|
id="cron-stream-test",
|
||||||
|
name="test-stream",
|
||||||
|
payload=CronPayload(
|
||||||
|
message="Say hello.",
|
||||||
|
deliver=True,
|
||||||
|
channel="websocket",
|
||||||
|
to="user-1",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
response = asyncio.run(cron.on_job(job))
|
||||||
|
|
||||||
|
assert response == "Hello world"
|
||||||
|
assert seen["on_stream"] is not None
|
||||||
|
assert seen["on_stream_end"] is not None
|
||||||
|
|
||||||
|
calls = bus.publish_outbound.await_args_list
|
||||||
|
# First two calls are streaming deltas
|
||||||
|
assert calls[0].args[0].metadata.get("_stream_delta") is True
|
||||||
|
assert calls[0].args[0].metadata.get("_stream_id") is not None
|
||||||
|
assert calls[0].args[0].content == "Hello"
|
||||||
|
assert calls[1].args[0].metadata.get("_stream_delta") is True
|
||||||
|
assert calls[1].args[0].metadata.get("_stream_id") == calls[0].args[0].metadata["_stream_id"]
|
||||||
|
assert calls[1].args[0].content == " world"
|
||||||
|
# Third call is stream_end
|
||||||
|
assert calls[2].args[0].metadata.get("_stream_end") is True
|
||||||
|
assert calls[2].args[0].metadata.get("_stream_id") == calls[0].args[0].metadata["_stream_id"]
|
||||||
|
# Fourth call is the final message with _streamed marker
|
||||||
|
assert calls[3].args[0].metadata.get("_streamed") is True
|
||||||
|
assert calls[3].args[0].content == "Hello world"
|
||||||
|
# Fifth call is turn_end
|
||||||
|
assert calls[4].args[0].metadata.get("_turn_end") is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_gateway_cron_job_streaming_respects_disabled_delivery(
|
||||||
|
monkeypatch, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
|
"""Streaming cron output must not reach the channel when delivery is disabled."""
|
||||||
|
config_file = tmp_path / "instance" / "config.json"
|
||||||
|
config_file.parent.mkdir(parents=True)
|
||||||
|
config_file.write_text("{}")
|
||||||
|
|
||||||
|
config = Config()
|
||||||
|
config.agents.defaults.workspace = str(tmp_path / "config-workspace")
|
||||||
|
bus = MagicMock()
|
||||||
|
bus.publish_outbound = AsyncMock()
|
||||||
|
seen: dict[str, object] = {}
|
||||||
|
|
||||||
|
monkeypatch.setattr("nanobot.config.loader.set_config_path", lambda _path: None)
|
||||||
|
monkeypatch.setattr("nanobot.config.loader.load_config", lambda _path=None: config)
|
||||||
|
monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None)
|
||||||
|
monkeypatch.setattr("nanobot.providers.factory.make_provider", lambda _config: _fake_provider())
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"nanobot.providers.factory.build_provider_snapshot",
|
||||||
|
lambda _config: _test_provider_snapshot(object(), _config),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"nanobot.providers.factory.load_provider_snapshot",
|
||||||
|
lambda _config_path=None: _test_provider_snapshot(object(), config),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr("nanobot.bus.queue.MessageBus", lambda: bus)
|
||||||
|
monkeypatch.setattr("nanobot.session.manager.SessionManager", lambda _workspace: object())
|
||||||
|
|
||||||
|
class _FakeStreamingChannel:
|
||||||
|
supports_streaming = True
|
||||||
|
|
||||||
|
class _FakeChannelManager:
|
||||||
|
def __init__(self, *_args, **_kwargs) -> None:
|
||||||
|
self.channels = {"websocket": _FakeStreamingChannel()}
|
||||||
|
self.enabled_channels = ["websocket"]
|
||||||
|
|
||||||
|
async def start_all(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def stop_all(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class _FakeCron:
|
||||||
|
def __init__(self, _store_path: Path) -> None:
|
||||||
|
self.on_job = None
|
||||||
|
seen["cron"] = self
|
||||||
|
|
||||||
|
def status(self):
|
||||||
|
return {"enabled": True, "jobs": 0, "next_wake_at_ms": None}
|
||||||
|
|
||||||
|
def register_system_job(self, job):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class _FakeAgentLoop:
|
||||||
|
@classmethod
|
||||||
|
def from_config(cls, config, bus=None, **extra):
|
||||||
|
return cls(**extra)
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
self.model = "test-model"
|
||||||
|
self.provider = object()
|
||||||
|
self.tools = {}
|
||||||
|
self.dream = MagicMock()
|
||||||
|
self.sessions = MagicMock()
|
||||||
|
|
||||||
|
async def process_direct(self, *_args, on_stream=None, on_stream_end=None, **_kwargs):
|
||||||
|
seen["on_stream"] = on_stream
|
||||||
|
seen["on_stream_end"] = on_stream_end
|
||||||
|
if on_stream:
|
||||||
|
await on_stream("This should not leak")
|
||||||
|
if on_stream_end:
|
||||||
|
await on_stream_end(resuming=False)
|
||||||
|
return OutboundMessage(
|
||||||
|
channel="websocket",
|
||||||
|
chat_id="user-1",
|
||||||
|
content="This should not leak",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def close_mcp(self) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def run(self) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
monkeypatch.setattr("nanobot.cron.service.CronService", _FakeCron)
|
||||||
|
monkeypatch.setattr("nanobot.cli.commands.AgentLoop", _FakeAgentLoop)
|
||||||
|
monkeypatch.setattr("nanobot.channels.manager.ChannelManager", _FakeChannelManager)
|
||||||
|
|
||||||
|
result = runner.invoke(app, ["gateway", "--config", str(config_file)])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
cron = seen["cron"]
|
||||||
|
job = CronJob(
|
||||||
|
id="cron-stream-rejected-test",
|
||||||
|
name="test-stream-rejected",
|
||||||
|
payload=CronPayload(
|
||||||
|
message="Say something optional.",
|
||||||
|
deliver=False,
|
||||||
|
channel="websocket",
|
||||||
|
to="user-1",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
response = asyncio.run(cron.on_job(job))
|
||||||
|
|
||||||
|
assert response == "This should not leak"
|
||||||
|
assert seen["on_stream"] is None
|
||||||
|
assert seen["on_stream_end"] is None
|
||||||
|
bus.publish_outbound.assert_not_awaited()
|
||||||
|
|
||||||
|
|
||||||
def test_gateway_workspace_override_does_not_migrate_legacy_cron(
|
def test_gateway_workspace_override_does_not_migrate_legacy_cron(
|
||||||
monkeypatch, tmp_path: Path
|
monkeypatch, tmp_path: Path
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
+1
-174
@@ -27,7 +27,6 @@
|
|||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.1.0",
|
"@testing-library/react": "^16.1.0",
|
||||||
@@ -38,16 +37,12 @@
|
|||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^10.4.0",
|
|
||||||
"eslint-plugin-react-hooks": "^7.1.1",
|
|
||||||
"globals": "^17.6.0",
|
|
||||||
"happy-dom": "^16.3.0",
|
"happy-dom": "^16.3.0",
|
||||||
"katex": "^0.16.21",
|
"katex": "^0.16.21",
|
||||||
"postcss": "^8.5.0",
|
"postcss": "^8.5.0",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
"typescript-eslint": "^8.59.4",
|
|
||||||
"vite": "^5.4.11",
|
"vite": "^5.4.11",
|
||||||
"vitest": "^2.1.8",
|
"vitest": "^2.1.8",
|
||||||
},
|
},
|
||||||
@@ -144,22 +139,6 @@
|
|||||||
|
|
||||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
|
||||||
|
|
||||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
|
||||||
|
|
||||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
|
||||||
|
|
||||||
"@eslint/config-array": ["@eslint/config-array@0.23.5", "https://registry.npmmirror.com/@eslint/config-array/-/config-array-0.23.5.tgz", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="],
|
|
||||||
|
|
||||||
"@eslint/config-helpers": ["@eslint/config-helpers@0.6.0", "https://registry.npmmirror.com/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA=="],
|
|
||||||
|
|
||||||
"@eslint/core": ["@eslint/core@1.2.1", "https://registry.npmmirror.com/@eslint/core/-/core-1.2.1.tgz", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="],
|
|
||||||
|
|
||||||
"@eslint/js": ["@eslint/js@10.0.1", "https://registry.npmmirror.com/@eslint/js/-/js-10.0.1.tgz", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="],
|
|
||||||
|
|
||||||
"@eslint/object-schema": ["@eslint/object-schema@3.0.5", "https://registry.npmmirror.com/@eslint/object-schema/-/object-schema-3.0.5.tgz", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="],
|
|
||||||
|
|
||||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.2", "https://registry.npmmirror.com/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A=="],
|
|
||||||
|
|
||||||
"@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="],
|
"@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="],
|
||||||
|
|
||||||
"@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="],
|
"@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="],
|
||||||
@@ -168,16 +147,6 @@
|
|||||||
|
|
||||||
"@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="],
|
"@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="],
|
||||||
|
|
||||||
"@humanfs/core": ["@humanfs/core@0.19.2", "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.2.tgz", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="],
|
|
||||||
|
|
||||||
"@humanfs/node": ["@humanfs/node@0.16.8", "https://registry.npmmirror.com/@humanfs/node/-/node-0.16.8.tgz", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="],
|
|
||||||
|
|
||||||
"@humanfs/types": ["@humanfs/types@0.15.0", "https://registry.npmmirror.com/@humanfs/types/-/types-0.15.0.tgz", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="],
|
|
||||||
|
|
||||||
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
|
|
||||||
|
|
||||||
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.4.3.tgz", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
|
||||||
|
|
||||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||||
|
|
||||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||||
@@ -330,16 +299,12 @@
|
|||||||
|
|
||||||
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
|
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
|
||||||
|
|
||||||
"@types/esrecurse": ["@types/esrecurse@4.3.1", "https://registry.npmmirror.com/@types/esrecurse/-/esrecurse-4.3.1.tgz", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
|
|
||||||
|
|
||||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
|
|
||||||
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
|
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
|
||||||
|
|
||||||
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
||||||
|
|
||||||
"@types/json-schema": ["@types/json-schema@7.0.15", "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
|
||||||
|
|
||||||
"@types/katex": ["@types/katex@0.16.8", "", {}, "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg=="],
|
"@types/katex": ["@types/katex@0.16.8", "", {}, "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg=="],
|
||||||
|
|
||||||
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
||||||
@@ -358,26 +323,6 @@
|
|||||||
|
|
||||||
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.60.1", "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.60.1", "@typescript-eslint/type-utils": "8.60.1", "@typescript-eslint/utils": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.60.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.60.1", "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.60.1.tgz", { "dependencies": { "@typescript-eslint/scope-manager": "8.60.1", "@typescript-eslint/types": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.60.1", "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.60.1.tgz", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.60.1", "@typescript-eslint/types": "^8.60.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.60.1", "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz", { "dependencies": { "@typescript-eslint/types": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1" } }, "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.60.1", "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.60.1", "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz", { "dependencies": { "@typescript-eslint/types": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1", "@typescript-eslint/utils": "8.60.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.60.1", "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.60.1.tgz", {}, "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.60.1", "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz", { "dependencies": { "@typescript-eslint/project-service": "8.60.1", "@typescript-eslint/tsconfig-utils": "8.60.1", "@typescript-eslint/types": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.60.1", "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.60.1.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.60.1", "@typescript-eslint/types": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.60.1", "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz", { "dependencies": { "@typescript-eslint/types": "8.60.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag=="],
|
|
||||||
|
|
||||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||||
|
|
||||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
|
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
|
||||||
@@ -396,12 +341,6 @@
|
|||||||
|
|
||||||
"@vitest/utils": ["@vitest/utils@2.1.9", "", { "dependencies": { "@vitest/pretty-format": "2.1.9", "loupe": "^3.1.2", "tinyrainbow": "^1.2.0" } }, "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ=="],
|
"@vitest/utils": ["@vitest/utils@2.1.9", "", { "dependencies": { "@vitest/pretty-format": "2.1.9", "loupe": "^3.1.2", "tinyrainbow": "^1.2.0" } }, "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ=="],
|
||||||
|
|
||||||
"acorn": ["acorn@8.16.0", "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
|
||||||
|
|
||||||
"acorn-jsx": ["acorn-jsx@5.3.2", "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
|
||||||
|
|
||||||
"ajv": ["ajv@6.15.0", "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
|
|
||||||
|
|
||||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
"ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
|
"ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
|
||||||
@@ -422,14 +361,10 @@
|
|||||||
|
|
||||||
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
||||||
|
|
||||||
"balanced-match": ["balanced-match@4.0.4", "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
|
||||||
|
|
||||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g=="],
|
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g=="],
|
||||||
|
|
||||||
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
||||||
|
|
||||||
"brace-expansion": ["brace-expansion@5.0.6", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.6.tgz", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
|
|
||||||
|
|
||||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||||
|
|
||||||
"browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
|
"browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
|
||||||
@@ -466,8 +401,6 @@
|
|||||||
|
|
||||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||||
|
|
||||||
"cross-spawn": ["cross-spawn@7.0.6", "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
|
||||||
|
|
||||||
"css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="],
|
"css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="],
|
||||||
|
|
||||||
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
||||||
@@ -480,8 +413,6 @@
|
|||||||
|
|
||||||
"deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
|
"deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
|
||||||
|
|
||||||
"deep-is": ["deep-is@0.1.4", "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
|
||||||
|
|
||||||
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||||
|
|
||||||
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
||||||
@@ -506,58 +437,26 @@
|
|||||||
|
|
||||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
"escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||||
|
|
||||||
"eslint": ["eslint@10.4.1", "https://registry.npmmirror.com/eslint/-/eslint-10.4.1.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw=="],
|
|
||||||
|
|
||||||
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.1.1", "https://registry.npmmirror.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g=="],
|
|
||||||
|
|
||||||
"eslint-scope": ["eslint-scope@9.1.2", "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-9.1.2.tgz", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
|
|
||||||
|
|
||||||
"eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
|
||||||
|
|
||||||
"espree": ["espree@11.2.0", "https://registry.npmmirror.com/espree/-/espree-11.2.0.tgz", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="],
|
|
||||||
|
|
||||||
"esquery": ["esquery@1.7.0", "https://registry.npmmirror.com/esquery/-/esquery-1.7.0.tgz", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
|
|
||||||
|
|
||||||
"esrecurse": ["esrecurse@4.3.0", "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
|
||||||
|
|
||||||
"estraverse": ["estraverse@5.3.0", "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
|
||||||
|
|
||||||
"estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
|
"estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
|
||||||
|
|
||||||
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||||
|
|
||||||
"esutils": ["esutils@2.0.3", "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
|
||||||
|
|
||||||
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
|
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
|
||||||
|
|
||||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||||
|
|
||||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
|
||||||
|
|
||||||
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||||
|
|
||||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
|
||||||
|
|
||||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
|
||||||
|
|
||||||
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
|
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
|
||||||
|
|
||||||
"fault": ["fault@1.0.4", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA=="],
|
"fault": ["fault@1.0.4", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA=="],
|
||||||
|
|
||||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||||
|
|
||||||
"file-entry-cache": ["file-entry-cache@8.0.0", "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
|
||||||
|
|
||||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||||
|
|
||||||
"find-up": ["find-up@5.0.0", "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
|
||||||
|
|
||||||
"flat-cache": ["flat-cache@4.0.1", "https://registry.npmmirror.com/flat-cache/-/flat-cache-4.0.1.tgz", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
|
||||||
|
|
||||||
"flatted": ["flatted@3.4.2", "https://registry.npmmirror.com/flatted/-/flatted-3.4.2.tgz", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
|
|
||||||
|
|
||||||
"format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="],
|
"format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="],
|
||||||
|
|
||||||
"fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
|
"fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
|
||||||
@@ -572,8 +471,6 @@
|
|||||||
|
|
||||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||||
|
|
||||||
"globals": ["globals@17.6.0", "https://registry.npmmirror.com/globals/-/globals-17.6.0.tgz", {}, "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA=="],
|
|
||||||
|
|
||||||
"happy-dom": ["happy-dom@16.8.1", "", { "dependencies": { "webidl-conversions": "^7.0.0", "whatwg-mimetype": "^3.0.0" } }, "sha512-n0QrmT9lD81rbpKsyhnlz3DgnMZlaOkJPpgi746doA+HvaMC79bdWkwjrNnGJRvDrWTI8iOcJiVTJ5CdT/AZRw=="],
|
"happy-dom": ["happy-dom@16.8.1", "", { "dependencies": { "webidl-conversions": "^7.0.0", "whatwg-mimetype": "^3.0.0" } }, "sha512-n0QrmT9lD81rbpKsyhnlz3DgnMZlaOkJPpgi746doA+HvaMC79bdWkwjrNnGJRvDrWTI8iOcJiVTJ5CdT/AZRw=="],
|
||||||
|
|
||||||
"hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="],
|
"hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="],
|
||||||
@@ -598,10 +495,6 @@
|
|||||||
|
|
||||||
"hastscript": ["hastscript@6.0.0", "", { "dependencies": { "@types/hast": "^2.0.0", "comma-separated-tokens": "^1.0.0", "hast-util-parse-selector": "^2.0.0", "property-information": "^5.0.0", "space-separated-tokens": "^1.0.0" } }, "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w=="],
|
"hastscript": ["hastscript@6.0.0", "", { "dependencies": { "@types/hast": "^2.0.0", "comma-separated-tokens": "^1.0.0", "hast-util-parse-selector": "^2.0.0", "property-information": "^5.0.0", "space-separated-tokens": "^1.0.0" } }, "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w=="],
|
||||||
|
|
||||||
"hermes-estree": ["hermes-estree@0.25.1", "https://registry.npmmirror.com/hermes-estree/-/hermes-estree-0.25.1.tgz", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="],
|
|
||||||
|
|
||||||
"hermes-parser": ["hermes-parser@0.25.1", "https://registry.npmmirror.com/hermes-parser/-/hermes-parser-0.25.1.tgz", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
|
|
||||||
|
|
||||||
"highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="],
|
"highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="],
|
||||||
|
|
||||||
"highlightjs-vue": ["highlightjs-vue@1.0.0", "", {}, "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA=="],
|
"highlightjs-vue": ["highlightjs-vue@1.0.0", "", {}, "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA=="],
|
||||||
@@ -612,10 +505,6 @@
|
|||||||
|
|
||||||
"i18next": ["i18next@26.2.0", "", { "peerDependencies": { "typescript": "^5 || ^6" }, "optionalPeers": ["typescript"] }, "sha512-zwBHldHdTmwN7r6UNc7lC6GWNN+YYg3DrRSeHR5PRRBf5QnJZcYHrQc0uaU26qZeYxR7iFZD+Y315dPnKP47wA=="],
|
"i18next": ["i18next@26.2.0", "", { "peerDependencies": { "typescript": "^5 || ^6" }, "optionalPeers": ["typescript"] }, "sha512-zwBHldHdTmwN7r6UNc7lC6GWNN+YYg3DrRSeHR5PRRBf5QnJZcYHrQc0uaU26qZeYxR7iFZD+Y315dPnKP47wA=="],
|
||||||
|
|
||||||
"ignore": ["ignore@5.3.2", "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
|
||||||
|
|
||||||
"imurmurhash": ["imurmurhash@0.1.4", "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
|
||||||
|
|
||||||
"indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
|
"indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
|
||||||
|
|
||||||
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
|
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
|
||||||
@@ -640,34 +529,20 @@
|
|||||||
|
|
||||||
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||||
|
|
||||||
"isexe": ["isexe@2.0.0", "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
|
||||||
|
|
||||||
"jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="],
|
"jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="],
|
||||||
|
|
||||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||||
|
|
||||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||||
|
|
||||||
"json-buffer": ["json-buffer@3.0.1", "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
|
||||||
|
|
||||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
|
||||||
|
|
||||||
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
|
||||||
|
|
||||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||||
|
|
||||||
"katex": ["katex@0.16.45", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA=="],
|
"katex": ["katex@0.16.45", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA=="],
|
||||||
|
|
||||||
"keyv": ["keyv@4.5.4", "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
|
||||||
|
|
||||||
"levn": ["levn@0.4.1", "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
|
||||||
|
|
||||||
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
|
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
|
||||||
|
|
||||||
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
||||||
|
|
||||||
"locate-path": ["locate-path@6.0.0", "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
|
||||||
|
|
||||||
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
||||||
|
|
||||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||||
@@ -784,16 +659,12 @@
|
|||||||
|
|
||||||
"min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="],
|
"min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="],
|
||||||
|
|
||||||
"minimatch": ["minimatch@10.2.5", "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
|
|
||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
|
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
|
||||||
|
|
||||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
"natural-compare": ["natural-compare@1.4.0", "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
|
||||||
|
|
||||||
"node-releases": ["node-releases@2.0.37", "", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="],
|
"node-releases": ["node-releases@2.0.37", "", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="],
|
||||||
|
|
||||||
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||||
@@ -802,20 +673,10 @@
|
|||||||
|
|
||||||
"object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="],
|
"object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="],
|
||||||
|
|
||||||
"optionator": ["optionator@0.9.4", "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
|
||||||
|
|
||||||
"p-limit": ["p-limit@3.1.0", "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
|
||||||
|
|
||||||
"p-locate": ["p-locate@5.0.0", "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
|
||||||
|
|
||||||
"parse-entities": ["parse-entities@2.0.0", "", { "dependencies": { "character-entities": "^1.0.0", "character-entities-legacy": "^1.0.0", "character-reference-invalid": "^1.0.0", "is-alphanumerical": "^1.0.0", "is-decimal": "^1.0.0", "is-hexadecimal": "^1.0.0" } }, "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ=="],
|
"parse-entities": ["parse-entities@2.0.0", "", { "dependencies": { "character-entities": "^1.0.0", "character-entities-legacy": "^1.0.0", "character-reference-invalid": "^1.0.0", "is-alphanumerical": "^1.0.0", "is-decimal": "^1.0.0", "is-hexadecimal": "^1.0.0" } }, "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ=="],
|
||||||
|
|
||||||
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
||||||
|
|
||||||
"path-exists": ["path-exists@4.0.0", "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
|
||||||
|
|
||||||
"path-key": ["path-key@3.1.1", "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
|
||||||
|
|
||||||
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
||||||
|
|
||||||
"pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
"pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
||||||
@@ -844,16 +705,12 @@
|
|||||||
|
|
||||||
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||||
|
|
||||||
"prelude-ls": ["prelude-ls@1.2.1", "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
|
||||||
|
|
||||||
"pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
|
"pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
|
||||||
|
|
||||||
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
|
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
|
||||||
|
|
||||||
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
|
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
|
||||||
|
|
||||||
"punycode": ["punycode@2.3.1", "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
|
||||||
|
|
||||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||||
|
|
||||||
"react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
|
"react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
|
||||||
@@ -910,10 +767,6 @@
|
|||||||
|
|
||||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
"shebang-command": ["shebang-command@2.0.0", "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
|
||||||
|
|
||||||
"shebang-regex": ["shebang-regex@3.0.0", "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
|
||||||
|
|
||||||
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
|
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
|
||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
@@ -964,18 +817,12 @@
|
|||||||
|
|
||||||
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
||||||
|
|
||||||
"ts-api-utils": ["ts-api-utils@2.5.0", "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
|
|
||||||
|
|
||||||
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
|
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
|
||||||
|
|
||||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
"type-check": ["type-check@0.4.0", "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
|
||||||
|
|
||||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
"typescript-eslint": ["typescript-eslint@8.60.1", "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.60.1.tgz", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.60.1", "@typescript-eslint/parser": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1", "@typescript-eslint/utils": "8.60.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA=="],
|
|
||||||
|
|
||||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
|
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
|
||||||
@@ -996,8 +843,6 @@
|
|||||||
|
|
||||||
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||||
|
|
||||||
"uri-js": ["uri-js@4.4.1", "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
|
||||||
|
|
||||||
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
|
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
|
||||||
|
|
||||||
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
|
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
|
||||||
@@ -1026,26 +871,14 @@
|
|||||||
|
|
||||||
"whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="],
|
"whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="],
|
||||||
|
|
||||||
"which": ["which@2.0.2", "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
|
||||||
|
|
||||||
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
|
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
|
||||||
|
|
||||||
"word-wrap": ["word-wrap@1.2.5", "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
|
||||||
|
|
||||||
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||||
|
|
||||||
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||||
|
|
||||||
"yocto-queue": ["yocto-queue@0.1.0", "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
|
||||||
|
|
||||||
"zod": ["zod@4.4.3", "https://registry.npmmirror.com/zod/-/zod-4.4.3.tgz", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
|
|
||||||
|
|
||||||
"zod-validation-error": ["zod-validation-error@4.0.2", "https://registry.npmmirror.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
|
|
||||||
|
|
||||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||||
|
|
||||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
"@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
"@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
"@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
@@ -1064,10 +897,6 @@
|
|||||||
|
|
||||||
"@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
|
"@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/semver": ["semver@7.8.1", "https://registry.npmmirror.com/semver/-/semver-7.8.1.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
|
|
||||||
|
|
||||||
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
|
|
||||||
"decode-named-character-reference/character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
|
"decode-named-character-reference/character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
|
||||||
@@ -1086,8 +915,6 @@
|
|||||||
|
|
||||||
"hastscript/space-separated-tokens": ["space-separated-tokens@1.1.5", "", {}, "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA=="],
|
"hastscript/space-separated-tokens": ["space-separated-tokens@1.1.5", "", {}, "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA=="],
|
||||||
|
|
||||||
"mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
|
||||||
|
|
||||||
"mdast-util-mdx-jsx/parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
|
"mdast-util-mdx-jsx/parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
|
||||||
|
|
||||||
"postcss-nested/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
|
"postcss-nested/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
|
||||||
|
|||||||
+1
-9
@@ -813,7 +813,6 @@ function Shell({
|
|||||||
navigate(defaultShellRoute());
|
navigate(defaultShellRoute());
|
||||||
setDraftWorkspaceScope(null);
|
setDraftWorkspaceScope(null);
|
||||||
setWorkspaceError(null);
|
setWorkspaceError(null);
|
||||||
setSessionSearchOpen(false);
|
|
||||||
setMobileSidebarOpen(false);
|
setMobileSidebarOpen(false);
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
@@ -1008,13 +1007,6 @@ function Shell({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (event: globalThis.KeyboardEvent) => {
|
const handleKeyDown = (event: globalThis.KeyboardEvent) => {
|
||||||
if (event.defaultPrevented) return;
|
if (event.defaultPrevented) return;
|
||||||
const commandShiftO =
|
|
||||||
(event.metaKey || event.ctrlKey) && event.shiftKey && !event.altKey;
|
|
||||||
if (commandShiftO && event.key.toLowerCase() === "o") {
|
|
||||||
event.preventDefault();
|
|
||||||
onNewChat();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const plainCommandK =
|
const plainCommandK =
|
||||||
(event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey;
|
(event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey;
|
||||||
if (!plainCommandK) return;
|
if (!plainCommandK) return;
|
||||||
@@ -1025,7 +1017,7 @@ function Shell({
|
|||||||
|
|
||||||
window.addEventListener("keydown", handleKeyDown);
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
}, [onNewChat, onOpenSessionSearch]);
|
}, [onOpenSessionSearch]);
|
||||||
|
|
||||||
const onSelectSearchResult = useCallback(
|
const onSelectSearchResult = useCallback(
|
||||||
(key: string) => {
|
(key: string) => {
|
||||||
|
|||||||
@@ -124,7 +124,6 @@ export function Sidebar(props: SidebarProps) {
|
|||||||
label={t("sidebar.newChat")}
|
label={t("sidebar.newChat")}
|
||||||
onClick={props.onNewChat}
|
onClick={props.onNewChat}
|
||||||
icon={<SquarePen className="h-4 w-4" />}
|
icon={<SquarePen className="h-4 w-4" />}
|
||||||
shortcut="Cmd/Ctrl+Shift+O"
|
|
||||||
/>
|
/>
|
||||||
<SidebarActionButton
|
<SidebarActionButton
|
||||||
collapsed={collapsed}
|
collapsed={collapsed}
|
||||||
@@ -214,7 +213,6 @@ function SidebarActionButton({
|
|||||||
onClick,
|
onClick,
|
||||||
active = false,
|
active = false,
|
||||||
className,
|
className,
|
||||||
shortcut,
|
|
||||||
}: {
|
}: {
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -222,17 +220,14 @@ function SidebarActionButton({
|
|||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
shortcut?: string;
|
|
||||||
}) {
|
}) {
|
||||||
const title = shortcut ? `${label} (${shortcut})` : collapsed ? label : undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
aria-current={active ? "page" : undefined}
|
aria-current={active ? "page" : undefined}
|
||||||
title={title}
|
title={collapsed ? label : undefined}
|
||||||
onClick={() => onClick()}
|
onClick={() => onClick()}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group h-8 min-w-0 gap-2 overflow-hidden rounded-full font-medium text-sidebar-foreground/85 hover:bg-sidebar-accent/75 hover:text-sidebar-foreground",
|
"group h-8 min-w-0 gap-2 overflow-hidden rounded-full font-medium text-sidebar-foreground/85 hover:bg-sidebar-accent/75 hover:text-sidebar-foreground",
|
||||||
|
|||||||
@@ -1354,67 +1354,6 @@ describe("App layout", () => {
|
|||||||
expect(createChatSpy).not.toHaveBeenCalled();
|
expect(createChatSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
|
||||||
["Command", { metaKey: true }],
|
|
||||||
["Control", { ctrlKey: true }],
|
|
||||||
])("starts a new chat from the %s keyboard shortcut", async (_label, modifier) => {
|
|
||||||
mockSessions = [
|
|
||||||
{
|
|
||||||
key: "websocket:chat-a",
|
|
||||||
channel: "websocket",
|
|
||||||
chatId: "chat-a",
|
|
||||||
createdAt: "2026-04-16T10:00:00Z",
|
|
||||||
updatedAt: "2026-04-16T10:00:00Z",
|
|
||||||
preview: "Existing chat",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
render(<App />);
|
|
||||||
|
|
||||||
await waitFor(() => expect(connectSpy).toHaveBeenCalled());
|
|
||||||
fireEvent.keyDown(window, { key: "O", shiftKey: true, ...modifier });
|
|
||||||
|
|
||||||
expect(window.location.hash).toBe("#/new");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("closes search when starting a new chat from the keyboard shortcut", async () => {
|
|
||||||
mockSessions = [
|
|
||||||
{
|
|
||||||
key: "websocket:chat-a",
|
|
||||||
channel: "websocket",
|
|
||||||
chatId: "chat-a",
|
|
||||||
createdAt: "2026-04-16T10:00:00Z",
|
|
||||||
updatedAt: "2026-04-16T10:00:00Z",
|
|
||||||
preview: "Existing chat",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
render(<App />);
|
|
||||||
|
|
||||||
await waitFor(() => expect(connectSpy).toHaveBeenCalled());
|
|
||||||
fireEvent.keyDown(window, { key: "k", metaKey: true });
|
|
||||||
expect(await screen.findByRole("dialog", { name: "Search" })).toBeInTheDocument();
|
|
||||||
|
|
||||||
fireEvent.keyDown(window, { key: "O", shiftKey: true, metaKey: true });
|
|
||||||
|
|
||||||
await waitFor(() =>
|
|
||||||
expect(screen.queryByRole("dialog", { name: "Search" })).not.toBeInTheDocument(),
|
|
||||||
);
|
|
||||||
expect(window.location.hash).toBe("#/new");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("exposes the new chat keyboard shortcut in the sidebar title", async () => {
|
|
||||||
render(<App />);
|
|
||||||
|
|
||||||
await waitFor(() => expect(connectSpy).toHaveBeenCalled());
|
|
||||||
const sidebar = screen.getByRole("navigation", { name: "Sidebar navigation" });
|
|
||||||
|
|
||||||
expect(within(sidebar).getByRole("button", { name: "New chat" })).toHaveAttribute(
|
|
||||||
"title",
|
|
||||||
"New chat (Cmd/Ctrl+Shift+O)",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps large sidebars light while search still covers every chat", async () => {
|
it("keeps large sidebars light while search still covers every chat", async () => {
|
||||||
mockSessions = Array.from({ length: 170 }, (_, index) => {
|
mockSessions = Array.from({ length: 170 }, (_, index) => {
|
||||||
const chatId = `chat-${index}`;
|
const chatId = `chat-${index}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user