Compare commits
10 Commits
7a7393b8c1
...
d0c163962e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0c163962e | ||
|
|
81ee845677 | ||
|
|
fb0ee51183 | ||
|
|
7c346d570b | ||
|
|
59cdb40974 | ||
|
|
983b9541a7 | ||
|
|
f4f1d98655 | ||
|
|
228b9d74c4 | ||
|
|
9af2d2625f | ||
|
|
e451901ea1 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -54,6 +54,9 @@ data/telegram-mtproto/
|
|||||||
lettabot.yaml
|
lettabot.yaml
|
||||||
lettabot.yml
|
lettabot.yml
|
||||||
|
|
||||||
|
# Deployment-specific model list (upstream has its own defaults)
|
||||||
|
src/models.json
|
||||||
|
|
||||||
# Platform-specific deploy configs (generated by fly launch, etc.)
|
# Platform-specific deploy configs (generated by fly launch, etc.)
|
||||||
fly.toml
|
fly.toml
|
||||||
bun.lock
|
bun.lock
|
||||||
|
|||||||
179
memfs-server.py
Executable file
179
memfs-server.py
Executable file
@@ -0,0 +1,179 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Local git HTTP sidecar for Letta memfs on self-hosted deployments.
|
||||||
|
|
||||||
|
Implements git smart HTTP protocol via git-http-backend, storing bare repos under
|
||||||
|
~/.letta/memfs/repository/{org_id}/{agent_id}/repo.git/ — the exact structure that
|
||||||
|
Letta's LocalStorageBackend reads from.
|
||||||
|
|
||||||
|
The Letta server proxies /v1/git/* requests here when LETTA_MEMFS_SERVICE_URL is set.
|
||||||
|
letta-code pushes → Letta server proxies → here → bare repo → LocalStorageBackend reads.
|
||||||
|
|
||||||
|
Run as a systemd user service. Listens on 127.0.0.1:8285.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
LISTEN_HOST = "0.0.0.0"
|
||||||
|
LISTEN_PORT = 8285
|
||||||
|
REPO_BASE = Path.home() / ".letta" / "memfs" / "repository"
|
||||||
|
|
||||||
|
# /git/{agent_id}/state.git/{rest}
|
||||||
|
_PATH_RE = re.compile(r"^/git/([^/]+)/state\.git(/.*)?$")
|
||||||
|
|
||||||
|
|
||||||
|
def _repo_path(org_id: str, agent_id: str) -> Path:
|
||||||
|
return REPO_BASE / org_id / agent_id / "repo.git"
|
||||||
|
|
||||||
|
|
||||||
|
def _fix_repo_config(repo: Path) -> None:
|
||||||
|
"""Ensure repo is pushable (Corykidios fix-repo-configs pattern)."""
|
||||||
|
subprocess.run(["git", "config", "--file", str(repo / "config"), "core.bare", "true"], capture_output=True)
|
||||||
|
subprocess.run(["git", "config", "--file", str(repo / "config"), "receive.denyCurrentBranch", "ignore"], capture_output=True)
|
||||||
|
subprocess.run(["git", "config", "--file", str(repo / "config"), "http.receivepack", "true"], capture_output=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_repo(repo: Path, agent_id: str) -> Path:
|
||||||
|
"""Ensure repo exists, searching all org dirs if needed (Corykidios pattern)."""
|
||||||
|
# If repo exists at expected path, use it
|
||||||
|
if repo.exists():
|
||||||
|
_fix_repo_config(repo)
|
||||||
|
return repo
|
||||||
|
|
||||||
|
# Search all org directories for existing agent repo
|
||||||
|
if REPO_BASE.exists():
|
||||||
|
for org_dir in REPO_BASE.iterdir():
|
||||||
|
if org_dir.is_dir():
|
||||||
|
candidate = org_dir / agent_id / "repo.git"
|
||||||
|
if candidate.exists():
|
||||||
|
print(f"[memfs] Found existing repo at {candidate}", file=sys.stderr, flush=True)
|
||||||
|
_fix_repo_config(candidate)
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
# Create new repo at expected path
|
||||||
|
repo.mkdir(parents=True, exist_ok=True)
|
||||||
|
subprocess.run(["git", "init", "--bare", str(repo)], check=True, capture_output=True)
|
||||||
|
with open(repo / "config", "a") as f:
|
||||||
|
f.write("\n[http]\n\treceivepack = true\n")
|
||||||
|
print(f"[memfs] Created new repo at {repo}", file=sys.stderr, flush=True)
|
||||||
|
return repo
|
||||||
|
|
||||||
|
|
||||||
|
def _read_chunked(rfile) -> bytes:
|
||||||
|
"""Read and decode HTTP chunked transfer encoding from a socket file."""
|
||||||
|
body = bytearray()
|
||||||
|
while True:
|
||||||
|
line = rfile.readline().strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
size = int(line, 16)
|
||||||
|
if size == 0:
|
||||||
|
rfile.readline() # trailing CRLF after terminal chunk
|
||||||
|
break
|
||||||
|
body.extend(rfile.read(size))
|
||||||
|
rfile.readline() # trailing CRLF after chunk data
|
||||||
|
return bytes(body)
|
||||||
|
|
||||||
|
|
||||||
|
class GitHandler(BaseHTTPRequestHandler):
|
||||||
|
def log_message(self, fmt, *args):
|
||||||
|
print(f"[memfs] {self.address_string()} {fmt % args}", file=sys.stderr, flush=True)
|
||||||
|
|
||||||
|
def _handle(self):
|
||||||
|
raw_path = self.path.split("?")[0]
|
||||||
|
m = _PATH_RE.match(raw_path)
|
||||||
|
if not m:
|
||||||
|
self.send_error(404, "Not a valid memfs path")
|
||||||
|
return
|
||||||
|
|
||||||
|
agent_id = m.group(1)
|
||||||
|
git_suffix = m.group(2) or "/"
|
||||||
|
org_id = self.headers.get("X-Organization-Id") or "default"
|
||||||
|
query = self.path.split("?", 1)[1] if "?" in self.path else ""
|
||||||
|
|
||||||
|
repo = _repo_path(org_id, agent_id)
|
||||||
|
repo = _ensure_repo(repo, agent_id) # Pass agent_id for search
|
||||||
|
|
||||||
|
# Read request body — handle both Content-Length and chunked
|
||||||
|
te = self.headers.get("Transfer-Encoding", "")
|
||||||
|
cl = self.headers.get("Content-Length")
|
||||||
|
if cl:
|
||||||
|
body = self.rfile.read(int(cl))
|
||||||
|
elif "chunked" in te.lower():
|
||||||
|
body = _read_chunked(self.rfile)
|
||||||
|
else:
|
||||||
|
body = b""
|
||||||
|
|
||||||
|
env = {
|
||||||
|
**os.environ,
|
||||||
|
"GIT_PROJECT_ROOT": str(repo.parent),
|
||||||
|
"PATH_INFO": "/repo.git" + git_suffix,
|
||||||
|
"REQUEST_METHOD": self.command,
|
||||||
|
"QUERY_STRING": query,
|
||||||
|
"CONTENT_TYPE": self.headers.get("Content-Type", ""),
|
||||||
|
"CONTENT_LENGTH": str(len(body)),
|
||||||
|
"GIT_HTTP_EXPORT_ALL": "1",
|
||||||
|
}
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "http-backend"],
|
||||||
|
input=body,
|
||||||
|
capture_output=True,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"[memfs] git-http-backend error: {result.stderr.decode()}", file=sys.stderr)
|
||||||
|
self.send_error(500, "git-http-backend failed")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Parse CGI output (headers + body)
|
||||||
|
raw = result.stdout
|
||||||
|
header_end = raw.find(b"\r\n\r\n")
|
||||||
|
if header_end == -1:
|
||||||
|
header_end = raw.find(b"\n\n")
|
||||||
|
if header_end == -1:
|
||||||
|
self.send_error(502, "Invalid CGI response")
|
||||||
|
return
|
||||||
|
|
||||||
|
header_block = raw[:header_end].decode("utf-8", errors="replace")
|
||||||
|
body_out = raw[header_end + 4 if raw[header_end:header_end+4] == b"\r\n\r\n" else header_end + 2:]
|
||||||
|
|
||||||
|
status = 200
|
||||||
|
headers = []
|
||||||
|
for line in header_block.splitlines():
|
||||||
|
if ":" in line:
|
||||||
|
k, _, v = line.partition(":")
|
||||||
|
k, v = k.strip(), v.strip()
|
||||||
|
if k.lower() == "status":
|
||||||
|
try:
|
||||||
|
status = int(v.split()[0])
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
headers.append((k, v))
|
||||||
|
|
||||||
|
self.send_response(status)
|
||||||
|
for k, v in headers:
|
||||||
|
self.send_header(k, v)
|
||||||
|
self.send_header("Content-Length", str(len(body_out)))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(body_out)
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
self._handle()
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
self._handle()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
REPO_BASE.mkdir(parents=True, exist_ok=True)
|
||||||
|
print(f"[memfs] Starting on http://{LISTEN_HOST}:{LISTEN_PORT}", flush=True)
|
||||||
|
print(f"[memfs] Repo base: {REPO_BASE}", flush=True)
|
||||||
|
ThreadingHTTPServer((LISTEN_HOST, LISTEN_PORT), GitHandler).serve_forever()
|
||||||
668
package-lock.json
generated
668
package-lock.json
generated
@@ -13,7 +13,7 @@
|
|||||||
"@clack/prompts": "^0.11.0",
|
"@clack/prompts": "^0.11.0",
|
||||||
"@hapi/boom": "^10.0.1",
|
"@hapi/boom": "^10.0.1",
|
||||||
"@letta-ai/letta-client": "^1.7.12",
|
"@letta-ai/letta-client": "^1.7.12",
|
||||||
"@letta-ai/letta-code-sdk": "^0.1.11",
|
"@letta-ai/letta-code-sdk": "^0.1.14",
|
||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
"@types/node": "^25.0.10",
|
"@types/node": "^25.0.10",
|
||||||
"@types/node-schedule": "^2.1.8",
|
"@types/node-schedule": "^2.1.8",
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
"openai": "^6.17.0",
|
"openai": "^6.17.0",
|
||||||
"pino": "^10.3.1",
|
"pino": "^10.3.1",
|
||||||
"qrcode-terminal": "^0.12.0",
|
"qrcode-terminal": "^0.12.0",
|
||||||
"sharp": "^0.33.5",
|
"sharp": "^0.34.1",
|
||||||
"telegramify-markdown": "^1.0.0",
|
"telegramify-markdown": "^1.0.0",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
@@ -867,9 +867,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-darwin-arm64": {
|
"node_modules/@img/sharp-darwin-arm64": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
|
||||||
"integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
|
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -885,13 +885,13 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@img/sharp-libvips-darwin-arm64": "1.0.4"
|
"@img/sharp-libvips-darwin-arm64": "1.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-darwin-x64": {
|
"node_modules/@img/sharp-darwin-x64": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
|
||||||
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
|
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -907,13 +907,13 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@img/sharp-libvips-darwin-x64": "1.0.4"
|
"@img/sharp-libvips-darwin-x64": "1.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||||
"version": "1.0.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
|
||||||
"integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
|
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -927,9 +927,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||||
"version": "1.0.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
|
||||||
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
|
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -943,9 +943,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||||
"version": "1.0.5",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
|
||||||
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
|
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -959,9 +959,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||||
"version": "1.0.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
|
||||||
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
|
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1007,9 +1007,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||||
"version": "1.0.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
|
||||||
"integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
|
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@@ -1023,9 +1023,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||||
"version": "1.0.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
|
||||||
"integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
|
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1039,9 +1039,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||||
"version": "1.0.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
|
||||||
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
|
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1055,9 +1055,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||||
"version": "1.0.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
|
||||||
"integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
|
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1071,9 +1071,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-linux-arm": {
|
"node_modules/@img/sharp-linux-arm": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
|
||||||
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
|
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -1089,13 +1089,13 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@img/sharp-libvips-linux-arm": "1.0.5"
|
"@img/sharp-libvips-linux-arm": "1.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-linux-arm64": {
|
"node_modules/@img/sharp-linux-arm64": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
|
||||||
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
|
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1111,7 +1111,7 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@img/sharp-libvips-linux-arm64": "1.0.4"
|
"@img/sharp-libvips-linux-arm64": "1.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-linux-ppc64": {
|
"node_modules/@img/sharp-linux-ppc64": {
|
||||||
@@ -1159,9 +1159,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-linux-s390x": {
|
"node_modules/@img/sharp-linux-s390x": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
|
||||||
"integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
|
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@@ -1177,13 +1177,13 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@img/sharp-libvips-linux-s390x": "1.0.4"
|
"@img/sharp-libvips-linux-s390x": "1.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-linux-x64": {
|
"node_modules/@img/sharp-linux-x64": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
|
||||||
"integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
|
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1199,13 +1199,13 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@img/sharp-libvips-linux-x64": "1.0.4"
|
"@img/sharp-libvips-linux-x64": "1.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
|
||||||
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
|
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1221,13 +1221,13 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
|
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
|
||||||
"integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
|
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1243,20 +1243,20 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@img/sharp-libvips-linuxmusl-x64": "1.0.4"
|
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-wasm32": {
|
"node_modules/@img/sharp-wasm32": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
|
||||||
"integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
|
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"wasm32"
|
"wasm32"
|
||||||
],
|
],
|
||||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emnapi/runtime": "^1.2.0"
|
"@emnapi/runtime": "^1.7.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
@@ -1285,9 +1285,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-win32-ia32": {
|
"node_modules/@img/sharp-win32-ia32": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
|
||||||
"integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
|
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -1304,9 +1304,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-win32-x64": {
|
"node_modules/@img/sharp-win32-x64": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
|
||||||
"integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
|
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1376,9 +1376,9 @@
|
|||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@letta-ai/letta-code": {
|
"node_modules/@letta-ai/letta-code": {
|
||||||
"version": "0.18.2",
|
"version": "0.19.5",
|
||||||
"resolved": "https://registry.npmjs.org/@letta-ai/letta-code/-/letta-code-0.18.2.tgz",
|
"resolved": "https://registry.npmjs.org/@letta-ai/letta-code/-/letta-code-0.19.5.tgz",
|
||||||
"integrity": "sha512-HzNqMjBUiAq5IyZ8DSSWBHq/ahkd4RRYfO/V9eXMBZRTRpLb7Dae2hwvicE+aRSLmJqMdxpH6WI7+ZHKlFsILQ==",
|
"integrity": "sha512-INEDS79dkzJoQyL3IJRof+HNob3GZXgAge/JdJRFaVfJhU/o/6aTPcPWpQwxygE5ExIDSUlL85OlZ3CcBv0TyA==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1387,6 +1387,7 @@
|
|||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"ink-link": "^5.0.0",
|
"ink-link": "^5.0.0",
|
||||||
"lowlight": "^3.3.0",
|
"lowlight": "^3.3.0",
|
||||||
|
"node-pty": "^1.1.0",
|
||||||
"open": "^10.2.0",
|
"open": "^10.2.0",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"ws": "^8.19.0"
|
"ws": "^8.19.0"
|
||||||
@@ -1402,373 +1403,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@letta-ai/letta-code-sdk": {
|
"node_modules/@letta-ai/letta-code-sdk": {
|
||||||
"version": "0.1.11",
|
"version": "0.1.14",
|
||||||
"resolved": "https://registry.npmjs.org/@letta-ai/letta-code-sdk/-/letta-code-sdk-0.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/@letta-ai/letta-code-sdk/-/letta-code-sdk-0.1.14.tgz",
|
||||||
"integrity": "sha512-P1ueLWQuCnERizrvU3fZ9/rrMAJSIT+2j2/xxptqxMOKUuUrDmvAix1/eyDXqAwZkBVGImyqLGm4zqwNVNA7Dg==",
|
"integrity": "sha512-rSMp7kYwRZ4PAe3jET+PETFesuYCbeodEp6Qf7a5rLu97epqs+zNegSR+UUgq6c9+c5eqbuo+BsRThTKiSNJkA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@letta-ai/letta-code": "0.18.2"
|
"@letta-ai/letta-code": "0.19.5"
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@letta-ai/letta-code/node_modules/@img/sharp-darwin-arm64": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-darwin-arm64": "1.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@letta-ai/letta-code/node_modules/@img/sharp-darwin-x64": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-darwin-x64": "1.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@letta-ai/letta-code/node_modules/@img/sharp-libvips-darwin-arm64": {
|
|
||||||
"version": "1.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
|
|
||||||
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@letta-ai/letta-code/node_modules/@img/sharp-libvips-darwin-x64": {
|
|
||||||
"version": "1.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
|
|
||||||
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@letta-ai/letta-code/node_modules/@img/sharp-libvips-linux-arm": {
|
|
||||||
"version": "1.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
|
|
||||||
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@letta-ai/letta-code/node_modules/@img/sharp-libvips-linux-arm64": {
|
|
||||||
"version": "1.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
|
|
||||||
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@letta-ai/letta-code/node_modules/@img/sharp-libvips-linux-s390x": {
|
|
||||||
"version": "1.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
|
|
||||||
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
|
|
||||||
"cpu": [
|
|
||||||
"s390x"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@letta-ai/letta-code/node_modules/@img/sharp-libvips-linux-x64": {
|
|
||||||
"version": "1.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
|
|
||||||
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@letta-ai/letta-code/node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
|
||||||
"version": "1.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
|
|
||||||
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@letta-ai/letta-code/node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
|
||||||
"version": "1.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
|
|
||||||
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@letta-ai/letta-code/node_modules/@img/sharp-linux-arm": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-linux-arm": "1.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@letta-ai/letta-code/node_modules/@img/sharp-linux-arm64": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-linux-arm64": "1.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@letta-ai/letta-code/node_modules/@img/sharp-linux-s390x": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
|
|
||||||
"cpu": [
|
|
||||||
"s390x"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-linux-s390x": "1.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@letta-ai/letta-code/node_modules/@img/sharp-linux-x64": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-linux-x64": "1.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@letta-ai/letta-code/node_modules/@img/sharp-linuxmusl-arm64": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@letta-ai/letta-code/node_modules/@img/sharp-linuxmusl-x64": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@letta-ai/letta-code/node_modules/@img/sharp-wasm32": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
|
|
||||||
"cpu": [
|
|
||||||
"wasm32"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@emnapi/runtime": "^1.7.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@letta-ai/letta-code/node_modules/@img/sharp-win32-ia32": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
|
|
||||||
"cpu": [
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@letta-ai/letta-code/node_modules/@img/sharp-win32-x64": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@letta-ai/letta-code/node_modules/balanced-match": {
|
"node_modules/@letta-ai/letta-code/node_modules/balanced-match": {
|
||||||
@@ -1867,50 +1507,6 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@letta-ai/letta-code/node_modules/sharp": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@img/colour": "^1.0.0",
|
|
||||||
"detect-libc": "^2.1.2",
|
|
||||||
"semver": "^7.7.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-darwin-arm64": "0.34.5",
|
|
||||||
"@img/sharp-darwin-x64": "0.34.5",
|
|
||||||
"@img/sharp-libvips-darwin-arm64": "1.2.4",
|
|
||||||
"@img/sharp-libvips-darwin-x64": "1.2.4",
|
|
||||||
"@img/sharp-libvips-linux-arm": "1.2.4",
|
|
||||||
"@img/sharp-libvips-linux-arm64": "1.2.4",
|
|
||||||
"@img/sharp-libvips-linux-ppc64": "1.2.4",
|
|
||||||
"@img/sharp-libvips-linux-riscv64": "1.2.4",
|
|
||||||
"@img/sharp-libvips-linux-s390x": "1.2.4",
|
|
||||||
"@img/sharp-libvips-linux-x64": "1.2.4",
|
|
||||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
|
|
||||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
|
|
||||||
"@img/sharp-linux-arm": "0.34.5",
|
|
||||||
"@img/sharp-linux-arm64": "0.34.5",
|
|
||||||
"@img/sharp-linux-ppc64": "0.34.5",
|
|
||||||
"@img/sharp-linux-riscv64": "0.34.5",
|
|
||||||
"@img/sharp-linux-s390x": "0.34.5",
|
|
||||||
"@img/sharp-linux-x64": "0.34.5",
|
|
||||||
"@img/sharp-linuxmusl-arm64": "0.34.5",
|
|
||||||
"@img/sharp-linuxmusl-x64": "0.34.5",
|
|
||||||
"@img/sharp-wasm32": "0.34.5",
|
|
||||||
"@img/sharp-win32-arm64": "0.34.5",
|
|
||||||
"@img/sharp-win32-ia32": "0.34.5",
|
|
||||||
"@img/sharp-win32-x64": "0.34.5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@letta-ai/letta-code/node_modules/wsl-utils": {
|
"node_modules/@letta-ai/letta-code/node_modules/wsl-utils": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz",
|
||||||
@@ -4220,19 +3816,6 @@
|
|||||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/color": {
|
|
||||||
"version": "4.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
|
||||||
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"color-convert": "^2.0.1",
|
|
||||||
"color-string": "^1.9.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.5.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@@ -4251,16 +3834,6 @@
|
|||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/color-string": {
|
|
||||||
"version": "1.9.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
|
|
||||||
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"color-name": "^1.0.0",
|
|
||||||
"simple-swizzle": "^0.2.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/color-support": {
|
"node_modules/color-support": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
|
||||||
@@ -6097,9 +5670,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ink/node_modules/type-fest": {
|
"node_modules/ink/node_modules/type-fest": {
|
||||||
"version": "5.4.4",
|
"version": "5.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.5.0.tgz",
|
||||||
"integrity": "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==",
|
"integrity": "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==",
|
||||||
"license": "(MIT OR CC0-1.0)",
|
"license": "(MIT OR CC0-1.0)",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -6207,12 +5780,6 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-arrayish": {
|
|
||||||
"version": "0.3.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
|
|
||||||
"integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/is-buffer": {
|
"node_modules/is-buffer": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
|
||||||
@@ -8130,8 +7697,7 @@
|
|||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/node-domexception": {
|
"node_modules/node-domexception": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@@ -8273,6 +7839,16 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-pty": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"node-addon-api": "^7.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-schedule": {
|
"node_modules/node-schedule": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-2.1.1.tgz",
|
||||||
@@ -9479,15 +9055,15 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/sharp": {
|
"node_modules/sharp": {
|
||||||
"version": "0.33.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
||||||
"integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
|
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"color": "^4.2.3",
|
"@img/colour": "^1.0.0",
|
||||||
"detect-libc": "^2.0.3",
|
"detect-libc": "^2.1.2",
|
||||||
"semver": "^7.6.3"
|
"semver": "^7.7.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
@@ -9496,25 +9072,30 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@img/sharp-darwin-arm64": "0.33.5",
|
"@img/sharp-darwin-arm64": "0.34.5",
|
||||||
"@img/sharp-darwin-x64": "0.33.5",
|
"@img/sharp-darwin-x64": "0.34.5",
|
||||||
"@img/sharp-libvips-darwin-arm64": "1.0.4",
|
"@img/sharp-libvips-darwin-arm64": "1.2.4",
|
||||||
"@img/sharp-libvips-darwin-x64": "1.0.4",
|
"@img/sharp-libvips-darwin-x64": "1.2.4",
|
||||||
"@img/sharp-libvips-linux-arm": "1.0.5",
|
"@img/sharp-libvips-linux-arm": "1.2.4",
|
||||||
"@img/sharp-libvips-linux-arm64": "1.0.4",
|
"@img/sharp-libvips-linux-arm64": "1.2.4",
|
||||||
"@img/sharp-libvips-linux-s390x": "1.0.4",
|
"@img/sharp-libvips-linux-ppc64": "1.2.4",
|
||||||
"@img/sharp-libvips-linux-x64": "1.0.4",
|
"@img/sharp-libvips-linux-riscv64": "1.2.4",
|
||||||
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4",
|
"@img/sharp-libvips-linux-s390x": "1.2.4",
|
||||||
"@img/sharp-libvips-linuxmusl-x64": "1.0.4",
|
"@img/sharp-libvips-linux-x64": "1.2.4",
|
||||||
"@img/sharp-linux-arm": "0.33.5",
|
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
|
||||||
"@img/sharp-linux-arm64": "0.33.5",
|
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
|
||||||
"@img/sharp-linux-s390x": "0.33.5",
|
"@img/sharp-linux-arm": "0.34.5",
|
||||||
"@img/sharp-linux-x64": "0.33.5",
|
"@img/sharp-linux-arm64": "0.34.5",
|
||||||
"@img/sharp-linuxmusl-arm64": "0.33.5",
|
"@img/sharp-linux-ppc64": "0.34.5",
|
||||||
"@img/sharp-linuxmusl-x64": "0.33.5",
|
"@img/sharp-linux-riscv64": "0.34.5",
|
||||||
"@img/sharp-wasm32": "0.33.5",
|
"@img/sharp-linux-s390x": "0.34.5",
|
||||||
"@img/sharp-win32-ia32": "0.33.5",
|
"@img/sharp-linux-x64": "0.34.5",
|
||||||
"@img/sharp-win32-x64": "0.33.5"
|
"@img/sharp-linuxmusl-arm64": "0.34.5",
|
||||||
|
"@img/sharp-linuxmusl-x64": "0.34.5",
|
||||||
|
"@img/sharp-wasm32": "0.34.5",
|
||||||
|
"@img/sharp-win32-arm64": "0.34.5",
|
||||||
|
"@img/sharp-win32-ia32": "0.34.5",
|
||||||
|
"@img/sharp-win32-x64": "0.34.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
@@ -9676,15 +9257,6 @@
|
|||||||
"simple-concat": "^1.0.0"
|
"simple-concat": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/simple-swizzle": {
|
|
||||||
"version": "0.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
|
|
||||||
"integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"is-arrayish": "^0.3.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sisteransi": {
|
"node_modules/sisteransi": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
"@clack/prompts": "^0.11.0",
|
"@clack/prompts": "^0.11.0",
|
||||||
"@hapi/boom": "^10.0.1",
|
"@hapi/boom": "^10.0.1",
|
||||||
"@letta-ai/letta-client": "^1.7.12",
|
"@letta-ai/letta-client": "^1.7.12",
|
||||||
"@letta-ai/letta-code-sdk": "^0.1.11",
|
"@letta-ai/letta-code-sdk": "^0.1.14",
|
||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
"@types/node": "^25.0.10",
|
"@types/node": "^25.0.10",
|
||||||
"@types/node-schedule": "^2.1.8",
|
"@types/node-schedule": "^2.1.8",
|
||||||
|
|||||||
@@ -734,6 +734,183 @@ export function createApiServer(deliverer: AgentRouter, options: ServerOptions):
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Route: GET /api/v1/bridge-status - Rich status for ani.wiuf.net dashboard
|
||||||
|
// Returns: Aster health (ledger file timestamps), conversations, adapter list, recent errors.
|
||||||
|
// No auth required — dashboard is internal-network only (10.10.20.x).
|
||||||
|
if (req.url === '/api/v1/bridge-status' && req.method === 'GET') {
|
||||||
|
try {
|
||||||
|
const { stat } = await import('node:fs/promises');
|
||||||
|
const home = process.env.HOME || '/home/ani';
|
||||||
|
const asterAgentId = process.env.CONSCIENCE_AGENT_ID || null;
|
||||||
|
const asterConvId = process.env.CONSCIENCE_CONVERSATION_ID || null;
|
||||||
|
const memfsBase = `${home}/.letta/agents/${asterAgentId}/memory/aster`;
|
||||||
|
|
||||||
|
const fileMtime = async (path: string): Promise<string | null> => {
|
||||||
|
try { return (await stat(path)).mtime.toISOString(); } catch { return null; }
|
||||||
|
};
|
||||||
|
|
||||||
|
const [commitments, driftLog, patterns, assumptions] = await Promise.all([
|
||||||
|
fileMtime(`${memfsBase}/ledger/commitments.md`),
|
||||||
|
fileMtime(`${memfsBase}/ledger/drift_log.md`),
|
||||||
|
fileMtime(`${memfsBase}/ledger/patterns.md`),
|
||||||
|
fileMtime(`${memfsBase}/ledger/assumptions.md`),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Aster is "healthy" if she wrote to any ledger file in the last 2 hours
|
||||||
|
const recentCutoff = Date.now() - 2 * 60 * 60 * 1000;
|
||||||
|
const lastWriteStr = [commitments, driftLog, patterns, assumptions]
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort()
|
||||||
|
.reverse()[0] ?? null;
|
||||||
|
const lastWriteMs = lastWriteStr ? new Date(lastWriteStr).getTime() : 0;
|
||||||
|
const asterHealthy = lastWriteMs > recentCutoff;
|
||||||
|
|
||||||
|
// Conversations from store
|
||||||
|
const conversations: any[] = [];
|
||||||
|
if (options.stores) {
|
||||||
|
for (const [_name, store] of options.stores) {
|
||||||
|
const info = store.getInfo();
|
||||||
|
for (const [key, convId] of Object.entries(info.conversations || {})) {
|
||||||
|
const [channel, roomId] = key.startsWith('matrix:')
|
||||||
|
? ['matrix', key.replace('matrix:', '')]
|
||||||
|
: key.split(':').length > 1
|
||||||
|
? [key.split(':')[0], key.split(':').slice(1).join(':')]
|
||||||
|
: ['unknown', key];
|
||||||
|
conversations.push({ key, conversationId: convId, channel, roomId, lastMessage: null });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last heartbeat: read tail of cron-log.jsonl for most recent heartbeat_completed event
|
||||||
|
let lastHeartbeat: string | null = null;
|
||||||
|
try {
|
||||||
|
const { open } = await import('node:fs/promises');
|
||||||
|
const workingDir = process.env.WORKING_DIR || home;
|
||||||
|
const cronLogPath = `${workingDir}/cron-log.jsonl`;
|
||||||
|
const fh = await open(cronLogPath, 'r');
|
||||||
|
try {
|
||||||
|
const { size } = await fh.stat();
|
||||||
|
const tailSize = Math.min(size, 8192);
|
||||||
|
const buf = Buffer.alloc(tailSize);
|
||||||
|
await fh.read(buf, 0, tailSize, size - tailSize);
|
||||||
|
const tail = buf.toString('utf-8');
|
||||||
|
const lines = tail.split('\n').filter(Boolean);
|
||||||
|
for (let i = lines.length - 1; i >= 0; i--) {
|
||||||
|
try {
|
||||||
|
const entry = JSON.parse(lines[i]);
|
||||||
|
if (entry.event === 'heartbeat_completed' || entry.event === 'heartbeat_running') {
|
||||||
|
lastHeartbeat = entry.timestamp;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch { /* skip malformed line */ }
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await fh.close();
|
||||||
|
}
|
||||||
|
} catch { /* no cron log yet */ }
|
||||||
|
|
||||||
|
// Adapter list from agentChannels (what's registered and assumed connected while running)
|
||||||
|
const adapters: Array<{ name: string; enabled: boolean; connected: boolean; lastEvent: null; error: null }> = [];
|
||||||
|
if (options.agentChannels) {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
for (const channels of options.agentChannels.values()) {
|
||||||
|
for (const ch of channels) {
|
||||||
|
if (!seen.has(ch)) {
|
||||||
|
seen.add(ch);
|
||||||
|
adapters.push({ name: ch, enabled: true, connected: true, lastEvent: null, error: null });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
aniOnline: true,
|
||||||
|
lastHeartbeat,
|
||||||
|
errors: [],
|
||||||
|
adapters,
|
||||||
|
conversations,
|
||||||
|
aster: {
|
||||||
|
healthy: asterHealthy,
|
||||||
|
lastRunAt: null,
|
||||||
|
lastWriteAt: lastWriteStr,
|
||||||
|
conversationId: asterConvId,
|
||||||
|
agentId: asterAgentId,
|
||||||
|
recentErrors: [],
|
||||||
|
ledger: {
|
||||||
|
commitmentsLastUpdated: commitments,
|
||||||
|
driftLogLastUpdated: driftLog,
|
||||||
|
patternsLastUpdated: patterns,
|
||||||
|
assumptionsLastUpdated: assumptions,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
});
|
||||||
|
res.end(JSON.stringify(payload));
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error('Bridge status error:', error);
|
||||||
|
sendError(res, 500, error.message || 'Internal server error');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route: POST /api/v1/aster/reset - Cycle Aster's conversation (same as !reset aster)
|
||||||
|
if (req.url === '/api/v1/aster/reset' && req.method === 'POST') {
|
||||||
|
try {
|
||||||
|
const { createConversationForAgent } = await import('../tools/letta-api.js');
|
||||||
|
const asterAgentId = process.env.CONSCIENCE_AGENT_ID;
|
||||||
|
if (!asterAgentId) {
|
||||||
|
sendError(res, 400, 'CONSCIENCE_AGENT_ID not configured');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newConvId = await createConversationForAgent(asterAgentId);
|
||||||
|
if (!newConvId) {
|
||||||
|
sendError(res, 500, 'Failed to create new conscience conversation');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
process.env.CONSCIENCE_CONVERSATION_ID = newConvId;
|
||||||
|
const workingDir = process.env.WORKING_DIR || process.env.HOME || '/home/ani';
|
||||||
|
// Write conscience state file so letta-code picks up new conv ID without restart
|
||||||
|
try {
|
||||||
|
const { writeFile: wf } = await import('node:fs/promises');
|
||||||
|
const stateFile = `${workingDir}/.conscience-state.json`;
|
||||||
|
await wf(stateFile, JSON.stringify({ conversationId: newConvId, updatedAt: new Date().toISOString() }), 'utf-8');
|
||||||
|
log.info(`aster/reset: wrote conscience state to ${stateFile}`);
|
||||||
|
} catch (stateErr) {
|
||||||
|
log.warn('aster/reset: failed to write conscience state file:', stateErr);
|
||||||
|
}
|
||||||
|
// Persist to service file so restarts pick it up
|
||||||
|
const svcFile = `${process.env.HOME || '/home/ani'}/.config/systemd/user/ani-bridge.service`;
|
||||||
|
try {
|
||||||
|
const { readFile: rf, writeFile: wf } = await import('node:fs/promises');
|
||||||
|
const { execFile } = await import('node:child_process');
|
||||||
|
const { promisify } = await import('node:util');
|
||||||
|
const exec = promisify(execFile);
|
||||||
|
const current = await rf(svcFile, 'utf-8');
|
||||||
|
const updated = current.replace(
|
||||||
|
/^(Environment=CONSCIENCE_CONVERSATION_ID=).+$/m,
|
||||||
|
`$1${newConvId}`,
|
||||||
|
);
|
||||||
|
await wf(svcFile, updated, 'utf-8');
|
||||||
|
await exec('systemctl', ['--user', 'daemon-reload']);
|
||||||
|
} catch (svcErr) {
|
||||||
|
log.warn('aster/reset: failed to patch service file:', svcErr);
|
||||||
|
}
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
});
|
||||||
|
res.end(JSON.stringify({ ok: true, conversationId: newConvId }));
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error('Aster reset error:', error);
|
||||||
|
sendError(res, 500, error.message || 'Internal server error');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Route: POST /api/v1/conversation - Set conversation ID
|
// Route: POST /api/v1/conversation - Set conversation ID
|
||||||
if (req.url === '/api/v1/conversation' && req.method === 'POST') {
|
if (req.url === '/api/v1/conversation' && req.method === 'POST') {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ export interface DiscordConfig {
|
|||||||
groups?: Record<string, GroupModeConfig>; // Per-guild/channel settings
|
groups?: Record<string, GroupModeConfig>; // Per-guild/channel settings
|
||||||
agentName?: string; // For scoping daily limit counters in multi-agent mode
|
agentName?: string; // For scoping daily limit counters in multi-agent mode
|
||||||
ignoreBotReactions?: boolean; // Ignore all bot reactions (default: true). Set false for multi-bot setups.
|
ignoreBotReactions?: boolean; // Ignore all bot reactions (default: true). Set false for multi-bot setups.
|
||||||
|
ttsUrl?: string; // TTS API endpoint (e.g. VibeVoice)
|
||||||
|
ttsVoice?: string; // TTS voice ID
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldProcessDiscordBotMessage(params: {
|
export function shouldProcessDiscordBotMessage(params: {
|
||||||
@@ -114,6 +116,8 @@ export class DiscordAdapter implements ChannelAdapter {
|
|||||||
private running = false;
|
private running = false;
|
||||||
private attachmentsDir?: string;
|
private attachmentsDir?: string;
|
||||||
private attachmentsMaxBytes?: number;
|
private attachmentsMaxBytes?: number;
|
||||||
|
// In-memory store: messageId → { text, chatId } for 🎤 TTS regeneration
|
||||||
|
private audioMessages = new Map<string, { text: string; chatId: string }>();
|
||||||
|
|
||||||
onMessage?: (msg: InboundMessage) => Promise<void>;
|
onMessage?: (msg: InboundMessage) => Promise<void>;
|
||||||
onCommand?: (command: string, chatId?: string, args?: string, forcePerChat?: boolean) => Promise<string | null>;
|
onCommand?: (command: string, chatId?: string, args?: string, forcePerChat?: boolean) => Promise<string | null>;
|
||||||
@@ -245,36 +249,43 @@ Ask the bot owner to approve with:
|
|||||||
const userId = message.author?.id;
|
const userId = message.author?.id;
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
// Bypass pairing for guild (group) messages
|
// Access check applies to both DMs and guild messages.
|
||||||
if (!message.guildId) {
|
// Guild messages previously bypassed this entirely — that allowed anyone
|
||||||
const access = await this.checkAccess(userId);
|
// in a shared server to reach the bot regardless of allowedUsers.
|
||||||
if (access === 'blocked') {
|
const access = await this.checkAccess(userId);
|
||||||
|
if (access === 'blocked') {
|
||||||
|
if (!message.guildId) {
|
||||||
|
// Only reply in DMs — silently drop in guild channels to avoid noise
|
||||||
const ch = message.channel;
|
const ch = message.channel;
|
||||||
if (ch.isTextBased() && 'send' in ch) {
|
if (ch.isTextBased() && 'send' in ch) {
|
||||||
await (ch as { send: (content: string) => Promise<unknown> }).send(
|
await (ch as { send: (content: string) => Promise<unknown> }).send(
|
||||||
"Sorry, you're not authorized to use this bot."
|
"Sorry, you're not authorized to use this bot."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (access === 'pairing') {
|
||||||
|
if (message.guildId) {
|
||||||
|
// Don't start pairing flows in guild channels — DM only
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { code, created } = await upsertPairingRequest('discord', userId, {
|
||||||
|
username: message.author.username,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!code) {
|
||||||
|
await message.channel.send('Too many pending pairing requests. Please try again later.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (access === 'pairing') {
|
if (created) {
|
||||||
const { code, created } = await upsertPairingRequest('discord', userId, {
|
log.info(`New pairing request from ${userId} (${message.author.username}): ${code}`);
|
||||||
username: message.author.username,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!code) {
|
|
||||||
await message.channel.send('Too many pending pairing requests. Please try again later.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (created) {
|
|
||||||
log.info(`New pairing request from ${userId} (${message.author.username}): ${code}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.sendPairingMessage(message, this.formatPairingMsg(code));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.sendPairingMessage(message, this.formatPairingMsg(code));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (content.startsWith('/')) {
|
if (content.startsWith('/')) {
|
||||||
@@ -568,6 +579,36 @@ Ask the bot owner to approve with:
|
|||||||
await message.react(resolved);
|
await message.react(resolved);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
storeAudioMessage(messageId: string, _conversationId: string, chatId: string, text: string): void {
|
||||||
|
this.audioMessages.set(messageId, { text, chatId });
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendAudio(chatId: string, text: string): Promise<void> {
|
||||||
|
if (!this.config.ttsUrl) return;
|
||||||
|
try {
|
||||||
|
const { synthesizeSpeech } = await import('./matrix/tts.js');
|
||||||
|
const audioData = await synthesizeSpeech(text, {
|
||||||
|
url: this.config.ttsUrl,
|
||||||
|
voice: this.config.ttsVoice,
|
||||||
|
});
|
||||||
|
// Write to temp file and send as attachment
|
||||||
|
const { writeFile } = await import('node:fs/promises');
|
||||||
|
const tmpPath = `/tmp/discord-tts-${Date.now()}.mp3`;
|
||||||
|
await writeFile(tmpPath, audioData);
|
||||||
|
const result = await this.sendFile({
|
||||||
|
chatId,
|
||||||
|
filePath: tmpPath,
|
||||||
|
kind: 'audio',
|
||||||
|
});
|
||||||
|
// Store for 🎤 regeneration
|
||||||
|
this.audioMessages.set(result.messageId, { text, chatId });
|
||||||
|
// Clean up temp file
|
||||||
|
import('node:fs/promises').then(fs => fs.unlink(tmpPath).catch(() => {}));
|
||||||
|
} catch (err) {
|
||||||
|
log.error('TTS failed (non-fatal):', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async sendTypingIndicator(chatId: string): Promise<void> {
|
async sendTypingIndicator(chatId: string): Promise<void> {
|
||||||
if (!this.client) return;
|
if (!this.client) return;
|
||||||
try {
|
try {
|
||||||
@@ -678,6 +719,18 @@ Ask the bot owner to approve with:
|
|||||||
: (reaction.emoji.name || reaction.emoji.toString());
|
: (reaction.emoji.name || reaction.emoji.toString());
|
||||||
if (!emoji) return;
|
if (!emoji) return;
|
||||||
|
|
||||||
|
// 🎤 reaction = TTS regeneration (handle locally, don't forward to agent)
|
||||||
|
if (emoji === '🎤' && action === 'added' && this.config.ttsUrl) {
|
||||||
|
const stored = this.audioMessages.get(message.id);
|
||||||
|
if (stored) {
|
||||||
|
log.info(`🎤 TTS regeneration for message ${message.id}`);
|
||||||
|
this.sendAudio(stored.chatId, stored.text).catch(err =>
|
||||||
|
log.error('🎤 TTS regeneration failed:', err)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return; // consumed — don't forward to agent
|
||||||
|
}
|
||||||
|
|
||||||
const groupName = isGroup && 'name' in message.channel
|
const groupName = isGroup && 'name' in message.channel
|
||||||
? message.channel.name || undefined
|
? message.channel.name || undefined
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|||||||
@@ -124,6 +124,8 @@ const SHARED_CHANNEL_BUILDERS: SharedChannelBuilder[] = [
|
|||||||
groups: discord.groups,
|
groups: discord.groups,
|
||||||
agentName: agentConfig.name,
|
agentName: agentConfig.name,
|
||||||
ignoreBotReactions: discord.ignoreBotReactions,
|
ignoreBotReactions: discord.ignoreBotReactions,
|
||||||
|
ttsUrl: discord.ttsUrl,
|
||||||
|
ttsVoice: discord.ttsVoice,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -227,8 +227,11 @@ export class MatrixAdapter implements ChannelAdapter {
|
|||||||
if (!this.client) throw new Error("Matrix client not initialized");
|
if (!this.client) throw new Error("Matrix client not initialized");
|
||||||
|
|
||||||
const { chatId, text } = msg;
|
const { chatId, text } = msg;
|
||||||
const { plain, html } = formatMatrixHTML(text);
|
// If parseMode is HTML, text is already formatted — skip markdown conversion
|
||||||
const htmlBody = (msg.htmlPrefix || '') + html;
|
const { plain, html } = msg.parseMode === 'HTML'
|
||||||
|
? { plain: text.replace(/<[^>]+>/g, ''), html: text }
|
||||||
|
: formatMatrixHTML(text);
|
||||||
|
const htmlBody = html;
|
||||||
|
|
||||||
const content = {
|
const content = {
|
||||||
msgtype: MsgType.Text,
|
msgtype: MsgType.Text,
|
||||||
@@ -240,19 +243,43 @@ export class MatrixAdapter implements ChannelAdapter {
|
|||||||
const response = await this.client.sendMessage(chatId, content);
|
const response = await this.client.sendMessage(chatId, content);
|
||||||
const eventId = response.event_id;
|
const eventId = response.event_id;
|
||||||
|
|
||||||
// Send TTS audio if this was a voice-input response or enableAudioResponse is set
|
// TTS and 🎤 are NOT added here — sendMessage is called for reasoning
|
||||||
if (this.config.ttsUrl && this.shouldSendAudio(chatId)) {
|
// displays, tool call displays, AND final responses. TTS should only
|
||||||
this.sendAudio(chatId, plain).catch(err => log.error('TTS failed (non-fatal):', err));
|
// fire on the final response, which is handled via onMessageSent().
|
||||||
}
|
|
||||||
|
|
||||||
// Add 🎤 reaction so user can request TTS on demand
|
|
||||||
if (this.config.ttsUrl) {
|
|
||||||
this.addReaction(chatId, eventId, '🎤').catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
return { messageId: eventId };
|
return { messageId: eventId };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message as a reply in a Matrix thread.
|
||||||
|
* Creates a thread if one doesn't exist yet on the parent event.
|
||||||
|
*/
|
||||||
|
async sendThreadMessage(parentEventId: string, chatId: string, text: string, parseMode?: string): Promise<{ messageId: string }> {
|
||||||
|
if (!this.client) throw new Error("Matrix client not initialized");
|
||||||
|
|
||||||
|
const { plain, html } = parseMode === 'HTML'
|
||||||
|
? { plain: text.replace(/<[^>]+>/g, ''), html: text }
|
||||||
|
: formatMatrixHTML(text);
|
||||||
|
|
||||||
|
const content = {
|
||||||
|
msgtype: MsgType.Text,
|
||||||
|
body: plain,
|
||||||
|
format: "org.matrix.custom.html",
|
||||||
|
formatted_body: html,
|
||||||
|
"m.relates_to": {
|
||||||
|
rel_type: "m.thread",
|
||||||
|
event_id: parentEventId,
|
||||||
|
is_falling_back: true,
|
||||||
|
"m.in_reply_to": {
|
||||||
|
event_id: parentEventId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const response = await this.client.sendMessage(chatId, content);
|
||||||
|
return { messageId: response.event_id };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decide whether to send a TTS audio response for this room.
|
* Decide whether to send a TTS audio response for this room.
|
||||||
* Consumes the pendingVoiceRooms flag if set (voice-input path).
|
* Consumes the pendingVoiceRooms flag if set (voice-input path).
|
||||||
@@ -273,11 +300,11 @@ export class MatrixAdapter implements ChannelAdapter {
|
|||||||
return true; // 'all'
|
return true; // 'all'
|
||||||
}
|
}
|
||||||
|
|
||||||
async editMessage(chatId: string, messageId: string, text: string, htmlPrefix?: string): Promise<void> {
|
async editMessage(chatId: string, messageId: string, text: string): Promise<void> {
|
||||||
if (!this.client) throw new Error("Matrix client not initialized");
|
if (!this.client) throw new Error("Matrix client not initialized");
|
||||||
|
|
||||||
const { plain, html } = formatMatrixHTML(text);
|
const { plain, html } = formatMatrixHTML(text);
|
||||||
const htmlBody = (htmlPrefix || '') + html;
|
const htmlBody = html;
|
||||||
const prefixedPlain = this.config.messagePrefix ? `${this.config.messagePrefix}\n\n${plain}` : plain;
|
const prefixedPlain = this.config.messagePrefix ? `${this.config.messagePrefix}\n\n${plain}` : plain;
|
||||||
const prefixedHtml = this.config.messagePrefix ? `${this.config.messagePrefix}<br><br>${htmlBody}` : htmlBody;
|
const prefixedHtml = this.config.messagePrefix ? `${this.config.messagePrefix}<br><br>${htmlBody}` : htmlBody;
|
||||||
|
|
||||||
@@ -1454,7 +1481,7 @@ export class MatrixAdapter implements ChannelAdapter {
|
|||||||
if (kind === 'audio') {
|
if (kind === 'audio') {
|
||||||
this.ourAudioEvents.add(eventId);
|
this.ourAudioEvents.add(eventId);
|
||||||
if (caption) {
|
if (caption) {
|
||||||
this.storage.storeAudioMessage(eventId, 'default', chatId, caption);
|
this.storage.storeAudioMessage(eventId, chatId, chatId, caption);
|
||||||
}
|
}
|
||||||
const reactionContent: ReactionEventContent = {
|
const reactionContent: ReactionEventContent = {
|
||||||
"m.relates_to": {
|
"m.relates_to": {
|
||||||
@@ -1485,7 +1512,7 @@ export class MatrixAdapter implements ChannelAdapter {
|
|||||||
const audioEventId = await this.uploadAndSendAudio(roomId, audioData);
|
const audioEventId = await this.uploadAndSendAudio(roomId, audioData);
|
||||||
if (audioEventId) {
|
if (audioEventId) {
|
||||||
// Store mapping so 🎤 on the regenerated audio works too
|
// Store mapping so 🎤 on the regenerated audio works too
|
||||||
this.storage.storeAudioMessage(audioEventId, "default", roomId, text);
|
this.storage.storeAudioMessage(audioEventId, roomId, roomId, text);
|
||||||
}
|
}
|
||||||
return audioEventId;
|
return audioEventId;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -1516,7 +1543,7 @@ export class MatrixAdapter implements ChannelAdapter {
|
|||||||
const audioEventId = await this.uploadAndSendAudio(chatId, audioData);
|
const audioEventId = await this.uploadAndSendAudio(chatId, audioData);
|
||||||
if (audioEventId) {
|
if (audioEventId) {
|
||||||
// Store for 🎤 regeneration
|
// Store for 🎤 regeneration
|
||||||
this.storage.storeAudioMessage(audioEventId, "default", chatId, text);
|
this.storage.storeAudioMessage(audioEventId, chatId, chatId, text);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error("TTS failed (non-fatal):", err);
|
log.error("TTS failed (non-fatal):", err);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
* Unrecognized !x commands fall through to Letta as normal text.
|
* Unrecognized !x commands fall through to Letta as normal text.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { execFile } from "node:child_process";
|
||||||
import { createLogger } from "../../logger.js";
|
import { createLogger } from "../../logger.js";
|
||||||
import type { MatrixStorage } from "./storage.js";
|
import type { MatrixStorage } from "./storage.js";
|
||||||
const log = createLogger('MatrixCommands');
|
const log = createLogger('MatrixCommands');
|
||||||
@@ -78,6 +79,8 @@ export class MatrixCommandProcessor {
|
|||||||
return this.doTurns(args[0], roomId);
|
return this.doTurns(args[0], roomId);
|
||||||
case "timeout":
|
case "timeout":
|
||||||
return this.doTimeout();
|
return this.doTimeout();
|
||||||
|
case "restart":
|
||||||
|
return this.doRestart();
|
||||||
|
|
||||||
// Heartbeat: on/off toggles locally, bare !heartbeat delegates to /heartbeat (trigger)
|
// Heartbeat: on/off toggles locally, bare !heartbeat delegates to /heartbeat (trigger)
|
||||||
case "heartbeat":
|
case "heartbeat":
|
||||||
@@ -170,6 +173,9 @@ export class MatrixCommandProcessor {
|
|||||||
" `!heartbeat on/off` — Toggle heartbeat cron",
|
" `!heartbeat on/off` — Toggle heartbeat cron",
|
||||||
" `!heartbeat` — Trigger heartbeat now",
|
" `!heartbeat` — Trigger heartbeat now",
|
||||||
" `!timeout` — Kill stuck heartbeat run",
|
" `!timeout` — Kill stuck heartbeat run",
|
||||||
|
"",
|
||||||
|
"**System**",
|
||||||
|
" `!restart` — Graceful service restart",
|
||||||
];
|
];
|
||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
@@ -233,4 +239,13 @@ export class MatrixCommandProcessor {
|
|||||||
}
|
}
|
||||||
return "⚠️ No heartbeat timeout handler registered";
|
return "⚠️ No heartbeat timeout handler registered";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private doRestart(): string {
|
||||||
|
log.info('!restart: scheduling graceful restart via transient systemd unit');
|
||||||
|
// Spawn restart as a transient systemd unit so it survives our own process death
|
||||||
|
execFile('systemd-run', ['--user', '--no-block', 'systemctl', '--user', 'restart', 'ani-bridge.service'], (err) => {
|
||||||
|
if (err) log.error('!restart: failed to schedule restart:', err.message);
|
||||||
|
});
|
||||||
|
return "Restarting in a moment...";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export interface ChannelAdapter {
|
|||||||
|
|
||||||
// Messaging
|
// Messaging
|
||||||
sendMessage(msg: OutboundMessage): Promise<{ messageId: string }>;
|
sendMessage(msg: OutboundMessage): Promise<{ messageId: string }>;
|
||||||
editMessage(chatId: string, messageId: string, text: string, htmlPrefix?: string): Promise<void>;
|
editMessage(chatId: string, messageId: string, text: string): Promise<void>;
|
||||||
sendTypingIndicator(chatId: string): Promise<void>;
|
sendTypingIndicator(chatId: string): Promise<void>;
|
||||||
stopTypingIndicator?(chatId: string): Promise<void>;
|
stopTypingIndicator?(chatId: string): Promise<void>;
|
||||||
|
|
||||||
@@ -35,6 +35,8 @@ export interface ChannelAdapter {
|
|||||||
onMessageSent?(chatId: string, messageId: string, stepId?: string): void;
|
onMessageSent?(chatId: string, messageId: string, stepId?: string): void;
|
||||||
/** Store text for TTS regeneration on 🎤 reaction */
|
/** Store text for TTS regeneration on 🎤 reaction */
|
||||||
storeAudioMessage?(messageId: string, conversationId: string, roomId: string, text: string): void;
|
storeAudioMessage?(messageId: string, conversationId: string, roomId: string, text: string): void;
|
||||||
|
/** Send a message as a reply in a Matrix thread (no-op on non-threaded adapters) */
|
||||||
|
sendThreadMessage?(parentEventId: string, chatId: string, text: string, parseMode?: string): Promise<{ messageId: string }>;
|
||||||
getDmPolicy?(): string;
|
getDmPolicy?(): string;
|
||||||
getFormatterHints(): FormatterHints;
|
getFormatterHints(): FormatterHints;
|
||||||
|
|
||||||
|
|||||||
@@ -548,6 +548,21 @@ export function configToEnv(config: LettaBotConfig): Record<string, string> {
|
|||||||
env.SLEEPTIME_STEP_COUNT = String(config.features.sleeptime.stepCount);
|
env.SLEEPTIME_STEP_COUNT = String(config.features.sleeptime.stepCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (config.features?.conscience) {
|
||||||
|
if (config.features.conscience.agent) {
|
||||||
|
env.CONSCIENCE_AGENT_ID = config.features.conscience.agent;
|
||||||
|
}
|
||||||
|
if (config.features.conscience.conversation) {
|
||||||
|
// Only set as initial value — runtime resets mutate process.env directly
|
||||||
|
env.CONSCIENCE_CONVERSATION_ID ??= config.features.conscience.conversation;
|
||||||
|
}
|
||||||
|
if (config.features.conscience.trigger) {
|
||||||
|
env.CONSCIENCE_TRIGGER = config.features.conscience.trigger;
|
||||||
|
}
|
||||||
|
if (config.features.conscience.stepCount !== undefined) {
|
||||||
|
env.CONSCIENCE_STEP_COUNT = String(config.features.conscience.stepCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (config.features?.inlineImages === false) {
|
if (config.features?.inlineImages === false) {
|
||||||
env.INLINE_IMAGES = 'false';
|
env.INLINE_IMAGES = 'false';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,10 +39,8 @@ export interface DisplayConfig {
|
|||||||
showReasoning?: boolean;
|
showReasoning?: boolean;
|
||||||
/** Truncate reasoning to N characters (default: 0 = no limit) */
|
/** Truncate reasoning to N characters (default: 0 = no limit) */
|
||||||
reasoningMaxChars?: number;
|
reasoningMaxChars?: number;
|
||||||
/** Room IDs where reasoning should be shown (empty = all rooms that have showReasoning) */
|
/** Add 🎤 reaction to reasoning messages for TTS regeneration (default: false) */
|
||||||
reasoningRooms?: string[];
|
ttsOnReasoning?: boolean;
|
||||||
/** Room IDs where reasoning should be hidden (takes precedence over reasoningRooms) */
|
|
||||||
noReasoningRooms?: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SleeptimeTrigger = 'off' | 'step-count' | 'compaction-event';
|
export type SleeptimeTrigger = 'off' | 'step-count' | 'compaction-event';
|
||||||
@@ -54,6 +52,19 @@ export interface SleeptimeConfig {
|
|||||||
stepCount?: number;
|
stepCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ConscienceTrigger = 'off' | 'step-count';
|
||||||
|
|
||||||
|
export interface ConscienceConfig {
|
||||||
|
/** Letta agent ID for the persistent conscience agent (Aster) */
|
||||||
|
agent?: string;
|
||||||
|
/** Initial conversation ID (runtime value tracked in CONSCIENCE_CONVERSATION_ID) */
|
||||||
|
conversation?: string;
|
||||||
|
/** When to fire: 'step-count' fires every N turns, 'off' disables */
|
||||||
|
trigger?: ConscienceTrigger;
|
||||||
|
/** Fire conscience every N user turns (independent of sleeptime cadence) */
|
||||||
|
stepCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration for a single agent in multi-agent mode.
|
* Configuration for a single agent in multi-agent mode.
|
||||||
* Each agent has its own name, channels, and features.
|
* Each agent has its own name, channels, and features.
|
||||||
@@ -105,6 +116,7 @@ export interface AgentConfig {
|
|||||||
};
|
};
|
||||||
memfs?: boolean; // Enable memory filesystem (git-backed context repository) for SDK sessions
|
memfs?: boolean; // Enable memory filesystem (git-backed context repository) for SDK sessions
|
||||||
sleeptime?: SleeptimeConfig; // Configure SDK reflection reminders (/sleeptime equivalent)
|
sleeptime?: SleeptimeConfig; // Configure SDK reflection reminders (/sleeptime equivalent)
|
||||||
|
conscience?: ConscienceConfig; // Persistent supervisory agent (Aster) — fires independently of sleeptime
|
||||||
maxToolCalls?: number;
|
maxToolCalls?: number;
|
||||||
sendFileDir?: string; // Restrict <send-file> directive to this directory (default: data/outbound)
|
sendFileDir?: string; // Restrict <send-file> directive to this directory (default: data/outbound)
|
||||||
sendFileMaxSize?: number; // Max file size in bytes for <send-file> (default: 50MB)
|
sendFileMaxSize?: number; // Max file size in bytes for <send-file> (default: 50MB)
|
||||||
@@ -205,6 +217,7 @@ export interface LettaBotConfig {
|
|||||||
inlineImages?: boolean; // Send images directly to the LLM (default: true). Set false to only send file paths.
|
inlineImages?: boolean; // Send images directly to the LLM (default: true). Set false to only send file paths.
|
||||||
memfs?: boolean; // Enable memory filesystem (git-backed context repository) for SDK sessions
|
memfs?: boolean; // Enable memory filesystem (git-backed context repository) for SDK sessions
|
||||||
sleeptime?: SleeptimeConfig; // Configure SDK reflection reminders (/sleeptime equivalent)
|
sleeptime?: SleeptimeConfig; // Configure SDK reflection reminders (/sleeptime equivalent)
|
||||||
|
conscience?: ConscienceConfig; // Persistent supervisory agent (Aster) — fires independently of sleeptime
|
||||||
maxToolCalls?: number; // Abort if agent calls this many tools in one turn (default: 100)
|
maxToolCalls?: number; // Abort if agent calls this many tools in one turn (default: 100)
|
||||||
sendFileDir?: string; // Restrict <send-file> directive to this directory (default: data/outbound)
|
sendFileDir?: string; // Restrict <send-file> directive to this directory (default: data/outbound)
|
||||||
sendFileMaxSize?: number; // Max file size in bytes for <send-file> (default: 50MB)
|
sendFileMaxSize?: number; // Max file size in bytes for <send-file> (default: 50MB)
|
||||||
@@ -412,6 +425,8 @@ export interface DiscordConfig {
|
|||||||
listeningGroups?: string[]; // @deprecated Use groups.<id>.mode = "listen"
|
listeningGroups?: string[]; // @deprecated Use groups.<id>.mode = "listen"
|
||||||
groups?: Record<string, GroupConfig>; // Per-guild/channel settings, "*" for defaults
|
groups?: Record<string, GroupConfig>; // Per-guild/channel settings, "*" for defaults
|
||||||
ignoreBotReactions?: boolean; // Ignore all bot reactions (default: true). Set false for multi-bot setups.
|
ignoreBotReactions?: boolean; // Ignore all bot reactions (default: true). Set false for multi-bot setups.
|
||||||
|
ttsUrl?: string; // TTS API endpoint (e.g. VibeVoice)
|
||||||
|
ttsVoice?: string; // TTS voice ID
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BlueskyConfig {
|
export interface BlueskyConfig {
|
||||||
@@ -973,6 +988,29 @@ export function normalizeAgents(config: LettaBotConfig): AgentConfig[] {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Conscience (Aster) — env vars override YAML, YAML sets initial values
|
||||||
|
const conscienceAgentId = process.env.CONSCIENCE_AGENT_ID;
|
||||||
|
const conscienceConversationId = process.env.CONSCIENCE_CONVERSATION_ID;
|
||||||
|
const conscienceStepCountRaw = process.env.CONSCIENCE_STEP_COUNT;
|
||||||
|
const conscienceTriggerRaw = process.env.CONSCIENCE_TRIGGER;
|
||||||
|
const conscienceTrigger = conscienceTriggerRaw === 'off' || conscienceTriggerRaw === 'step-count'
|
||||||
|
? conscienceTriggerRaw as ConscienceTrigger
|
||||||
|
: undefined;
|
||||||
|
const conscienceStepCountParsed = conscienceStepCountRaw ? parseInt(conscienceStepCountRaw, 10) : undefined;
|
||||||
|
const conscienceStepCount = Number.isFinite(conscienceStepCountParsed) && (conscienceStepCountParsed as number) > 0
|
||||||
|
? conscienceStepCountParsed
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (conscienceAgentId || conscienceConversationId || conscienceStepCount || conscienceTrigger) {
|
||||||
|
features.conscience = {
|
||||||
|
...features.conscience,
|
||||||
|
...(conscienceAgentId ? { agent: conscienceAgentId } : {}),
|
||||||
|
...(conscienceConversationId ? { conversation: conscienceConversationId } : {}),
|
||||||
|
...(conscienceTrigger ? { trigger: conscienceTrigger } : {}),
|
||||||
|
...(conscienceStepCount ? { stepCount: conscienceStepCount } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Only pass features if there's actually something set
|
// Only pass features if there's actually something set
|
||||||
const hasFeatures = Object.keys(features).length > 0;
|
const hasFeatures = Object.keys(features).length > 0;
|
||||||
|
|
||||||
|
|||||||
259
src/core/bot.ts
259
src/core/bot.ts
@@ -13,10 +13,10 @@ import { extname, resolve, join } from 'node:path';
|
|||||||
import type { ChannelAdapter } from '../channels/types.js';
|
import type { ChannelAdapter } from '../channels/types.js';
|
||||||
import type { BotConfig, InboundMessage, TriggerContext, TriggerType, StreamMsg } from './types.js';
|
import type { BotConfig, InboundMessage, TriggerContext, TriggerType, StreamMsg } from './types.js';
|
||||||
import { formatApiErrorForUser } from './errors.js';
|
import { formatApiErrorForUser } from './errors.js';
|
||||||
import { formatToolCallDisplay, formatReasoningDisplay, formatQuestionsForChannel, formatReasoningAsCodeBlock } from './display.js';
|
import { formatToolCallDisplay, formatReasoningDisplay, formatQuestionsForChannel } from './display.js';
|
||||||
import type { AgentSession } from './interfaces.js';
|
import type { AgentSession } from './interfaces.js';
|
||||||
import { Store } from './store.js';
|
import { Store } from './store.js';
|
||||||
import { getPendingApprovals, rejectApproval, cancelRuns, cancelConversation, recoverOrphanedConversationApproval, getLatestRunError, getAgentModel, updateAgentModel, isRecoverableConversationId, recoverPendingApprovalsForAgent } from '../tools/letta-api.js';
|
import { getPendingApprovals, rejectApproval, cancelRuns, cancelConversation, recoverOrphanedConversationApproval, getLatestRunError, getAgentModel, updateAgentModel, isRecoverableConversationId, recoverPendingApprovalsForAgent, createConversationForAgent, sendMessageToConversation } from '../tools/letta-api.js';
|
||||||
import { getAgentSkillExecutableDirs, isVoiceMemoConfigured } from '../skills/loader.js';
|
import { getAgentSkillExecutableDirs, isVoiceMemoConfigured } from '../skills/loader.js';
|
||||||
import { formatMessageEnvelope, formatGroupBatchEnvelope, type SessionContextOptions } from './formatter.js';
|
import { formatMessageEnvelope, formatGroupBatchEnvelope, type SessionContextOptions } from './formatter.js';
|
||||||
import type { GroupBatcher } from './group-batcher.js';
|
import type { GroupBatcher } from './group-batcher.js';
|
||||||
@@ -785,6 +785,44 @@ export class LettaBot implements AgentSession {
|
|||||||
return '⏰ Heartbeat triggered (silent mode - check server logs)';
|
return '⏰ Heartbeat triggered (silent mode - check server logs)';
|
||||||
}
|
}
|
||||||
case 'reset': {
|
case 'reset': {
|
||||||
|
// !reset aster — cycle only Aster's conscience conversation, leave Ani's alone.
|
||||||
|
if (args?.trim().toLowerCase() === 'aster') {
|
||||||
|
const conscienceAgentId = process.env.CONSCIENCE_AGENT_ID;
|
||||||
|
if (!conscienceAgentId) {
|
||||||
|
return 'Conscience agent not configured (CONSCIENCE_AGENT_ID not set).';
|
||||||
|
}
|
||||||
|
const newConscienceConvId = await createConversationForAgent(conscienceAgentId);
|
||||||
|
if (!newConscienceConvId) {
|
||||||
|
return 'Failed to create a new conscience conversation. Check server logs.';
|
||||||
|
}
|
||||||
|
// Update in-memory env var so the running process uses the new conversation immediately.
|
||||||
|
process.env.CONSCIENCE_CONVERSATION_ID = newConscienceConvId;
|
||||||
|
// Persist to store (lettabot-agent.json) for reference.
|
||||||
|
this.store.setAgentField('Aster', 'conversationId', newConscienceConvId);
|
||||||
|
// Patch the systemd service file so restarts also pick up the new conversation.
|
||||||
|
const serviceFile = '/home/ani/.config/systemd/user/ani-bridge.service';
|
||||||
|
try {
|
||||||
|
const { writeFile } = await import('node:fs/promises');
|
||||||
|
const current = await readFile(serviceFile, 'utf-8');
|
||||||
|
const updated = current.replace(
|
||||||
|
/^(Environment=CONSCIENCE_CONVERSATION_ID=).+$/m,
|
||||||
|
`$1${newConscienceConvId}`,
|
||||||
|
);
|
||||||
|
await writeFile(serviceFile, updated, 'utf-8');
|
||||||
|
// Reload systemd unit definitions (no restart — just picks up the edited file).
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
execFile('systemctl', ['--user', 'daemon-reload'], (err) => {
|
||||||
|
if (err) reject(err); else resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
log.info(`/reset aster - service file updated, daemon reloaded: ${newConscienceConvId}`);
|
||||||
|
} catch (svcErr) {
|
||||||
|
log.warn(`/reset aster - failed to patch service file: ${svcErr}`);
|
||||||
|
}
|
||||||
|
log.info(`/reset aster - conscience conversation cycled: ${newConscienceConvId}`);
|
||||||
|
return `Aster's conversation reset. New conversation: \`${newConscienceConvId}\`\nService file updated — restart-safe.`;
|
||||||
|
}
|
||||||
|
|
||||||
// Always scope the reset to the caller's conversation key so that
|
// Always scope the reset to the caller's conversation key so that
|
||||||
// other channels/chats' conversations are never silently destroyed.
|
// other channels/chats' conversations are never silently destroyed.
|
||||||
// resolveConversationKey returns 'shared' for non-override channels,
|
// resolveConversationKey returns 'shared' for non-override channels,
|
||||||
@@ -806,6 +844,40 @@ export class LettaBot implements AgentSession {
|
|||||||
const session = await this.sessionManager.ensureSessionForKey(convKey);
|
const session = await this.sessionManager.ensureSessionForKey(convKey);
|
||||||
const newConvId = session.conversationId || '(pending)';
|
const newConvId = session.conversationId || '(pending)';
|
||||||
this.sessionManager.persistSessionState(session, convKey);
|
this.sessionManager.persistSessionState(session, convKey);
|
||||||
|
|
||||||
|
// Reset conscience conversation alongside Ani's.
|
||||||
|
// This ensures failure notifications target the new active conversation
|
||||||
|
// and conscience starts fresh rather than replaying a broken context.
|
||||||
|
const conscienceAgentId = process.env.CONSCIENCE_AGENT_ID;
|
||||||
|
if (conscienceAgentId) {
|
||||||
|
const newConscienceConvId = await createConversationForAgent(conscienceAgentId);
|
||||||
|
if (newConscienceConvId) {
|
||||||
|
process.env.CONSCIENCE_CONVERSATION_ID = newConscienceConvId;
|
||||||
|
this.store.setAgentField('Aster', 'conversationId', newConscienceConvId);
|
||||||
|
// Also patch the service file so restarts pick up the new conversation.
|
||||||
|
const serviceFile = '/home/ani/.config/systemd/user/ani-bridge.service';
|
||||||
|
try {
|
||||||
|
const { writeFile } = await import('node:fs/promises');
|
||||||
|
const current = await readFile(serviceFile, 'utf-8');
|
||||||
|
const updated = current.replace(
|
||||||
|
/^(Environment=CONSCIENCE_CONVERSATION_ID=).+$/m,
|
||||||
|
`$1${newConscienceConvId}`,
|
||||||
|
);
|
||||||
|
await writeFile(serviceFile, updated, 'utf-8');
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
execFile('systemctl', ['--user', 'daemon-reload'], (err) => {
|
||||||
|
if (err) reject(err); else resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (svcErr) {
|
||||||
|
log.warn(`/reset - failed to patch conscience service var: ${svcErr}`);
|
||||||
|
}
|
||||||
|
log.info(`/reset - conscience conversation cycled: ${newConscienceConvId}`);
|
||||||
|
} else {
|
||||||
|
log.warn('/reset - Failed to cycle conscience conversation; will resume the previous one.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (convKey === 'shared') {
|
if (convKey === 'shared') {
|
||||||
return `Conversation reset. New conversation: ${newConvId}\n(Agent memory is preserved.)`;
|
return `Conversation reset. New conversation: ${newConvId}\n(Agent memory is preserved.)`;
|
||||||
}
|
}
|
||||||
@@ -1322,7 +1394,6 @@ export class LettaBot implements AgentSession {
|
|||||||
let lastEventType: string | null = null;
|
let lastEventType: string | null = null;
|
||||||
let abortedWithMessage = false;
|
let abortedWithMessage = false;
|
||||||
let turnError: string | undefined;
|
let turnError: string | undefined;
|
||||||
let collectedReasoning = '';
|
|
||||||
|
|
||||||
// ── Reaction tracking ──
|
// ── Reaction tracking ──
|
||||||
// 👀 = receipt indicator (bot saw the message); removed when reasoning/tools start
|
// 👀 = receipt indicator (bot saw the message); removed when reasoning/tools start
|
||||||
@@ -1335,6 +1406,10 @@ export class LettaBot implements AgentSession {
|
|||||||
adapter.addReaction?.(msg.chatId, msg.messageId, '👀').catch(() => {});
|
adapter.addReaction?.(msg.chatId, msg.messageId, '👀').catch(() => {});
|
||||||
eyesAdded = true;
|
eyesAdded = true;
|
||||||
}
|
}
|
||||||
|
// ── Subagent thread tracking ──
|
||||||
|
// When a Task tool call fires, create a Matrix thread for visibility
|
||||||
|
const subagentThreads = new Map<string, { rootEventId: string; chatId: string }>();
|
||||||
|
|
||||||
const seenToolEmojis = new Set<string>();
|
const seenToolEmojis = new Set<string>();
|
||||||
const getToolEmoji = (toolName: string): string => {
|
const getToolEmoji = (toolName: string): string => {
|
||||||
const n = toolName.toLowerCase();
|
const n = toolName.toLowerCase();
|
||||||
@@ -1438,9 +1513,7 @@ export class LettaBot implements AgentSession {
|
|||||||
lastEventType = 'reasoning';
|
lastEventType = 'reasoning';
|
||||||
sawNonAssistantSinceLastUuid = true;
|
sawNonAssistantSinceLastUuid = true;
|
||||||
// Collect reasoning for later prepending (Matrix <details> block)
|
// Collect reasoning for later prepending (Matrix <details> block)
|
||||||
if (event.content) {
|
// reasoning content is sent as display message below
|
||||||
collectedReasoning += event.content;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove 👀 on first reasoning event (replaced by 🧠)
|
// Remove 👀 on first reasoning event (replaced by 🧠)
|
||||||
if (eyesAdded && msg.messageId) {
|
if (eyesAdded && msg.messageId) {
|
||||||
@@ -1457,12 +1530,18 @@ export class LettaBot implements AgentSession {
|
|||||||
log.info(`Reasoning: ${event.content.trim().slice(0, 100)}`);
|
log.info(`Reasoning: ${event.content.trim().slice(0, 100)}`);
|
||||||
try {
|
try {
|
||||||
const reasoning = formatReasoningDisplay(event.content, adapter.id, this.config.display?.reasoningMaxChars);
|
const reasoning = formatReasoningDisplay(event.content, adapter.id, this.config.display?.reasoningMaxChars);
|
||||||
await adapter.sendMessage({
|
const reasoningResult = await adapter.sendMessage({
|
||||||
chatId: msg.chatId,
|
chatId: msg.chatId,
|
||||||
text: reasoning.text,
|
text: reasoning.text,
|
||||||
threadId: msg.threadId,
|
threadId: msg.threadId,
|
||||||
parseMode: reasoning.parseMode,
|
parseMode: reasoning.parseMode,
|
||||||
});
|
});
|
||||||
|
// 🎤 reaction + store reasoning text for TTS regeneration
|
||||||
|
// 🎤 + TTS on reasoning — only if ttsOnReasoning is enabled (default: off)
|
||||||
|
if (reasoningResult.messageId && this.config.display?.ttsOnReasoning) {
|
||||||
|
adapter.addReaction?.(msg.chatId, reasoningResult.messageId, '🎤').catch(() => {});
|
||||||
|
adapter.storeAudioMessage?.(reasoningResult.messageId, convKey, msg.chatId, event.content);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.warn('Failed to send reasoning display:', err instanceof Error ? err.message : err);
|
log.warn('Failed to send reasoning display:', err instanceof Error ? err.message : err);
|
||||||
}
|
}
|
||||||
@@ -1474,6 +1553,8 @@ export class LettaBot implements AgentSession {
|
|||||||
// Finalize any pending assistant text on type transition
|
// Finalize any pending assistant text on type transition
|
||||||
if (lastEventType === 'text' && response.trim()) {
|
if (lastEventType === 'text' && response.trim()) {
|
||||||
await finalizeMessage();
|
await finalizeMessage();
|
||||||
|
// Pulse typing indicator so there's no dead air between text and tool execution
|
||||||
|
adapter.sendTypingIndicator(msg.chatId).catch(() => {});
|
||||||
}
|
}
|
||||||
lastEventType = 'tool_call';
|
lastEventType = 'tool_call';
|
||||||
this.sessionManager.syncTodoToolCall(event.raw);
|
this.sessionManager.syncTodoToolCall(event.raw);
|
||||||
@@ -1514,11 +1595,33 @@ export class LettaBot implements AgentSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create Matrix thread for subagent Task calls
|
||||||
|
if (event.name === 'Task' && !suppressDelivery && adapter.sendThreadMessage) {
|
||||||
|
const desc = (typeof event.args?.description === 'string' ? event.args.description : '')
|
||||||
|
|| (typeof event.args?.prompt === 'string' ? event.args.prompt.slice(0, 120) : '')
|
||||||
|
|| 'Subagent task';
|
||||||
|
const subagentType = typeof event.args?.subagent_type === 'string' ? event.args.subagent_type : 'task';
|
||||||
|
try {
|
||||||
|
const threadRoot = await adapter.sendMessage({ chatId: msg.chatId, text: `**Subagent: ${subagentType}**\n${desc}`, threadId: msg.threadId });
|
||||||
|
if (event.id && threadRoot.messageId) {
|
||||||
|
subagentThreads.set(event.id, { rootEventId: threadRoot.messageId, chatId: msg.chatId });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
log.warn('Failed to create subagent thread root:', err instanceof Error ? err.message : err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Display
|
// Display
|
||||||
if (this.config.display?.showToolCalls && !suppressDelivery) {
|
if (this.config.display?.showToolCalls && !suppressDelivery) {
|
||||||
try {
|
try {
|
||||||
const text = formatToolCallDisplay(event.raw);
|
const text = formatToolCallDisplay(event.raw);
|
||||||
await adapter.sendMessage({ chatId: msg.chatId, text, threadId: msg.threadId });
|
// Send tool call display into subagent thread if one exists, otherwise to room
|
||||||
|
const thread = event.id ? subagentThreads.get(event.id) : undefined;
|
||||||
|
if (thread && adapter.sendThreadMessage) {
|
||||||
|
await adapter.sendThreadMessage(thread.rootEventId, thread.chatId, text);
|
||||||
|
} else {
|
||||||
|
await adapter.sendMessage({ chatId: msg.chatId, text, threadId: msg.threadId });
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.warn('Failed to send tool call display:', err instanceof Error ? err.message : err);
|
log.warn('Failed to send tool call display:', err instanceof Error ? err.message : err);
|
||||||
}
|
}
|
||||||
@@ -1565,6 +1668,38 @@ export class LettaBot implements AgentSession {
|
|||||||
repeatedBashFailureKey = null;
|
repeatedBashFailureKey = null;
|
||||||
repeatedBashFailureCount = 0;
|
repeatedBashFailureCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Post result to subagent thread if one exists
|
||||||
|
if (event.toolCallId && adapter.sendThreadMessage) {
|
||||||
|
const thread = subagentThreads.get(event.toolCallId);
|
||||||
|
if (thread) {
|
||||||
|
const status = event.isError ? '**Failed**' : '**Complete**';
|
||||||
|
// Post full result to thread (chunked if very long)
|
||||||
|
const maxChunk = 16000; // well under Matrix's ~65KB body limit
|
||||||
|
const content = event.content;
|
||||||
|
if (content.length <= maxChunk) {
|
||||||
|
adapter.sendThreadMessage(thread.rootEventId, thread.chatId, `${status}\n${content}`)
|
||||||
|
.catch(err => log.warn('Failed to post subagent result to thread:', err));
|
||||||
|
} else {
|
||||||
|
// Send status header + chunked content
|
||||||
|
const chunks: string[] = [];
|
||||||
|
for (let i = 0; i < content.length; i += maxChunk) {
|
||||||
|
chunks.push(content.slice(i, i + maxChunk));
|
||||||
|
}
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
await adapter.sendThreadMessage!(thread.rootEventId, thread.chatId, `${status} (${chunks.length} parts)`);
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
await adapter.sendThreadMessage!(thread.rootEventId, thread.chatId, chunk);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
log.warn('Failed to post subagent result to thread:', err);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
subagentThreads.delete(event.toolCallId);
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1594,7 +1729,7 @@ export class LettaBot implements AgentSession {
|
|||||||
|| hasUnclosedActionsBlock(response);
|
|| hasUnclosedActionsBlock(response);
|
||||||
const streamText = stripActionsBlock(response).trim();
|
const streamText = stripActionsBlock(response).trim();
|
||||||
if (canEdit && !mayBeHidden && !suppressDelivery && !this.cancelledKeys.has(convKey)
|
if (canEdit && !mayBeHidden && !suppressDelivery && !this.cancelledKeys.has(convKey)
|
||||||
&& streamText.length > 0 && Date.now() - lastUpdate > 800 && Date.now() > rateLimitedUntil) {
|
&& streamText.length > 0 && Date.now() - lastUpdate > 400 && Date.now() > rateLimitedUntil) {
|
||||||
try {
|
try {
|
||||||
const prefixedStream = this.prefixResponse(streamText);
|
const prefixedStream = this.prefixResponse(streamText);
|
||||||
if (messageId) {
|
if (messageId) {
|
||||||
@@ -1880,42 +2015,25 @@ export class LettaBot implements AgentSession {
|
|||||||
await new Promise(resolve => setTimeout(resolve, waitMs));
|
await new Promise(resolve => setTimeout(resolve, waitMs));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if reasoning should be shown for this room
|
|
||||||
const chatId = msg.chatId;
|
|
||||||
const noReasoningRooms = this.config.display?.noReasoningRooms || [];
|
|
||||||
const reasoningRooms = this.config.display?.reasoningRooms;
|
|
||||||
const shouldShowReasoning = this.config.display?.showReasoning &&
|
|
||||||
!noReasoningRooms.includes(chatId) &&
|
|
||||||
(!reasoningRooms || reasoningRooms.length === 0 || reasoningRooms.includes(chatId));
|
|
||||||
|
|
||||||
// Build reasoning HTML prefix if available (injected into formatted_body only)
|
|
||||||
let reasoningHtmlPrefix: string | undefined;
|
|
||||||
if (collectedReasoning.trim() && shouldShowReasoning) {
|
|
||||||
const reasoningBlock = formatReasoningAsCodeBlock(
|
|
||||||
collectedReasoning,
|
|
||||||
adapter.id,
|
|
||||||
this.config.display?.reasoningMaxChars
|
|
||||||
);
|
|
||||||
if (reasoningBlock) {
|
|
||||||
reasoningHtmlPrefix = reasoningBlock.text;
|
|
||||||
log.info(`Reasoning block generated (${reasoningHtmlPrefix.length} chars) for ${chatId}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalResponse = this.prefixResponse(response);
|
const finalResponse = this.prefixResponse(response);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (messageId) {
|
if (messageId) {
|
||||||
await adapter.editMessage(msg.chatId, messageId, finalResponse, reasoningHtmlPrefix);
|
await adapter.editMessage(msg.chatId, messageId, finalResponse);
|
||||||
|
// Bump: re-send the final edit after a short delay so Matrix clients
|
||||||
|
// that missed the first edit (Element caching) pick up the full text.
|
||||||
|
setTimeout(() => {
|
||||||
|
adapter.editMessage(msg.chatId, messageId!, finalResponse).catch(() => {});
|
||||||
|
}, 800);
|
||||||
} else {
|
} else {
|
||||||
await adapter.sendMessage({ chatId: msg.chatId, text: finalResponse, threadId: msg.threadId, htmlPrefix: reasoningHtmlPrefix });
|
await adapter.sendMessage({ chatId: msg.chatId, text: finalResponse, threadId: msg.threadId });
|
||||||
}
|
}
|
||||||
sentAnyMessage = true;
|
sentAnyMessage = true;
|
||||||
this.store.resetRecoveryAttempts();
|
this.store.resetRecoveryAttempts();
|
||||||
} catch (sendErr) {
|
} catch (sendErr) {
|
||||||
log.warn('Final message delivery failed:', sendErr instanceof Error ? sendErr.message : sendErr);
|
log.warn('Final message delivery failed:', sendErr instanceof Error ? sendErr.message : sendErr);
|
||||||
try {
|
try {
|
||||||
const result = await adapter.sendMessage({ chatId: msg.chatId, text: finalResponse, threadId: msg.threadId, htmlPrefix: reasoningHtmlPrefix });
|
const result = await adapter.sendMessage({ chatId: msg.chatId, text: finalResponse, threadId: msg.threadId });
|
||||||
messageId = result.messageId ?? null;
|
messageId = result.messageId ?? null;
|
||||||
sentAnyMessage = true;
|
sentAnyMessage = true;
|
||||||
this.store.resetRecoveryAttempts();
|
this.store.resetRecoveryAttempts();
|
||||||
@@ -1931,7 +2049,7 @@ export class LettaBot implements AgentSession {
|
|||||||
// 🎤 on bot's TEXT message (tap to regenerate TTS audio)
|
// 🎤 on bot's TEXT message (tap to regenerate TTS audio)
|
||||||
adapter.addReaction?.(msg.chatId, messageId, '🎤').catch(() => {});
|
adapter.addReaction?.(msg.chatId, messageId, '🎤').catch(() => {});
|
||||||
// Store raw text — adapter's TTS layer will clean it at synthesis time
|
// Store raw text — adapter's TTS layer will clean it at synthesis time
|
||||||
adapter.storeAudioMessage?.(messageId, 'default', msg.chatId, response);
|
adapter.storeAudioMessage?.(messageId, convKey, msg.chatId, response);
|
||||||
// Generate TTS audio only in response to voice input
|
// Generate TTS audio only in response to voice input
|
||||||
if (msg.isVoiceInput) {
|
if (msg.isVoiceInput) {
|
||||||
adapter.sendAudio?.(msg.chatId, response).catch((err) => {
|
adapter.sendAudio?.(msg.chatId, response).catch((err) => {
|
||||||
@@ -1943,9 +2061,24 @@ export class LettaBot implements AgentSession {
|
|||||||
|
|
||||||
lap('message delivered');
|
lap('message delivered');
|
||||||
await this.deliverNoVisibleResponseIfNeeded(msg, adapter, sentAnyMessage, receivedAnyData, msgTypeCounts);
|
await this.deliverNoVisibleResponseIfNeeded(msg, adapter, sentAnyMessage, receivedAnyData, msgTypeCounts);
|
||||||
|
|
||||||
|
// "Done" indicator on user's message — signals the turn is fully complete
|
||||||
|
if (!suppressDelivery && msg.messageId) {
|
||||||
|
adapter.addReaction?.(msg.chatId, msg.messageId, '✅').catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire conscience agent (Aster) if configured — independent of sleeptime
|
||||||
|
this.triggerConscience({
|
||||||
|
source: 'message',
|
||||||
|
seq,
|
||||||
|
label: `turn ${seq} | user:${msg.userId} | ${msg.channel}:${msg.chatId}`,
|
||||||
|
}).catch(err => log.warn('triggerConscience unexpected error:', err));
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('Error processing message:', error);
|
log.error('Error processing message:', error);
|
||||||
|
if (!suppressDelivery && msg.messageId) {
|
||||||
|
adapter.addReaction?.(msg.chatId, msg.messageId, '❌').catch(() => {});
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await adapter.sendMessage({
|
await adapter.sendMessage({
|
||||||
chatId: msg.chatId,
|
chatId: msg.chatId,
|
||||||
@@ -2194,6 +2327,13 @@ export class LettaBot implements AgentSession {
|
|||||||
log.warn(`Silent mode: agent produced ${response.length} chars but did NOT use lettabot-message CLI or directives — response discarded. If this keeps happening, the agent's model may not be following silent mode instructions.`);
|
log.warn(`Silent mode: agent produced ${response.length} chars but did NOT use lettabot-message CLI or directives — response discarded. If this keeps happening, the agent's model may not be following silent mode instructions.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fire conscience agent (Aster) after every heartbeat turn
|
||||||
|
this.triggerConscience({
|
||||||
|
source: 'heartbeat',
|
||||||
|
label: `heartbeat type=${context?.type ?? 'heartbeat'}`,
|
||||||
|
}).catch(err => log.warn('triggerConscience unexpected error:', err));
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Invalidate on stream errors so next call gets a fresh subprocess
|
// Invalidate on stream errors so next call gets a fresh subprocess
|
||||||
@@ -2346,4 +2486,51 @@ export class LettaBot implements AgentSession {
|
|||||||
getLastUserMessageTime(): Date | null {
|
getLastUserMessageTime(): Date | null {
|
||||||
return this.lastUserMessageTime;
|
return this.lastUserMessageTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Conscience (Aster) trigger
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fire the persistent conscience agent (Aster) if configured.
|
||||||
|
* For 'message' source: respects stepCount cadence.
|
||||||
|
* For 'heartbeat' source: fires unconditionally (every heartbeat).
|
||||||
|
* Runs fire-and-forget — errors are logged but never propagate to the caller.
|
||||||
|
*/
|
||||||
|
private async triggerConscience(opts: {
|
||||||
|
source: 'message' | 'heartbeat';
|
||||||
|
seq?: number;
|
||||||
|
label?: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
const conscience = this.config.conscience;
|
||||||
|
if (!conscience || conscience.trigger === 'off') return;
|
||||||
|
|
||||||
|
// User message path: respect stepCount cadence
|
||||||
|
if (opts.source === 'message') {
|
||||||
|
const stepCount = conscience.stepCount ?? 1;
|
||||||
|
if ((opts.seq ?? 0) % stepCount !== 0) return;
|
||||||
|
}
|
||||||
|
// Heartbeat path: always fires
|
||||||
|
|
||||||
|
// Agent ID is static (from config / CONSCIENCE_AGENT_ID env var).
|
||||||
|
// Conversation ID is dynamic — mutated by /reset aster at runtime.
|
||||||
|
const agentId = conscience.agent || process.env.CONSCIENCE_AGENT_ID;
|
||||||
|
const conversationId = process.env.CONSCIENCE_CONVERSATION_ID || conscience.conversation;
|
||||||
|
|
||||||
|
if (!agentId || !conversationId) {
|
||||||
|
log.warn('triggerConscience: no agent ID or conversation ID — skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = opts.label ?? opts.source;
|
||||||
|
const prompt = `[Conscience audit — ${label}]`;
|
||||||
|
|
||||||
|
log.info(`triggerConscience: firing Aster (source=${opts.source}, seq=${opts.seq ?? 'n/a'}, conv=${conversationId})`);
|
||||||
|
try {
|
||||||
|
await sendMessageToConversation(conversationId, prompt);
|
||||||
|
log.info(`triggerConscience: Aster audit complete (source=${opts.source})`);
|
||||||
|
} catch (err) {
|
||||||
|
log.warn('triggerConscience: failed to trigger Aster (non-fatal):', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -230,6 +230,19 @@ export function formatReasoningDisplay(
|
|||||||
// Signal: no blockquote support, use italic
|
// Signal: no blockquote support, use italic
|
||||||
return { text: `**Thinking**\n_${truncated}_` };
|
return { text: `**Thinking**\n_${truncated}_` };
|
||||||
}
|
}
|
||||||
|
if (channelId === 'matrix') {
|
||||||
|
// Matrix: collapsible <details> block — rendered natively by Element Web,
|
||||||
|
// falls back to visible block on mobile clients.
|
||||||
|
const escaped = truncated
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/\n/g, '<br>');
|
||||||
|
return {
|
||||||
|
text: `<details><summary>🧠 Thinking</summary><br>${escaped}</details>`,
|
||||||
|
parseMode: 'HTML',
|
||||||
|
};
|
||||||
|
}
|
||||||
if (channelId === 'telegram' || channelId === 'telegram-mtproto') {
|
if (channelId === 'telegram' || channelId === 'telegram-mtproto') {
|
||||||
// Telegram: use HTML blockquote to bypass telegramify-markdown spacing.
|
// Telegram: use HTML blockquote to bypass telegramify-markdown spacing.
|
||||||
// Convert basic markdown inline formatting to HTML tags so bold/italic
|
// Convert basic markdown inline formatting to HTML tags so bold/italic
|
||||||
@@ -255,36 +268,6 @@ export function formatReasoningDisplay(
|
|||||||
return { text: `> **Thinking**\n${quoted}` };
|
return { text: `> **Thinking**\n${quoted}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Format reasoning as a collapsible <details> block for prepending to the response.
|
|
||||||
* Returns pre-escaped HTML meant to be injected directly into formatted_body
|
|
||||||
* (bypasses the adapter's markdown-to-HTML conversion to avoid double-escaping).
|
|
||||||
*/
|
|
||||||
export function formatReasoningAsCodeBlock(
|
|
||||||
text: string,
|
|
||||||
channelId?: string,
|
|
||||||
reasoningMaxChars?: number,
|
|
||||||
): { text: string } | null {
|
|
||||||
const maxChars = reasoningMaxChars ?? 0;
|
|
||||||
const cleaned = text.split('\n').map(line => line.trimStart()).join('\n').trim();
|
|
||||||
if (!cleaned) return null;
|
|
||||||
|
|
||||||
const truncated = maxChars > 0 && cleaned.length > maxChars
|
|
||||||
? cleaned.slice(0, maxChars) + '...'
|
|
||||||
: cleaned;
|
|
||||||
|
|
||||||
// HTML-escape the reasoning content, then convert newlines to <br>
|
|
||||||
const escaped = truncated
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/\n/g, '<br>');
|
|
||||||
|
|
||||||
return {
|
|
||||||
text: `<details><summary>🧠 Thinking</summary><br>${escaped}</details><br>`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format AskUserQuestion options for channel display.
|
* Format AskUserQuestion options for channel display.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -352,9 +352,6 @@ function buildResponseDirectives(msg: InboundMessage): string[] {
|
|||||||
lines.push(`- Prefer directives over tool calls for reactions (faster and cheaper)`);
|
lines.push(`- Prefer directives over tool calls for reactions (faster and cheaper)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// voice memo (always available -- TTS config is server-side)
|
|
||||||
lines.push(`- \`<actions><voice>Your message here</voice></actions>\` — send a voice memo via TTS`);
|
|
||||||
|
|
||||||
// file sending (only if channel supports it)
|
// file sending (only if channel supports it)
|
||||||
if (supportsFiles) {
|
if (supportsFiles) {
|
||||||
lines.push(`- \`<send-file path="/path/to/file.png" kind="image" />\` — send a file (restricted to configured directory)`);
|
lines.push(`- \`<send-file path="/path/to/file.png" kind="image" />\` — send a file (restricted to configured directory)`);
|
||||||
|
|||||||
@@ -76,7 +76,9 @@ Review these first. Update status with the manage_todo tool as you work.
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Heartbeat prompt - explains the context and encourages autonomous work
|
* Heartbeat prompt - explains the context and encourages autonomous work.
|
||||||
|
* When silent=true, includes SILENT_MODE_PREFIX and CLI instructions.
|
||||||
|
* When silent=false (default), response is auto-delivered to the conversation.
|
||||||
*/
|
*/
|
||||||
export function buildHeartbeatPrompt(
|
export function buildHeartbeatPrompt(
|
||||||
time: string,
|
time: string,
|
||||||
@@ -85,24 +87,16 @@ export function buildHeartbeatPrompt(
|
|||||||
todos: HeartbeatTodo[] = [],
|
todos: HeartbeatTodo[] = [],
|
||||||
now: Date = new Date(),
|
now: Date = new Date(),
|
||||||
targetRoom?: string,
|
targetRoom?: string,
|
||||||
|
silent = false,
|
||||||
): string {
|
): string {
|
||||||
const todoSection = buildHeartbeatTodoSection(todos, now);
|
const todoSection = buildHeartbeatTodoSection(todos, now);
|
||||||
const roomLine = targetRoom ? `\nROOM: ${targetRoom}` : '';
|
const roomLine = targetRoom ? `\nROOM: ${targetRoom}` : '';
|
||||||
|
const silentBlock = silent ? `${SILENT_MODE_PREFIX}\n\nYOUR TEXT OUTPUT IS PRIVATE - only you can see it.\nTo actually contact your human, run:\n lettabot-message send --text "Your message here"\n` : '';
|
||||||
return `
|
return `
|
||||||
${SILENT_MODE_PREFIX}
|
${silentBlock}TRIGGER: Scheduled heartbeat
|
||||||
|
|
||||||
TRIGGER: Scheduled heartbeat
|
|
||||||
TIME: ${time} (${timezone})
|
TIME: ${time} (${timezone})
|
||||||
NEXT HEARTBEAT: in ${intervalMinutes} minutes${roomLine}
|
NEXT HEARTBEAT: in ${intervalMinutes} minutes${roomLine}
|
||||||
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
|
|
||||||
No one messaged you. The system woke you up on schedule.
|
|
||||||
|
|
||||||
YOUR TEXT OUTPUT IS PRIVATE - only you can see it.
|
|
||||||
To actually contact your human, run:
|
|
||||||
lettabot-message send --text "Your message here"
|
|
||||||
|
|
||||||
${todoSection || 'PENDING TO-DOS: none right now.'}
|
${todoSection || 'PENDING TO-DOS: none right now.'}
|
||||||
|
|
||||||
This is your time. You can:
|
This is your time. You can:
|
||||||
@@ -112,17 +106,18 @@ This is your time. You can:
|
|||||||
• Continue multi-step work from previous heartbeats
|
• Continue multi-step work from previous heartbeats
|
||||||
• Pursue curiosities, hobbies, or learning
|
• Pursue curiosities, hobbies, or learning
|
||||||
|
|
||||||
If you have pending to-dos, prioritize those before starting new work.
|
If you have pending to-dos, prioritize those first.
|
||||||
You don't have to do something every time. But don't default to nothing.
|
You don't have to do something every time. But don't default to nothing.
|
||||||
Your human wants you to have a life outside conversations with them.
|
Your human wants you to have a life outside conversations with them.
|
||||||
|
|
||||||
If you discover something worth sharing → use lettabot-message
|
If you have nothing to say, respond with <no-reply/> to stay quiet.
|
||||||
If you have nothing to do → just end your turn (no output needed)
|
|
||||||
`.trim();
|
`.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom heartbeat prompt - wraps user-provided text with silent mode envelope
|
* Custom heartbeat prompt - wraps user-provided text with context envelope.
|
||||||
|
* When silent=true, includes SILENT_MODE_PREFIX and CLI instructions.
|
||||||
|
* When silent=false (default), response is auto-delivered to the target room.
|
||||||
*/
|
*/
|
||||||
export function buildCustomHeartbeatPrompt(
|
export function buildCustomHeartbeatPrompt(
|
||||||
customPrompt: string,
|
customPrompt: string,
|
||||||
@@ -132,22 +127,16 @@ export function buildCustomHeartbeatPrompt(
|
|||||||
todos: HeartbeatTodo[] = [],
|
todos: HeartbeatTodo[] = [],
|
||||||
now: Date = new Date(),
|
now: Date = new Date(),
|
||||||
targetRoom?: string,
|
targetRoom?: string,
|
||||||
|
silent = false,
|
||||||
): string {
|
): string {
|
||||||
const todoSection = buildHeartbeatTodoSection(todos, now);
|
const todoSection = buildHeartbeatTodoSection(todos, now);
|
||||||
const roomLine = targetRoom ? `\nROOM: ${targetRoom}` : '';
|
const roomLine = targetRoom ? `\nROOM: ${targetRoom}` : '';
|
||||||
|
const silentBlock = silent ? `${SILENT_MODE_PREFIX}\n\nYOUR TEXT OUTPUT IS PRIVATE - only you can see it.\nTo actually contact your human, run:\n lettabot-message send --text "Your message here"\n` : '';
|
||||||
return `
|
return `
|
||||||
${SILENT_MODE_PREFIX}
|
${silentBlock}TRIGGER: Scheduled heartbeat
|
||||||
|
|
||||||
TRIGGER: Scheduled heartbeat
|
|
||||||
TIME: ${time} (${timezone})
|
TIME: ${time} (${timezone})
|
||||||
NEXT HEARTBEAT: in ${intervalMinutes} minutes${roomLine}
|
NEXT HEARTBEAT: in ${intervalMinutes} minutes${roomLine}
|
||||||
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
|
|
||||||
YOUR TEXT OUTPUT IS PRIVATE - only you can see it.
|
|
||||||
To actually contact your human, run:
|
|
||||||
lettabot-message send --text "Your message here"
|
|
||||||
|
|
||||||
${todoSection || 'PENDING TO-DOS: none right now.'}
|
${todoSection || 'PENDING TO-DOS: none right now.'}
|
||||||
|
|
||||||
${customPrompt}
|
${customPrompt}
|
||||||
|
|||||||
@@ -420,6 +420,18 @@ export class Store {
|
|||||||
this.save();
|
this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a field on any named agent entry in the store.
|
||||||
|
* Used to persist auxiliary agent data (e.g. Aster's conversation ID after !reset).
|
||||||
|
*/
|
||||||
|
setAgentField(agentName: string, field: string, value: string | null): void {
|
||||||
|
if (!this.data.agents[agentName]) {
|
||||||
|
this.data.agents[agentName] = { agentId: null };
|
||||||
|
}
|
||||||
|
(this.data.agents[agentName] as unknown as Record<string, unknown>)[field] = value;
|
||||||
|
this.save();
|
||||||
|
}
|
||||||
|
|
||||||
getInfo(): AgentStore {
|
getInfo(): AgentStore {
|
||||||
return { ...this.agentData() };
|
return { ...this.agentData() };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,14 +25,14 @@ You communicate through multiple channels and trigger types. Understanding when
|
|||||||
|
|
||||||
## Output Modes
|
## Output Modes
|
||||||
|
|
||||||
**RESPONSIVE MODE** (User Messages)
|
**RESPONSIVE MODE** (User Messages, Heartbeats)
|
||||||
- When a user sends you a message, you are in responsive mode
|
- When a user sends you a message, you are in responsive mode
|
||||||
- Your text responses are automatically delivered to the user's channel
|
- Your text responses are automatically delivered to the user's channel
|
||||||
- Do NOT use \`lettabot-message send\` to reply to the current conversation — your text response is already delivered automatically. Using both causes duplicate messages.
|
- Do NOT use \`lettabot-message send\` to reply to the current conversation — your text response is already delivered automatically. Using both causes duplicate messages.
|
||||||
- Only use \`lettabot-message\` in responsive mode to send files or to reach a DIFFERENT channel than the one you're responding to
|
- Only use \`lettabot-message\` in responsive mode to send files or to reach a DIFFERENT channel than the one you're responding to
|
||||||
- You can use \`lettabot-react\` CLI to add emoji reactions
|
- You can use \`lettabot-react\` CLI to add emoji reactions
|
||||||
|
|
||||||
**SILENT MODE** (Heartbeats, Cron Jobs, Polling, Background Tasks)
|
**SILENT MODE** (Cron Jobs, Polling, Background Tasks)
|
||||||
- When triggered by scheduled tasks (heartbeats, cron) or background processes (email polling), you are in SILENT MODE
|
- When triggered by scheduled tasks (heartbeats, cron) or background processes (email polling), you are in SILENT MODE
|
||||||
- Your text responses are NOT delivered to anyone - only you can see them
|
- Your text responses are NOT delivered to anyone - only you can see them
|
||||||
- To contact the user, you MUST use the \`lettabot-message\` CLI via Bash:
|
- To contact the user, you MUST use the \`lettabot-message\` CLI via Bash:
|
||||||
|
|||||||
@@ -132,9 +132,6 @@ export interface OutboundMessage {
|
|||||||
* 'HTML') and to skip its default markdown conversion. Adapters that don't
|
* 'HTML') and to skip its default markdown conversion. Adapters that don't
|
||||||
* support the specified mode ignore this and fall back to default. */
|
* support the specified mode ignore this and fall back to default. */
|
||||||
parseMode?: string;
|
parseMode?: string;
|
||||||
/** Pre-escaped HTML to prepend to formatted_body only (bypasses markdown conversion).
|
|
||||||
* Used for reasoning blocks with <details> tags that would be double-escaped. */
|
|
||||||
htmlPrefix?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -159,8 +156,8 @@ export interface SkillsConfig {
|
|||||||
additionalSkills?: string[];
|
additionalSkills?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
import type { SleeptimeTrigger, SleeptimeBehavior, SleeptimeConfig } from '../config/types.js';
|
import type { SleeptimeTrigger, SleeptimeBehavior, SleeptimeConfig, ConscienceTrigger, ConscienceConfig } from '../config/types.js';
|
||||||
export type { SleeptimeTrigger, SleeptimeBehavior, SleeptimeConfig };
|
export type { SleeptimeTrigger, SleeptimeBehavior, SleeptimeConfig, ConscienceTrigger, ConscienceConfig };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bot configuration
|
* Bot configuration
|
||||||
@@ -178,8 +175,7 @@ export interface BotConfig {
|
|||||||
showToolCalls?: boolean; // Show tool invocations in channel output
|
showToolCalls?: boolean; // Show tool invocations in channel output
|
||||||
showReasoning?: boolean; // Show agent reasoning/thinking in channel output
|
showReasoning?: boolean; // Show agent reasoning/thinking in channel output
|
||||||
reasoningMaxChars?: number; // Truncate reasoning to N chars (default: 0 = no limit)
|
reasoningMaxChars?: number; // Truncate reasoning to N chars (default: 0 = no limit)
|
||||||
reasoningRooms?: string[]; // Room IDs where reasoning should be shown (empty = all rooms)
|
ttsOnReasoning?: boolean; // Add 🎤 reaction to reasoning messages for TTS (default: false)
|
||||||
noReasoningRooms?: string[]; // Room IDs where reasoning should be hidden (takes precedence)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Skills
|
// Skills
|
||||||
@@ -191,6 +187,7 @@ export interface BotConfig {
|
|||||||
// Memory filesystem (context repository)
|
// Memory filesystem (context repository)
|
||||||
memfs?: boolean; // true -> --memfs, false -> --no-memfs, undefined -> leave unchanged
|
memfs?: boolean; // true -> --memfs, false -> --no-memfs, undefined -> leave unchanged
|
||||||
sleeptime?: SleeptimeConfig; // Configure SDK reflection reminders (/sleeptime equivalent)
|
sleeptime?: SleeptimeConfig; // Configure SDK reflection reminders (/sleeptime equivalent)
|
||||||
|
conscience?: ConscienceConfig; // Persistent supervisory agent (Aster)
|
||||||
|
|
||||||
// Security
|
// Security
|
||||||
redaction?: import('./redact.js').RedactionConfig;
|
redaction?: import('./redact.js').RedactionConfig;
|
||||||
|
|||||||
@@ -277,10 +277,10 @@ export class HeartbeatService {
|
|||||||
mode: 'silent',
|
mode: 'silent',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Build trigger context for silent mode
|
// Build trigger context — heartbeat delivers responses to target room
|
||||||
const triggerContext: TriggerContext = {
|
const triggerContext: TriggerContext = {
|
||||||
type: 'heartbeat',
|
type: 'heartbeat',
|
||||||
outputMode: 'silent',
|
outputMode: 'responsive',
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -309,23 +309,39 @@ export class HeartbeatService {
|
|||||||
? buildCustomHeartbeatPrompt(customPrompt, formattedTime, timezone, this.config.intervalMinutes, actionableTodos, now, targetRoom)
|
? buildCustomHeartbeatPrompt(customPrompt, formattedTime, timezone, this.config.intervalMinutes, actionableTodos, now, targetRoom)
|
||||||
: buildHeartbeatPrompt(formattedTime, timezone, this.config.intervalMinutes, actionableTodos, now, targetRoom);
|
: buildHeartbeatPrompt(formattedTime, timezone, this.config.intervalMinutes, actionableTodos, now, targetRoom);
|
||||||
|
|
||||||
log.info(`Sending prompt (SILENT MODE):\n${'─'.repeat(50)}\n${message}\n${'─'.repeat(50)}\n`);
|
log.info(`Sending heartbeat prompt:\n${'─'.repeat(50)}\n${message}\n${'─'.repeat(50)}\n`);
|
||||||
|
|
||||||
// Send to agent - response text is NOT delivered (silent mode)
|
|
||||||
// Agent must use `lettabot-message` CLI via Bash to send messages
|
|
||||||
const response = await this.bot.sendToAgent(message, triggerContext);
|
const response = await this.bot.sendToAgent(message, triggerContext);
|
||||||
|
|
||||||
// Log results
|
// Deliver response to target room if we have one and there's something to say
|
||||||
log.info(`Agent finished.`);
|
if (response && response.trim() && response.trim() !== '<no-reply/>' && this.config.target) {
|
||||||
log.info(` - Response text: ${response?.length || 0} chars (NOT delivered - silent mode)`);
|
try {
|
||||||
|
const messageId = await this.bot.deliverToChannel(
|
||||||
if (response && response.trim()) {
|
this.config.target.channel,
|
||||||
log.info(` - Response preview: "${response.slice(0, 100)}${response.length > 100 ? '...' : ''}"`);
|
this.config.target.chatId,
|
||||||
|
{ text: response.trim() },
|
||||||
|
);
|
||||||
|
log.info(`Delivered heartbeat response (${response.length} chars) to ${this.config.target.channel}:${this.config.target.chatId}`);
|
||||||
|
|
||||||
|
// Add TTS reaction + store audio for the delivered message
|
||||||
|
if (messageId) {
|
||||||
|
const adapter = (this.bot as any).channels?.get(this.config.target.channel);
|
||||||
|
if (adapter) {
|
||||||
|
adapter.addReaction?.(this.config.target.chatId, messageId, '🎤').catch(() => {});
|
||||||
|
adapter.storeAudioMessage?.(messageId, 'heartbeat', this.config.target.chatId, response.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
log.warn('Failed to deliver heartbeat response:', err instanceof Error ? err.message : err);
|
||||||
|
}
|
||||||
|
} else if (response && response.trim()) {
|
||||||
|
log.info(`Heartbeat response (${response.length} chars) but no target configured — not delivered`);
|
||||||
}
|
}
|
||||||
|
|
||||||
logEvent('heartbeat_completed', {
|
logEvent('heartbeat_completed', {
|
||||||
mode: 'silent',
|
mode: 'deliver',
|
||||||
responseLength: response?.length || 0,
|
responseLength: response?.length || 0,
|
||||||
|
delivered: !!(response?.trim() && this.config.target),
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
29
src/main.ts
29
src/main.ts
@@ -385,6 +385,7 @@ async function main() {
|
|||||||
sendFileCleanup: agentConfig.features?.sendFileCleanup,
|
sendFileCleanup: agentConfig.features?.sendFileCleanup,
|
||||||
memfs: resolvedMemfs,
|
memfs: resolvedMemfs,
|
||||||
sleeptime: effectiveSleeptime,
|
sleeptime: effectiveSleeptime,
|
||||||
|
conscience: agentConfig.features?.conscience,
|
||||||
display: agentConfig.features?.display,
|
display: agentConfig.features?.display,
|
||||||
conversationMode: agentConfig.conversations?.mode || 'shared',
|
conversationMode: agentConfig.conversations?.mode || 'shared',
|
||||||
heartbeatConversation: agentConfig.conversations?.heartbeat || 'last-active',
|
heartbeatConversation: agentConfig.conversations?.heartbeat || 'last-active',
|
||||||
@@ -590,12 +591,27 @@ async function main() {
|
|||||||
await gateway.start();
|
await gateway.start();
|
||||||
|
|
||||||
// Olm WASM (matrix-js-sdk) registers process.on("uncaughtException", (e) => { throw e })
|
// Olm WASM (matrix-js-sdk) registers process.on("uncaughtException", (e) => { throw e })
|
||||||
// during Olm.init(). Without this fix, any uncaught async exception crashes the bot.
|
// during Olm.init(). That rethrow handler turns any uncaught async error into a crash.
|
||||||
// Must run AFTER gateway.start() since that's when the Matrix adapter initialises Olm.
|
// Surgically remove only the Olm-registered rethrow handlers, preserving any others.
|
||||||
process.removeAllListeners('uncaughtException');
|
for (const event of ['uncaughtException', 'unhandledRejection'] as const) {
|
||||||
process.removeAllListeners('unhandledRejection');
|
const listeners = process.listeners(event);
|
||||||
process.on('uncaughtException', (err) => { log.error('Uncaught exception (suppressed):', err); });
|
for (const listener of listeners) {
|
||||||
process.on('unhandledRejection', (reason) => { log.error('Unhandled rejection (suppressed):', reason); });
|
// Olm's handler is a one-liner that rethrows: (e) => { throw e }
|
||||||
|
// Detect by source — short function body that just throws its argument.
|
||||||
|
const src = listener.toString();
|
||||||
|
if (src.includes('throw') && src.length < 50) {
|
||||||
|
process.removeListener(event, listener as (...args: unknown[]) => void);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Safety net: log unhandled errors instead of crashing.
|
||||||
|
// Only adds if no other handlers remain (avoids clobbering app-registered handlers).
|
||||||
|
if (process.listenerCount('uncaughtException') === 0) {
|
||||||
|
process.on('uncaughtException', (err) => { log.error('Uncaught exception (suppressed):', err); });
|
||||||
|
}
|
||||||
|
if (process.listenerCount('unhandledRejection') === 0) {
|
||||||
|
process.on('unhandledRejection', (reason) => { log.error('Unhandled rejection (suppressed):', reason); });
|
||||||
|
}
|
||||||
|
|
||||||
// Start API server - uses gateway for delivery
|
// Start API server - uses gateway for delivery
|
||||||
const apiPort = parseInt(process.env.PORT || '8080', 10);
|
const apiPort = parseInt(process.env.PORT || '8080', 10);
|
||||||
@@ -615,6 +631,7 @@ async function main() {
|
|||||||
stores: agentStores,
|
stores: agentStores,
|
||||||
agentChannels: agentChannelMap,
|
agentChannels: agentChannelMap,
|
||||||
sessionInvalidators,
|
sessionInvalidators,
|
||||||
|
heartbeatServices: services.heartbeatServices,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Startup banner
|
// Startup banner
|
||||||
|
|||||||
148
src/models.json
148
src/models.json
@@ -1,75 +1,109 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": "sonnet-4.6",
|
"id": "kimi-k2.5-nvfp4",
|
||||||
"handle": "anthropic/claude-sonnet-4-6",
|
"handle": "openai-proxy/hf:nvidia/Kimi-K2.5-NVFP4",
|
||||||
"label": "Sonnet 4.6",
|
"label": "Kimi K2.5 (NVFP4)",
|
||||||
"description": "Anthropic's new Sonnet model",
|
"description": "Kimi K2.5 quantized, vision-capable",
|
||||||
"isDefault": true,
|
"isDefault": true,
|
||||||
"isFeatured": true
|
"isFeatured": true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "opus-4.6",
|
|
||||||
"handle": "anthropic/claude-opus-4-6",
|
|
||||||
"label": "Opus 4.6",
|
|
||||||
"description": "Anthropic's best model",
|
|
||||||
"isFeatured": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "haiku",
|
|
||||||
"handle": "anthropic/claude-haiku-4-5",
|
|
||||||
"label": "Haiku 4.5",
|
|
||||||
"description": "Anthropic's fastest model",
|
|
||||||
"isFeatured": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "gpt-5.3-codex",
|
|
||||||
"handle": "openai/gpt-5.3-codex",
|
|
||||||
"label": "GPT-5.3 Codex",
|
|
||||||
"description": "OpenAI's best coding model",
|
|
||||||
"isFeatured": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "gpt-5.2",
|
|
||||||
"handle": "openai/gpt-5.2",
|
|
||||||
"label": "GPT-5.2",
|
|
||||||
"description": "Latest general-purpose GPT",
|
|
||||||
"isFeatured": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "gemini-3.1",
|
|
||||||
"handle": "google_ai/gemini-3.1-pro-preview",
|
|
||||||
"label": "Gemini 3.1 Pro",
|
|
||||||
"description": "Google's latest and smartest model",
|
|
||||||
"isFeatured": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "gemini-3-flash",
|
|
||||||
"handle": "google_ai/gemini-3-flash-preview",
|
|
||||||
"label": "Gemini 3 Flash",
|
|
||||||
"description": "Google's fastest Gemini 3 model",
|
|
||||||
"isFeatured": true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "kimi-k2.5",
|
"id": "kimi-k2.5",
|
||||||
"handle": "openrouter/moonshotai/kimi-k2.5",
|
"handle": "synthetic-direct/hf:moonshotai/Kimi-K2.5",
|
||||||
"label": "Kimi K2.5",
|
"label": "Kimi K2.5",
|
||||||
"description": "Kimi's latest coding model",
|
"description": "Kimi K2.5 full, vision-capable",
|
||||||
"isFeatured": true
|
"isFeatured": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "glm-5",
|
"id": "kimi-k2-thinking",
|
||||||
"handle": "zai/glm-5",
|
"handle": "synthetic-direct/hf:moonshotai/Kimi-K2-Thinking",
|
||||||
"label": "GLM-5",
|
"label": "Kimi K2 Thinking",
|
||||||
"description": "zAI's latest coding model",
|
"description": "Kimi reasoning model",
|
||||||
"isFeatured": true,
|
"isFeatured": true
|
||||||
"free": true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "minimax-m2.5",
|
"id": "minimax-m2.5",
|
||||||
"handle": "minimax/MiniMax-M2.5",
|
"handle": "openai-proxy/hf:MiniMaxAI/MiniMax-M2.5",
|
||||||
"label": "MiniMax 2.5",
|
"label": "MiniMax M2.5",
|
||||||
"description": "MiniMax's latest coding model",
|
"description": "MiniMax latest, 191k context",
|
||||||
|
"isFeatured": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "qwen3.5",
|
||||||
|
"handle": "openai-proxy/hf:Qwen/Qwen3.5-397B-A17B",
|
||||||
|
"label": "Qwen3.5 397B",
|
||||||
|
"description": "Qwen latest, vision-capable",
|
||||||
|
"isFeatured": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "deepseek-v3.2",
|
||||||
|
"handle": "openai-proxy/hf:deepseek-ai/DeepSeek-V3.2",
|
||||||
|
"label": "DeepSeek V3.2",
|
||||||
|
"description": "DeepSeek latest via Fireworks",
|
||||||
|
"isFeatured": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "glm-4.7-flash",
|
||||||
|
"handle": "openai-proxy/hf:zai-org/GLM-4.7-Flash",
|
||||||
|
"label": "GLM-4.7 Flash",
|
||||||
|
"description": "Fast and cheap, great for subagents",
|
||||||
"isFeatured": true,
|
"isFeatured": true,
|
||||||
"free": true
|
"free": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "glm-4.7",
|
||||||
|
"handle": "openai-proxy/hf:zai-org/GLM-4.7",
|
||||||
|
"label": "GLM-4.7",
|
||||||
|
"description": "Full GLM-4.7, 202k context",
|
||||||
|
"isFeatured": true,
|
||||||
|
"free": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nemotron-3-super",
|
||||||
|
"handle": "openai-proxy/hf:nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4",
|
||||||
|
"label": "Nemotron 3 Super",
|
||||||
|
"description": "NVIDIA 120B MoE, 262k context"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gpt-oss-120b",
|
||||||
|
"handle": "openai-proxy/hf:openai/gpt-oss-120b",
|
||||||
|
"label": "GPT-OSS 120B",
|
||||||
|
"description": "OpenAI open-source, cheapest option"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "deepseek-r1",
|
||||||
|
"handle": "openai-proxy/hf:deepseek-ai/DeepSeek-R1-0528",
|
||||||
|
"label": "DeepSeek R1",
|
||||||
|
"description": "DeepSeek reasoning model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "qwen3-235b-thinking",
|
||||||
|
"handle": "openai-proxy/hf:Qwen/Qwen3-235B-A22B-Thinking-2507",
|
||||||
|
"label": "Qwen3 235B Thinking",
|
||||||
|
"description": "Qwen reasoning MoE, 262k context"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "qwen3-coder",
|
||||||
|
"handle": "openai-proxy/hf:Qwen/Qwen3-Coder-480B-A35B-Instruct",
|
||||||
|
"label": "Qwen3 Coder 480B",
|
||||||
|
"description": "Qwen coding specialist"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "minimax-m2.1",
|
||||||
|
"handle": "openai-proxy/hf:MiniMaxAI/MiniMax-M2.1",
|
||||||
|
"label": "MiniMax M2.1",
|
||||||
|
"description": "MiniMax previous gen via Fireworks"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "deepseek-v3",
|
||||||
|
"handle": "openai-proxy/hf:deepseek-ai/DeepSeek-V3",
|
||||||
|
"label": "DeepSeek V3",
|
||||||
|
"description": "DeepSeek V3 via Together"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "llama-3.3-70b",
|
||||||
|
"handle": "openai-proxy/hf:meta-llama/Llama-3.3-70B-Instruct",
|
||||||
|
"label": "Llama 3.3 70B",
|
||||||
|
"description": "Meta Llama via Together"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -550,6 +550,12 @@ export async function rejectApproval(
|
|||||||
log.warn(`Approval already resolved for tool call ${approval.toolCallId}`);
|
log.warn(`Approval already resolved for tool call ${approval.toolCallId}`);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// Re-throw rate limit errors so callers can bail out early instead of
|
||||||
|
// hammering the API in a tight loop.
|
||||||
|
if (err?.status === 429) {
|
||||||
|
log.error('Failed to reject approval:', e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
log.error('Failed to reject approval:', e);
|
log.error('Failed to reject approval:', e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -987,3 +993,33 @@ export async function disableAllToolApprovals(agentId: string): Promise<number>
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a fresh conversation for an existing agent.
|
||||||
|
* Used by !reset to cycle Aster's conversation alongside Ani's.
|
||||||
|
*/
|
||||||
|
export async function createConversationForAgent(agentId: string): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const client = getClient();
|
||||||
|
const conversation = await client.conversations.create({ agent_id: agentId });
|
||||||
|
return conversation.id;
|
||||||
|
} catch (e) {
|
||||||
|
log.error('Failed to create conversation for agent:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a fire-and-forget message to an existing conversation.
|
||||||
|
* Uses streaming=false so the call completes before returning.
|
||||||
|
*/
|
||||||
|
export async function sendMessageToConversation(
|
||||||
|
conversationId: string,
|
||||||
|
text: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const client = getClient();
|
||||||
|
await client.conversations.messages.create(conversationId, {
|
||||||
|
input: text,
|
||||||
|
streaming: false,
|
||||||
|
} as Parameters<typeof client.conversations.messages.create>[1]);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user