Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
283eab1eb0 | ||
|
|
7f4d500302 | ||
|
|
cf48d2db3e | ||
|
|
114058e6ae | ||
|
|
eb8187ff6e | ||
|
|
53dc805fc3 | ||
|
|
ed8489616e |
No files matched your search
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
_commit: v0.3.3
|
_commit: v0.4.0
|
||||||
_src_path: https://git.kotikot.com/templates/backend-python.git
|
_src_path: https://git.kotikot.com/templates/backend-python.git
|
||||||
author_email: h@kotikot.com
|
author_email: h@kotikot.com
|
||||||
author_name: h
|
author_name: h
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
_commit: v0.3.1
|
_commit: v0.4.2
|
||||||
_src_path: https://git.kotikot.com/templates/frontend-svelte.git
|
_src_path: https://git.kotikot.com/templates/frontend-svelte.git
|
||||||
include_impeccable: false
|
|
||||||
install_deps: false
|
install_deps: false
|
||||||
layout: part
|
layout: part
|
||||||
project_name: beavergram
|
project_name: beavergram
|
||||||
|
|||||||
+4
-1
@@ -1,7 +1,10 @@
|
|||||||
_commit: v0.4.0
|
_commit: v0.5.0
|
||||||
_src_path: https://git.kotikot.com/templates/infra.git
|
_src_path: https://git.kotikot.com/templates/infra.git
|
||||||
init_git: false
|
init_git: false
|
||||||
layout: parts
|
layout: parts
|
||||||
|
mcp_servers:
|
||||||
|
- context7
|
||||||
|
- svelte
|
||||||
parts:
|
parts:
|
||||||
- backend
|
- backend
|
||||||
- frontend
|
- frontend
|
||||||
|
|||||||
@@ -6,3 +6,6 @@ docker-compose.override.yml
|
|||||||
.idea
|
.idea
|
||||||
docs
|
docs
|
||||||
storage
|
storage
|
||||||
|
|
||||||
|
# Local playground
|
||||||
|
t/
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"svelte": {
|
"context7": {
|
||||||
"type": "http",
|
"type": "http",
|
||||||
"url": "https://mcp.svelte.dev/mcp"
|
"url": "https://mcp.context7.com/mcp"
|
||||||
},
|
},
|
||||||
"context7": {
|
"svelte": {
|
||||||
"type": "http",
|
"type": "http",
|
||||||
"url": "https://mcp.context7.com/mcp"
|
"url": "https://mcp.svelte.dev/mcp"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,8 +3,17 @@ PARTS := $(patsubst %/Makefile,%,$(wildcard */Makefile))
|
|||||||
.PHONY: env recreate rebuild restart logs down deploy fmt check build pre-deploy post-deploy
|
.PHONY: env recreate rebuild restart logs down deploy fmt check build pre-deploy post-deploy
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@test -f .env || awk 'FNR == 1 && NR > 1 { print "" } 1' .env.example $(addsuffix /.env.example,$(PARTS)) > .env
|
@fresh=; test -f .env || { awk 'FNR == 1 && NR > 1 { print "" } 1' .env.example $(addsuffix /.env.example,$(PARTS)) > .env; fresh=1; }; \
|
||||||
@for part in $(PARTS); do test -f $$part/docker-compose.override.yml || cp $$part/docker-compose.override.yml.example $$part/docker-compose.override.yml; done
|
for part in $(PARTS); do \
|
||||||
|
override=$$part/docker-compose.override.yml; \
|
||||||
|
test -f $$override && continue; \
|
||||||
|
cp $$override.example $$override; \
|
||||||
|
for port in $$(sed -n 's/.*127\.0\.0\.1:\([0-9]*\):.*/\1/p' $$override | sort -u); do \
|
||||||
|
random=$$((20000 + $$(od -An -N2 -tu2 /dev/urandom) % 40000)); \
|
||||||
|
sed "s/127\.0\.0\.1:$$port:/127.0.0.1:$$random:/" $$override > $$override.tmp && mv $$override.tmp $$override; \
|
||||||
|
test -z "$$fresh" || { sed "s|localhost:$$port/|localhost:$$random/|" .env > .env.tmp && mv .env.tmp .env; }; \
|
||||||
|
done; \
|
||||||
|
done
|
||||||
|
|
||||||
recreate:
|
recreate:
|
||||||
docker compose up -d --force-recreate
|
docker compose up -d --force-recreate
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ services:
|
|||||||
caddy:
|
caddy:
|
||||||
aliases:
|
aliases:
|
||||||
- beavergram-api
|
- beavergram-api
|
||||||
|
# ports:
|
||||||
|
# - "127.0.0.1:8080:${API__PORT}"
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
ports:
|
# ports:
|
||||||
- "127.0.0.1:${DB__PORT:-5432}:5432"
|
# - "127.0.0.1:5432:${DB__PORT}"
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
caddy:
|
caddy:
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ services:
|
|||||||
POSTGRES_USER: ${DB__USER:-beavergram}
|
POSTGRES_USER: ${DB__USER:-beavergram}
|
||||||
POSTGRES_PASSWORD: ${DB__PASSWORD:-beavergram}
|
POSTGRES_PASSWORD: ${DB__PASSWORD:-beavergram}
|
||||||
POSTGRES_DB: ${DB__DB_NAME:-beavergram}
|
POSTGRES_DB: ${DB__DB_NAME:-beavergram}
|
||||||
|
PGPORT: ${DB__PORT:-5432}
|
||||||
volumes:
|
volumes:
|
||||||
- pgdata:/var/lib/postgresql/data
|
- pgdata:/var/lib/postgresql/data
|
||||||
- ./backend/migrations/init:/docker-entrypoint-initdb.d:ro
|
- ./backend/migrations/init:/docker-entrypoint-initdb.d:ro
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import asyncio
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import asyncpg
|
||||||
|
from fastmcp.exceptions import ToolError
|
||||||
|
from fastmcp.server.dependencies import get_http_request
|
||||||
|
from fastmcp.utilities.types import Image
|
||||||
|
from mcp.types import TextContent
|
||||||
|
|
||||||
|
from utils.files import expiry_from_seconds
|
||||||
|
from utils.jobs import enqueue
|
||||||
|
from utils.read import chats, media, shares
|
||||||
|
from utils.read.models import FileShareView, MediaView
|
||||||
|
from utils.storage import ContentAddressedStorage
|
||||||
|
|
||||||
|
LINK_TTL_SECONDS = 24 * 3600
|
||||||
|
MAX_WAIT_SECONDS = 120
|
||||||
|
_POLL_SECONDS = 1.0
|
||||||
|
_INLINE_IMAGE_BYTES = 5 * 1024 * 1024
|
||||||
|
_INLINE_TEXT_BYTES = 256 * 1024
|
||||||
|
_INLINE_IMAGE_MIMES = {"image/jpeg", "image/png", "image/gif", "image/webp"}
|
||||||
|
_TEXT_MIMES = {"application/json", "application/xml", "application/x-yaml"}
|
||||||
|
_FAILED_JOB = ("failed", "canceled")
|
||||||
|
|
||||||
|
|
||||||
|
async def _job_error(pool: asyncpg.Pool, job_id: int) -> str | None:
|
||||||
|
row = await pool.fetchrow("SELECT status, error FROM jobs WHERE id = $1", job_id)
|
||||||
|
if row is None or row["status"] in _FAILED_JOB:
|
||||||
|
return (row and row["error"]) or "download job is gone"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_media(
|
||||||
|
pool: asyncpg.Pool,
|
||||||
|
account_id: int,
|
||||||
|
chat_id: int,
|
||||||
|
message_id: int,
|
||||||
|
wait_seconds: float,
|
||||||
|
) -> MediaView | None:
|
||||||
|
item = await media.get_message_media(pool, account_id, chat_id, message_id)
|
||||||
|
if item is None:
|
||||||
|
msg = f"message {chat_id}/{message_id} has no media"
|
||||||
|
raise ToolError(msg)
|
||||||
|
if item.downloaded and item.storage_key:
|
||||||
|
return item
|
||||||
|
job_id = await enqueue(
|
||||||
|
pool, account_id, "fetch_media", {"chat_id": chat_id, "message_id": message_id}
|
||||||
|
)
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
deadline = loop.time() + wait_seconds
|
||||||
|
while loop.time() < deadline:
|
||||||
|
await asyncio.sleep(_POLL_SECONDS)
|
||||||
|
item = await media.get_message_media(pool, account_id, chat_id, message_id)
|
||||||
|
if item is not None and item.downloaded and item.storage_key:
|
||||||
|
return item
|
||||||
|
error = await _job_error(pool, job_id)
|
||||||
|
if error is not None:
|
||||||
|
msg = f"download failed: {error}"
|
||||||
|
raise ToolError(msg)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _origin() -> str:
|
||||||
|
request = get_http_request()
|
||||||
|
proto = request.headers.get("x-forwarded-proto", request.url.scheme)
|
||||||
|
return f"{proto}://{request.url.netloc}"
|
||||||
|
|
||||||
|
|
||||||
|
async def share_media(pool: asyncpg.Pool, item: MediaView, mime: str) -> FileShareView:
|
||||||
|
storage_key = item.storage_key or ""
|
||||||
|
share = await shares.find_active_share(pool, item.account_id, storage_key)
|
||||||
|
if share is None:
|
||||||
|
chat = await chats.get_chat(pool, item.account_id, item.chat_id)
|
||||||
|
share = await shares.create_share(
|
||||||
|
pool,
|
||||||
|
item.account_id,
|
||||||
|
"media",
|
||||||
|
storage_key,
|
||||||
|
item.file_name or f"media_{item.id}",
|
||||||
|
mime=mime,
|
||||||
|
file_size=item.file_size,
|
||||||
|
title=chat.title if chat else None,
|
||||||
|
chat_id=item.chat_id,
|
||||||
|
message_id=item.message_id,
|
||||||
|
expires_at=expiry_from_seconds(LINK_TTL_SECONDS),
|
||||||
|
)
|
||||||
|
return share
|
||||||
|
|
||||||
|
|
||||||
|
def _is_text(mime: str) -> bool:
|
||||||
|
return mime.startswith("text/") or mime in _TEXT_MIMES
|
||||||
|
|
||||||
|
|
||||||
|
def inline_content(storage: ContentAddressedStorage, key: str, mime: str) -> list[Any]:
|
||||||
|
size = Path(storage.url(key)).stat().st_size
|
||||||
|
if mime in _INLINE_IMAGE_MIMES and size <= _INLINE_IMAGE_BYTES:
|
||||||
|
return [Image(data=storage.get(key), format=mime.rsplit("/", maxsplit=1)[-1])]
|
||||||
|
if _is_text(mime) and size <= _INLINE_TEXT_BYTES:
|
||||||
|
text = storage.get(key).decode("utf-8", errors="replace")
|
||||||
|
return [TextContent(type="text", text=text)]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def describe(item: MediaView, mime: str, share: FileShareView) -> str:
|
||||||
|
size = f"{item.file_size} bytes" if item.file_size else "size unknown"
|
||||||
|
expires = share.expires_at.isoformat() if share.expires_at else "never"
|
||||||
|
return (
|
||||||
|
f"{item.file_name} ({item.kind}, {mime}, {size})\n"
|
||||||
|
f"Download: {_origin()}/f/{share.token}/{share.url_name}\n"
|
||||||
|
f"Public link, expires {expires}; fetch it with curl/wget or any HTTP "
|
||||||
|
"client, no auth needed."
|
||||||
|
)
|
||||||
@@ -7,12 +7,19 @@ from fastmcp import FastMCP
|
|||||||
from fastmcp.exceptions import ToolError
|
from fastmcp.exceptions import ToolError
|
||||||
from fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext
|
from fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext
|
||||||
from fastmcp.tools.base import ToolResult
|
from fastmcp.tools.base import ToolResult
|
||||||
from mcp.types import CallToolRequestParams
|
from mcp.types import CallToolRequestParams, TextContent
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from api.mcp.download import (
|
||||||
|
MAX_WAIT_SECONDS,
|
||||||
|
describe,
|
||||||
|
fetch_media,
|
||||||
|
inline_content,
|
||||||
|
share_media,
|
||||||
|
)
|
||||||
from api.mcp.format import build_transcript, load_notes, load_photos, resolve_names
|
from api.mcp.format import build_transcript, load_notes, load_photos, resolve_names
|
||||||
from dependencies.container import container
|
from dependencies.container import container
|
||||||
from utils.jobs import enqueue
|
from utils.files import resolve_mime
|
||||||
from utils.read import annotations, chats, media, peers, presence, social, watches
|
from utils.read import annotations, chats, media, peers, presence, social, watches
|
||||||
from utils.read.accounts import self_user_id, unknown_account_hint
|
from utils.read.accounts import self_user_id, unknown_account_hint
|
||||||
from utils.read.models import DEFAULT_LIMIT, Page
|
from utils.read.models import DEFAULT_LIMIT, Page
|
||||||
@@ -44,7 +51,7 @@ Typical flows:
|
|||||||
`before_id` (first id you saw), to newer with `after_id` (last id you saw);
|
`before_id` (first id you saw), to newer with `after_id` (last id you saw);
|
||||||
begin a full forward walk with `after_id=0`. Each transcript line starts
|
begin a full forward walk with `after_id=0`. Each transcript line starts
|
||||||
with its `#message_id`, and photos show as `[photo #message_id]` — pass that
|
with its `#message_id`, and photos show as `[photo #message_id]` — pass that
|
||||||
id to `get_media` (with `fetch=True` to download) or to annotation tools.
|
id to `download_media` or to annotation tools.
|
||||||
Lines marked
|
Lines marked
|
||||||
"📝 [your private note, NOT in Telegram]" are the user's own annotations
|
"📝 [your private note, NOT in Telegram]" are the user's own annotations
|
||||||
attached locally via the web UI; they never existed in Telegram. Treat them
|
attached locally via the web UI; they never existed in Telegram. Treat them
|
||||||
@@ -53,8 +60,11 @@ Typical flows:
|
|||||||
transcripts; supports chat/sender/date filters and regex).
|
transcripts; supports chat/sender/date filters and regex).
|
||||||
- Forensics: `get_deleted_messages` and `get_message_versions` recover content
|
- Forensics: `get_deleted_messages` and `get_message_versions` recover content
|
||||||
removed or edited in Telegram but kept in the archive.
|
removed or edited in Telegram but kept in the archive.
|
||||||
- Media: `get_media` returns metadata; pass `fetch=True` to enqueue a lazy
|
- Files: `download_media` gets any message attachment (photo, video, voice,
|
||||||
download if the file isn't stored yet.
|
document, ...). It pulls the file from Telegram if the archive lacks it and
|
||||||
|
returns a public link valid 24h that needs no auth (`curl -O <link>`), plus
|
||||||
|
the content itself for small images and text files. `get_media` returns
|
||||||
|
metadata only.
|
||||||
- Monitoring: `set_watch` creates a local rule, `list_watches` lists rules, and
|
- Monitoring: `set_watch` creates a local rule, `list_watches` lists rules, and
|
||||||
`list_alerts` reads what those rules fired.
|
`list_alerts` reads what those rules fired.
|
||||||
|
|
||||||
@@ -63,7 +73,8 @@ Watched bots:
|
|||||||
own `account_id` and its chats, messages and media read through the same
|
own `account_id` and its chats, messages and media read through the same
|
||||||
tools as a user account. Nothing bot-specific to call.
|
tools as a user account. Nothing bot-specific to call.
|
||||||
|
|
||||||
Writes: everything is read-only except `set_watch`, the only allowed write.
|
Writes: everything is read-only except `set_watch` and the share links
|
||||||
|
`download_media` creates (visible and revocable in the web UI).
|
||||||
|
|
||||||
Paging: list/search tools accept `limit` and `offset`.
|
Paging: list/search tools accept `limit` and `offset`.
|
||||||
"""
|
"""
|
||||||
@@ -239,21 +250,40 @@ async def get_message_versions(
|
|||||||
|
|
||||||
@mcp.tool
|
@mcp.tool
|
||||||
async def get_media(
|
async def get_media(
|
||||||
account_id: int, chat_id: int, message_id: int, fetch: bool = False
|
account_id: int, chat_id: int, message_id: int
|
||||||
) -> dict[str, Any] | None:
|
) -> dict[str, Any] | None:
|
||||||
"""Get media metadata for a message; set fetch=True to enqueue lazy download."""
|
"""Get media metadata for a message; use download_media to get the file."""
|
||||||
|
item = await media.get_message_media(await _pool(), account_id, chat_id, message_id)
|
||||||
|
return item.model_dump(mode="json") if item else None
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool(output_schema=None)
|
||||||
|
async def download_media(
|
||||||
|
account_id: int, chat_id: int, message_id: int, wait_seconds: int = 45
|
||||||
|
) -> list[Any]:
|
||||||
|
"""Download the file attached to a message: any photo, video, voice, document.
|
||||||
|
|
||||||
|
Pulls it from Telegram first if the archive doesn't have it yet, waiting up
|
||||||
|
to `wait_seconds`. Returns a public download link (valid 24h, no auth) and
|
||||||
|
inlines small images and text files.
|
||||||
|
"""
|
||||||
pool = await _pool()
|
pool = await _pool()
|
||||||
item = await media.get_message_media(pool, account_id, chat_id, message_id)
|
wait = min(max(wait_seconds, 0), MAX_WAIT_SECONDS)
|
||||||
if item is None:
|
item = await fetch_media(pool, account_id, chat_id, message_id, wait)
|
||||||
return None
|
if item is None or item.storage_key is None:
|
||||||
if fetch and not item.downloaded:
|
return [
|
||||||
await enqueue(
|
TextContent(
|
||||||
pool,
|
type="text",
|
||||||
account_id,
|
text="Still downloading from Telegram; call download_media again.",
|
||||||
"fetch_media",
|
)
|
||||||
{"chat_id": chat_id, "message_id": message_id},
|
]
|
||||||
)
|
mime = resolve_mime(item.kind, item.mime, item.file_name)
|
||||||
return item.model_dump(mode="json")
|
share = await share_media(pool, item, mime)
|
||||||
|
storage = await container.get(ContentAddressedStorage)
|
||||||
|
return [
|
||||||
|
TextContent(type="text", text=describe(item, mime, share)),
|
||||||
|
*inline_content(storage, item.storage_key, mime),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool
|
@mcp.tool
|
||||||
|
|||||||
+23
-15
@@ -11,7 +11,7 @@
|
|||||||
"uqr": "^0.1.3",
|
"uqr": "^0.1.3",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.5.13",
|
"@biomejs/biome": "2.5.14",
|
||||||
"@sveltejs/adapter-auto": "^7.0.1",
|
"@sveltejs/adapter-auto": "^7.0.1",
|
||||||
"@sveltejs/adapter-static": "^3.0.10",
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
"@sveltejs/kit": "^2.57.0",
|
"@sveltejs/kit": "^2.57.0",
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"svelte-check": "^4.7.6",
|
"svelte-check": "^4.7.6",
|
||||||
"tailwindcss": "^4.2.2",
|
"tailwindcss": "^4.2.2",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"ultracite": "7.12.0",
|
"ultracite": "7.12.2",
|
||||||
"vite": "^8.0.7",
|
"vite": "^8.0.7",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -39,23 +39,23 @@
|
|||||||
|
|
||||||
"@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
"@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
||||||
|
|
||||||
"@biomejs/biome": ["@biomejs/biome@2.5.13", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.13", "@biomejs/cli-darwin-x64": "2.5.13", "@biomejs/cli-linux-arm64": "2.5.13", "@biomejs/cli-linux-arm64-musl": "2.5.13", "@biomejs/cli-linux-x64": "2.5.13", "@biomejs/cli-linux-x64-musl": "2.5.13", "@biomejs/cli-win32-arm64": "2.5.13", "@biomejs/cli-win32-x64": "2.5.13" }, "bin": { "biome": "bin/biome" } }, "sha512-+SEC/mFk1a+5mvUANZgbZTaiZXs1nj4iMhL/PHiqDT5TPUEPFIlliEtkKKZB4N862yylHC3UI+/Sj2I0HJEqhA=="],
|
"@biomejs/biome": ["@biomejs/biome@2.5.14", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.14", "@biomejs/cli-darwin-x64": "2.5.14", "@biomejs/cli-linux-arm64": "2.5.14", "@biomejs/cli-linux-arm64-musl": "2.5.14", "@biomejs/cli-linux-x64": "2.5.14", "@biomejs/cli-linux-x64-musl": "2.5.14", "@biomejs/cli-win32-arm64": "2.5.14", "@biomejs/cli-win32-x64": "2.5.14" }, "bin": { "biome": "bin/biome" } }, "sha512-0FabLIjd4M/dm8VFI86RMaLLdgepzbdfiAL2R8cr7V81OYYrP1w7Z73KfAwPK5h9SrEBXTzNG2y+mBwPo9xRnw=="],
|
||||||
|
|
||||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-nYSuDJ6zgVqZUkAkJkvhXxsF2PYIrUk/g638K4voCXz7foI9f7b6C2/7+oAihsHQlivZNMUrm/y8ywlcHQtZOw=="],
|
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UnzaXO65L4tsZimFITFP2M121GyhDcWFrT3pL5ZJ5U4XcS/0L5VHYytVohdCf/gnDUFHgl2I9xnt5bV/J1kxHQ=="],
|
||||||
|
|
||||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-KVy1ceEDuJ3AzFxjT9kkxbVy+UANw1pjEMUS6lvKfxjJ+fmkRvc7sQn1Xo6ETgseEI7wUQoV03KSga3XfE8YRQ=="],
|
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-kiy8qA16K93J7uvFfWi4LgjqDpnRKyePAna6A0Y4jxyUga35SYpIZ2cNWlhy0lsfgqRenCpfymnJWEZ6mgpRgA=="],
|
||||||
|
|
||||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-VlNMtoxOqs0dUR6drxxHr18SNUvI7xxAuHZlH4s/dstYSf3g6RaqVgo8JRnhcOhKz9afWrvtJTboNG1JuYlIDQ=="],
|
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-vO/9BaU1n30CiFNLx49gMTMtbCAAqlo/EqFoG0BU3deQInTbJrmXSmTQ9e3FoDZIKQnvSLDVNL4lx1ci7y1T1Q=="],
|
||||||
|
|
||||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-CH32xpep3dNS5EVJpAHlYchkBYznxyVZQrx0b6YYYlPL9u9ZeD2NtqiK/6s64+HFS2a7hGnryLlqqZAOh8ax5g=="],
|
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-SJ9PrZkBnnH9dHJDnxk34vKs0GB2dbsioaft6/hPhWJ7AHpwYH83Isnhj0FSRpFkGgpVfJ1lds23ApB2czUDLQ=="],
|
||||||
|
|
||||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.13", "", { "os": "linux", "cpu": "x64" }, "sha512-Fi6gIxbUaJ3ZCIXeG3ggIBPF76O2DjDN67WrPX/ODRv54GeXPm2gbEPC96Yb0yrJoiH0Eax1JLx1Pi0XbsNFZQ=="],
|
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.14", "", { "os": "linux", "cpu": "x64" }, "sha512-VHZRa7CCQBUWKxNwUvHJeuW03WFRgN5NWO//SDGOitc9QIeNyfA42V9zlKm+x82xFSG9Bzi5fi+hbhm9anO8yA=="],
|
||||||
|
|
||||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.13", "", { "os": "linux", "cpu": "x64" }, "sha512-F3pmwl+VHoUuVJN/tbNLKeLt0SVK3EIyN1jXlMcNyQGYRQQTVqXF+GgkP0/CjGXFN87e/mv0sBfUnWqWza5PKQ=="],
|
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.14", "", { "os": "linux", "cpu": "x64" }, "sha512-2kI5PrMgW5dcEZYrstLPmUmCwkUwZY39rP4BN93Vxbwcs2O57LDQOktUZZYPvvspN5i0mhGr3TJtF5sU26NUWg=="],
|
||||||
|
|
||||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-+WD13qshXrr0Icv4BfsAdzm8Fs3TL+nZ59zEQxsYNd8lcgZJ0A5+OrlwsL1PipVCmWeRpxgIPD6DAcEd7smkCQ=="],
|
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-pHgAFffmZtaYoxavEsWEYNvcA7NwOIjLyqw2HXyLbt16xYryX0L4fMV2u0G9ag6LNYPIoqWaWqzUcnc6rLGGXg=="],
|
||||||
|
|
||||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.13", "", { "os": "win32", "cpu": "x64" }, "sha512-VOofU/nW761XWzUeUNE8zzYNyPrxuMuNFMizL0qz7J75yeV8N7WFheUlk1/K6dOkaFpH9wJ+lDKlwjAbw0346w=="],
|
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.14", "", { "os": "win32", "cpu": "x64" }, "sha512-oJWmBhoHsnhUKIke+0gXDX0mltJrWHA1UyHsrTlXwX0TL64ilVZAo+TYZm97baecV0esBdpzy3k96q+09JaZUQ=="],
|
||||||
|
|
||||||
"@clack/core": ["@clack/core@1.5.1", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ=="],
|
"@clack/core": ["@clack/core@1.5.1", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ=="],
|
||||||
|
|
||||||
@@ -263,7 +263,7 @@
|
|||||||
|
|
||||||
"devalue": ["devalue@5.8.1", "", {}, "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw=="],
|
"devalue": ["devalue@5.8.1", "", {}, "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw=="],
|
||||||
|
|
||||||
"empathic": ["empathic@2.0.1", "", {}, "sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q=="],
|
"empathic": ["empathic@2.1.0", "", {}, "sha512-AnfC1ATldl49/cvZdLPDjBfrRNwbDO05aibiOtzQu3qtlbJtomNLhF30HEtn/7iBz50dlMECqATo3fG0LrdEgw=="],
|
||||||
|
|
||||||
"enhanced-resolve": ["enhanced-resolve@5.22.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww=="],
|
"enhanced-resolve": ["enhanced-resolve@5.22.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww=="],
|
||||||
|
|
||||||
@@ -445,7 +445,7 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
"string-width": ["string-width@8.2.2", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg=="],
|
"string-width": ["string-width@8.3.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ=="],
|
||||||
|
|
||||||
"strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
"strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
||||||
|
|
||||||
@@ -479,7 +479,7 @@
|
|||||||
|
|
||||||
"ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="],
|
"ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="],
|
||||||
|
|
||||||
"ultracite": ["ultracite@7.12.0", "", { "dependencies": { "@clack/prompts": "^1.7.0", "cli-truncate": "^6.1.1", "commander": "^15.0.0", "deepmerge": "^4.3.1", "empathic": "^2.0.1", "execa": "^10.0.1", "fast-glob": "^3.3.3", "find-workspaces": "^0.3.1", "jsonc-parser": "^3.3.1", "log-update": "^8.0.0", "magicast": "^0.5.4", "nypm": "^0.6.9", "resolve.exports": "^2.0.3", "semver": "^7.8.5", "string-width": "^8.2.2", "yaml": "^2.9.0", "zod": "^4.5.4" }, "peerDependencies": { "@biomejs/biome": "^2.5.0", "eslint": "^10.0.0", "oxfmt": ">=0.40.0", "oxlint": "^1.79.0", "prettier": "^3.0.0", "stylelint": "^17.0.0" }, "optionalPeers": ["@biomejs/biome", "eslint", "oxfmt", "oxlint", "prettier", "stylelint"], "bin": { "ultracite": "dist/index.js" } }, "sha512-thOrO9IMaEcGjQrwp44W5UNm9muLIY/vVD83hVPhQ5wZncSAQB+qWq6U78QgVAoWaD3mDpkZDvfwF+JLSQtZzg=="],
|
"ultracite": ["ultracite@7.12.2", "", { "dependencies": { "@clack/prompts": "^1.8.1", "cli-truncate": "^6.1.1", "commander": "^15.0.0", "deepmerge": "^4.3.1", "empathic": "^2.1.0", "execa": "^10.0.1", "fast-glob": "^3.3.3", "find-workspaces": "^0.3.1", "jsonc-parser": "^3.3.1", "log-update": "^8.0.0", "magicast": "^0.5.5", "nypm": "^0.6.10", "resolve.exports": "^2.0.3", "semver": "^7.8.5", "string-width": "^8.3.0", "yaml": "^2.9.1", "zod": "^4.6.5" }, "peerDependencies": { "@biomejs/biome": "^2.5.0", "eslint": "^10.0.0", "oxfmt": ">=0.59.0", "oxlint": "^1.82.0", "prettier": "^3.0.0", "stylelint": "^17.0.0" }, "optionalPeers": ["@biomejs/biome", "eslint", "oxfmt", "oxlint", "prettier", "stylelint"], "bin": { "ultracite": "dist/index.js" } }, "sha512-0FQE7EuVFnX1FjtlIBklkfBTiZ5bJxknXSxcsjKos5Y73FlkasubWH7uVLS8jgOAI204B7xaIq+buE3vUybuwQ=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
|
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
|
||||||
|
|
||||||
@@ -497,7 +497,7 @@
|
|||||||
|
|
||||||
"wrap-ansi": ["wrap-ansi@10.0.1", "", { "dependencies": { "ansi-styles": "^6.2.3", "string-width": "^8.2.0" } }, "sha512-M0N4xzyzosiIok3svYlEo1sdLZts/8FPgYH/GPC3wvlmPoRvnoManGMrE54waYj3tISA8w6lsdesfVv67qSr8Q=="],
|
"wrap-ansi": ["wrap-ansi@10.0.1", "", { "dependencies": { "ansi-styles": "^6.2.3", "string-width": "^8.2.0" } }, "sha512-M0N4xzyzosiIok3svYlEo1sdLZts/8FPgYH/GPC3wvlmPoRvnoManGMrE54waYj3tISA8w6lsdesfVv67qSr8Q=="],
|
||||||
|
|
||||||
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
|
"yaml": ["yaml@2.9.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw=="],
|
||||||
|
|
||||||
"yoctocolors": ["yoctocolors@2.2.0", "", {}, "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg=="],
|
"yoctocolors": ["yoctocolors@2.2.0", "", {}, "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg=="],
|
||||||
|
|
||||||
@@ -517,10 +517,18 @@
|
|||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"cli-truncate/string-width": ["string-width@8.2.2", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg=="],
|
||||||
|
|
||||||
|
"find-workspaces/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
|
||||||
|
|
||||||
|
"log-update/string-width": ["string-width@8.2.2", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg=="],
|
||||||
|
|
||||||
"micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
"micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||||
|
|
||||||
"svelte-check/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
"svelte-check/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||||
|
|
||||||
|
"wrap-ansi/string-width": ["string-width@8.2.2", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg=="],
|
||||||
|
|
||||||
"svelte-check/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
"svelte-check/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,8 @@ services:
|
|||||||
caddy:
|
caddy:
|
||||||
aliases:
|
aliases:
|
||||||
- beavergram-frontend
|
- beavergram-frontend
|
||||||
|
# ports:
|
||||||
|
# - "127.0.0.1:3000:3000"
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
caddy:
|
caddy:
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"uqr": "^0.1.3"
|
"uqr": "^0.1.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.5.13",
|
"@biomejs/biome": "2.5.14",
|
||||||
"@sveltejs/adapter-auto": "^7.0.1",
|
"@sveltejs/adapter-auto": "^7.0.1",
|
||||||
"@sveltejs/adapter-static": "^3.0.10",
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
"@sveltejs/kit": "^2.57.0",
|
"@sveltejs/kit": "^2.57.0",
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
"svelte-check": "^4.7.6",
|
"svelte-check": "^4.7.6",
|
||||||
"tailwindcss": "^4.2.2",
|
"tailwindcss": "^4.2.2",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"ultracite": "7.12.0",
|
"ultracite": "7.12.2",
|
||||||
"vite": "^8.0.7"
|
"vite": "^8.0.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user