Merge branch 'master' into nd/fix-first-profile-avatar

This commit is contained in:
Narasimha-sc committed 2026-10-22 16:32:48 +00:00
commit e40822e310
234 files changed
+73854 -1140

No files matched your search

+439 -8
View File
@@ -1,12 +1,10 @@
name: android-emu
# A placeholder, so that the Android app test workflow can be started by hand on a branch that has its full
# version: GitHub dispatches only workflows whose file is on the default branch. On master it does nothing.
on:
workflow_dispatch:
inputs:
claude:
description: Also run the Claude job
description: Also run the Claude job, which spends the Claude subscription's usage limits
type: boolean
default: false
backend:
@@ -14,17 +12,450 @@ on:
type: boolean
default: false
release:
description: Also test the latest release's onboarding
description: Also test the latest released app, to tell a branch's failure from the runner's
type: boolean
default: false
permissions:
contents: read
actions: read
concurrency:
group: android-emu-${{ github.ref }}
cancel-in-progress: true
env:
GHC_VER: 9.6.3
CABAL_VER: 3.10.2.0
JAVA_VER: 17
CORE_PKG: android-core-arm64-v8a.tar.gz
BACKEND_PKG: desktop-linux-backend.tar.gz
APK: android-foss-arm64-v8a-debug.apk
jobs:
placeholder:
plan:
name: choose jobs, find earlier builds
runs-on: ubuntu-24.04
timeout-minutes: 5
timeout-minutes: 10
outputs:
run-claude: ${{ steps.jobs.outputs.claude }}
run-backend: ${{ steps.jobs.outputs.backend }}
run-release: ${{ steps.jobs.outputs.release }}
claude-backend: ${{ steps.scope.outputs.backend }}
core: ${{ steps.names.outputs.core }}
core-run: ${{ steps.core.outputs.run-id }}
backend: ${{ steps.names.outputs.backend }}
backend-run: ${{ steps.backend.outputs.run-id }}
steps:
- name: Nothing to run here
run: echo "This branch has only the placeholder of android-emu; start it on a branch with the full workflow."
- name: Choose jobs
id: jobs
env:
CLAUDE: ${{ inputs.claude }}
BACKEND: ${{ inputs.backend }}
RELEASE: ${{ inputs.release }}
run: |
echo "claude=$CLAUDE" | tee -a "$GITHUB_OUTPUT"
echo "backend=$BACKEND" | tee -a "$GITHUB_OUTPUT"
echo "release=$RELEASE" | tee -a "$GITHUB_OUTPUT"
- name: Checkout code
uses: actions/checkout@v6
- name: Claude scope
id: scope
if: steps.jobs.outputs.claude == 'true'
env:
BACKEND: ${{ steps.jobs.outputs.backend }}
run: |
backend=$(sed -n 's/^backend: *//p' scripts/ci/android/claude-scope)
case "$backend" in
yes | no) echo "backend=$backend" | tee -a "$GITHUB_OUTPUT" ;;
*)
echo "::error::claude-scope must say backend: yes or backend: no"
exit 1
;;
esac
if [ "$backend" = yes ] && [ "$BACKEND" != true ]; then
echo "::error::claude-scope says the Claude session uses the test backend: ask for it too"
exit 1
fi
# artifacts are named after the sources they are built from, so a run uses one that an earlier run uploaded;
# the backend has desktop-linux's name and sources, so each workflow reuses the other's
- name: Artifact names
id: names
run: |
echo "core=android-core-${{ hashFiles('src/**', 'cabal.project', 'simplex-chat.cabal', 'PRIVACY.md', 'flake.nix', 'flake.lock', 'scripts/nix/**', 'scripts/ci/android/build-core.sh') }}" >> "$GITHUB_OUTPUT"
echo "backend=desktop-linux-backend-${{ hashFiles('src/**', 'apps/simplex-chat/**', 'apps/simplex-directory-service/Main.hs', 'apps/simplex-directory-service/src/**', 'cabal.project', 'simplex-chat.cabal', 'scripts/ci/desktop-linux/build-backend.sh') }}" >> "$GITHUB_OUTPUT"
- name: Find core library
id: core
env:
GH_TOKEN: ${{ github.token }}
run: scripts/ci/desktop-linux/earlier-run.sh "${{ steps.names.outputs.core }}"
- name: Find backend
id: backend
if: steps.jobs.outputs.backend == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: scripts/ci/desktop-linux/earlier-run.sh "${{ steps.names.outputs.backend }}"
backend:
name: test backend for the app
needs: plan
if: needs.plan.outputs.run-backend == 'true' && needs.plan.outputs.backend-run == ''
runs-on: ubuntu-24.04
timeout-minutes: 150
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Show runner
run: |
lsb_release -ds
nproc
free -h
df -h /
- name: Setup Haskell
uses: simplex-chat/setup-haskell-action@v2
with:
ghc-version: ${{ env.GHC_VER }}
cabal-version: ${{ env.CABAL_VER }}
- name: Install libraries
run: |
sudo apt-get update
sudo apt-get install -y libffi-dev libgmp-dev libnuma-dev libssl-dev zlib1g-dev
- name: Build backend
run: scripts/ci/desktop-linux/build-backend.sh ~/backend/${{ env.BACKEND_PKG }}
- name: Upload backend
uses: actions/upload-artifact@v7
with:
name: ${{ needs.plan.outputs.backend }}
path: ~/backend/${{ env.BACKEND_PKG }}
compression-level: 0
retention-days: 30
if-no-files-found: error
core:
name: core library for Android arm64
needs: plan
if: needs.plan.outputs.core-run == ''
runs-on: ubuntu-24.04
timeout-minutes: 150
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
filter: tree:0
- name: Show runner
run: |
lsb_release -ds
nproc
free -h
df -h /
- name: Build core library
run: scripts/ci/android/build-core.sh ~/core/${{ env.CORE_PKG }}
- name: Upload core library
uses: actions/upload-artifact@v7
with:
name: ${{ needs.plan.outputs.core }}
path: ~/core/${{ env.CORE_PKG }}
compression-level: 0
retention-days: 30
if-no-files-found: error
app:
name: Android app in the emulator
needs: [plan, core, backend]
# core and backend are skipped when an earlier run built them, and backend also when the run doesn't start it
if: ${{ !cancelled() && !failure() && needs.plan.result == 'success' }}
runs-on: ubuntu-24.04
timeout-minutes: 75
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
submodules: recursive
- name: Show runner
run: |
lsb_release -ds
nproc
free -h
df -h /
echo "ANDROID_HOME=$ANDROID_HOME"
ls "$ANDROID_HOME/platforms" "$ANDROID_HOME/build-tools" "$ANDROID_HOME/ndk" || true
# the emulator's qemu needs libpulse0 and libegl1; the backend's avatars and checks need the rest
- name: Install tools
run: |
sudo apt-get update
sudo apt-get install -y libpulse0 libegl1 imagemagick sqlite3 fonts-dejavu-core
dpkg-query -W libpulse0 libegl1 imagemagick sqlite3 fonts-dejavu-core
- name: Download core library
uses: actions/download-artifact@v8
with:
name: ${{ needs.plan.outputs.core }}
path: ~/core
run-id: ${{ needs.plan.outputs.core-run || github.run_id }}
github-token: ${{ github.token }}
- name: Install core library
id: core
run: scripts/ci/android/install-core.sh ~/core/${{ env.CORE_PKG }}
- name: Download backend
if: needs.plan.outputs.run-backend == 'true'
uses: actions/download-artifact@v8
with:
name: ${{ needs.plan.outputs.backend }}
path: ~/backend
run-id: ${{ needs.plan.outputs.backend-run || github.run_id }}
github-token: ${{ github.token }}
- name: Start test backend
id: backend
if: needs.plan.outputs.run-backend == 'true'
timeout-minutes: 30
run: scripts/ci/desktop-linux/backend.sh start ~/backend/${{ env.BACKEND_PKG }} "$RUNNER_TEMP/backend"
# the app and its tests are still built when the backend fails
- name: Setup Java
if: ${{ !cancelled() && steps.core.outcome == 'success' }}
uses: actions/setup-java@v5
with:
distribution: corretto
java-version: ${{ env.JAVA_VER }}
# the injected ABI builds only arm64-v8a, whose core the job has, and puts the APK under build/intermediates
- name: Build app
id: build
if: ${{ !cancelled() && steps.core.outcome == 'success' }}
working-directory: apps/multiplatform
run: |
./gradlew --no-daemon --console=plain -Psimplex.assets.dir=../../assets -Pandroid.injected.build.abi=arm64-v8a :android:assembleFossDebug
mkdir -p "$RUNNER_TEMP/apk"
cp "android/build/intermediates/apk/foss/debug/$APK" "$RUNNER_TEMP/apk"
ls -la "$RUNNER_TEMP/apk"
unzip -l "$RUNNER_TEMP/apk/$APK" | grep -E ' lib/'
- name: Upload app
if: ${{ !cancelled() && steps.build.outcome == 'success' && needs.plan.outputs.run-claude == 'true' }}
uses: actions/upload-artifact@v7
with:
name: android-app
path: ${{ runner.temp }}/apk/${{ env.APK }}
compression-level: 0
retention-days: 1
if-no-files-found: error
- name: Install emulator and system image
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
run: scripts/ci/android/install-sdk.sh "$RUNNER_TEMP/android-sdk"
- name: Boot emulator
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
run: scripts/ci/android/emulator.sh start "$RUNNER_TEMP/smoke-test"
# the test SMP server's addresses have the runner's 127.0.0.1, which adb reverse forwards from the emulator's
- name: Reach the test backend
id: reach
if: ${{ !cancelled() && steps.build.outcome == 'success' && steps.backend.outcome == 'success' }}
run: scripts/ci/android/emulator.sh reverse 5223
- name: Run smoke test
if: ${{ !cancelled() && steps.build.outcome == 'success' && steps.backend.outcome != 'failure' && steps.reach.outcome != 'failure' }}
env:
BACKEND: ${{ steps.reach.outcome }}
run: |
if [ "$BACKEND" = success ]; then
export NAMES_SERVER=$(cat "$RUNNER_TEMP/backend/names-server")
cases=backend
fi
scripts/ci/android/smoke-test.sh "$RUNNER_TEMP/apk/$APK" "$RUNNER_TEMP/smoke-test" ${cases:-}
- name: Stop test backend
if: ${{ !cancelled() && steps.backend.outcome != 'skipped' }}
run: scripts/ci/desktop-linux/backend.sh stop "$RUNNER_TEMP/backend"
- name: Upload smoke test results
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: android-smoke-test
path: ${{ runner.temp }}/smoke-test
retention-days: 3
if-no-files-found: warn
- name: Upload backend logs
if: ${{ !cancelled() && steps.backend.outcome != 'skipped' }}
uses: actions/upload-artifact@v7
with:
name: android-backend-logs
path: ${{ runner.temp }}/backend/*.log
retention-days: 3
if-no-files-found: warn
# Claude Code repeats the smoke test with the ui helper. Each run spends the Claude subscription's usage
# limits, so it runs only when asked for. The app-sim-claude environment's branch rule decides which branches
# may use the token.
claude:
name: Claude tests the Android app
needs: [plan, app]
if: ${{ !cancelled() && needs.plan.outputs.run-claude == 'true' && needs.app.result == 'success' }}
runs-on: ubuntu-24.04
timeout-minutes: 60
environment:
name: app-sim-claude
deployment: false
concurrency:
group: android-emu-claude-${{ github.ref }}
cancel-in-progress: true
env:
# the same in all app workflows
CLAUDE_CODE_VERSION: 2.1.300
CLAUDE_MODEL: claude-haiku-5-5
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Install tools
run: |
sudo apt-get update
sudo apt-get install -y libpulse0 libegl1 imagemagick sqlite3 fonts-dejavu-core
dpkg-query -W libpulse0 libegl1 imagemagick sqlite3 fonts-dejavu-core
- name: Download app
uses: actions/download-artifact@v8
with:
name: android-app
path: ${{ runner.temp }}/apk
- name: Download backend
if: needs.plan.outputs.claude-backend == 'yes'
uses: actions/download-artifact@v8
with:
name: ${{ needs.plan.outputs.backend }}
path: ~/backend
run-id: ${{ needs.plan.outputs.backend-run || github.run_id }}
github-token: ${{ github.token }}
- name: Start test backend
id: backend
if: needs.plan.outputs.claude-backend == 'yes'
timeout-minutes: 30
run: scripts/ci/desktop-linux/backend.sh start ~/backend/${{ env.BACKEND_PKG }} "$RUNNER_TEMP/backend"
- name: Install emulator and system image
run: scripts/ci/android/install-sdk.sh "$RUNNER_TEMP/android-sdk"
- name: Boot emulator
run: scripts/ci/android/emulator.sh start "$RUNNER_TEMP/claude-test/screens"
- name: Reach the test backend
if: steps.backend.outcome == 'success'
run: scripts/ci/android/emulator.sh reverse 5223
# ui goes on every shell's PATH, where Claude's Bash tool finds it whatever environment it builds
- name: Install Claude Code and the ui command
run: |
npm install --prefix "$RUNNER_TEMP/claude-code" "@anthropic-ai/claude-code@$CLAUDE_CODE_VERSION"
echo "$RUNNER_TEMP/claude-code/node_modules/.bin" >> "$GITHUB_PATH"
printf '#!/bin/sh\nexec %s/scripts/ci/android/ui "$@"\n' "$PWD" | sudo tee /usr/local/bin/ui
sudo chmod +x /usr/local/bin/ui
- name: Start the app
run: scripts/ci/android/smoke-test.sh "$RUNNER_TEMP/apk/$APK" "$RUNNER_TEMP/claude-test/screens" start
# Claude's Bash is limited to single ui commands by the guard hook in claude-test.sh. Claude Code's credential
# scrub stays off: on Linux it forces its whole Bash sandbox (bubblewrap, socat), which ui isn't set up for.
- name: Run Claude
id: claude
env:
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1
run: scripts/ci/desktop-linux/claude-test.sh "$RUNNER_TEMP/claude-test" "$RUNNER_TEMP/claude-transcript.jsonl" scripts/ci/android/claude-test.md
- name: Report Claude usage
if: ${{ !cancelled() && steps.claude.outcome != 'skipped' }}
run: scripts/ci/desktop-linux/claude-usage.py "$RUNNER_TEMP/claude-transcript.jsonl" "$RUNNER_TEMP/claude-usage.json"
- name: Stop test backend
if: ${{ !cancelled() && steps.backend.outcome != 'skipped' }}
run: scripts/ci/desktop-linux/backend.sh stop "$RUNNER_TEMP/backend"
- name: Upload Claude test results
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: android-claude-test
path: |
${{ runner.temp }}/claude-test
${{ runner.temp }}/claude-transcript.jsonl
${{ runner.temp }}/claude-transcript.jsonl.stderr
${{ runner.temp }}/claude-transcript.jsonl.guard.log
${{ runner.temp }}/claude-usage.json
retention-days: 7
if-no-files-found: warn
- name: Upload backend logs
if: ${{ !cancelled() && steps.backend.outcome != 'skipped' }}
uses: actions/upload-artifact@v7
with:
name: android-claude-backend-logs
path: ${{ runner.temp }}/backend/*.log
retention-days: 3
if-no-files-found: warn
# the latest release's onboarding, to tell a branch's own failure from the runner's
release:
name: released app in the emulator
needs: plan
if: needs.plan.outputs.run-release == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
filter: tree:0
- name: Install tools
run: |
sudo apt-get update
sudo apt-get install -y libpulse0 libegl1
dpkg-query -W libpulse0 libegl1
- name: Download released app
run: scripts/ci/android/release-apk.sh "$RUNNER_TEMP/release"
- name: Install emulator and system image
run: scripts/ci/android/install-sdk.sh "$RUNNER_TEMP/android-sdk"
- name: Boot emulator
run: scripts/ci/android/emulator.sh start "$RUNNER_TEMP/smoke-test"
- name: Run smoke test
run: scripts/ci/android/smoke-test.sh "$RUNNER_TEMP/release/simplex-aarch64.apk" "$RUNNER_TEMP/smoke-test" onboarding
- name: Upload smoke test results
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: android-release-smoke-test
path: ${{ runner.temp }}/smoke-test
retention-days: 3
if-no-files-found: warn
+54 -12
View File
@@ -222,13 +222,6 @@ jobs:
fail-fast: false
matrix:
include:
- os: 22.04
os_underscore: 22_04
arch: x86_64
runner: "ubuntu-22.04"
ghc: "8.10.7"
hash: 'sha256:5c8b2c0a6c745bc177669abfaa716b4bc57d58e2ea3882fb5da67f4d59e3dda5'
should_run: ${{ !(github.ref == 'refs/heads/stable' || startsWith(github.ref, 'refs/tags/v')) }}
- os: 22.04
os_underscore: 22_04
arch: x86_64
@@ -629,7 +622,7 @@ jobs:
- name: Setup Node.js
id: setup-node
if: ${{ !cancelled() && ((needs.maybe-release.outputs.core_changed && steps.build-lib.outcome == 'success') || (matrix.libs && (needs.maybe-release.outputs.addon_draft || needs.maybe-release.outputs.publish))) }}
if: ${{ !cancelled() && matrix.libs && (github.event_name == 'pull_request' || needs.maybe-release.outputs.addon_draft || needs.maybe-release.outputs.publish) }}
uses: actions/setup-node@v7
with:
node-version: 22
@@ -642,11 +635,11 @@ jobs:
python-version: "3.12"
- name: Package tests (pull request)
if: ${{ !cancelled() && steps.setup-python.outcome == 'success' && needs.maybe-release.outputs.core_changed }}
if: ${{ !cancelled() && steps.setup-python.outcome == 'success' && github.event_name == 'pull_request' && steps.build-lib.outcome != 'failure' }}
timeout-minutes: 30
shell: bash
env:
SIMPLEX_LIBS_DIR: ${{ github.workspace }}/release-libs/simplex-chat-libs-linux-${{ matrix.arch }}/libs
SIMPLEX_LIBS_DIR: ${{ steps.build-lib.outcome == 'success' && format('{0}/release-libs/simplex-chat-libs-linux-{1}/libs', github.workspace, matrix.arch) || '' }}
run: |
scripts/ci/package-tests.sh build linux-${{ matrix.arch }}
scripts/ci/package-tests.sh test
@@ -757,6 +750,31 @@ jobs:
hash: 'sha256:68434214381cb38287104e629fe8ee720167dd98cbb36ab1cbbab342515fa6ab'
steps: *linux-steps
build-linux-ghc8:
name: "ubuntu-${{ matrix.os }}-${{ matrix.arch }} (CLI,Desktop), GHC: ${{ matrix.ghc }}"
needs: [maybe-release, variables]
if: ${{ !(github.ref == 'refs/heads/stable' || startsWith(github.ref, 'refs/tags/v')) }}
runs-on: ${{ matrix.runner }}
permissions:
contents: write
packages: write
env:
image_cache: ghcr.io/simplex-chat/simplex-chat-build-cache:${{ matrix.os }}-${{ matrix.arch }}-ghc${{ matrix.ghc }}
source_hashes: dist-newstyle/sources.sha256
concurrency: *linux-concurrency
strategy:
fail-fast: false
matrix:
include:
- os: 22.04
os_underscore: 22_04
arch: x86_64
runner: "ubuntu-22.04"
ghc: "8.10.7"
hash: 'sha256:5c8b2c0a6c745bc177669abfaa716b4bc57d58e2ea3882fb5da67f4d59e3dda5'
should_run: true
steps: *linux-steps
# =================================
# Linux PostgreSQL Library Build
# =================================
@@ -1093,6 +1111,14 @@ jobs:
github_ref: ${{ github.ref }}
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: Mount RAM disk for tests
if: matrix.arch == 'aarch64'
shell: bash
run: |
disk=$(hdiutil attach -nomount ram://1048576 | tr -d '[:space:]')
diskutil erasevolume HFS+ tests "$disk"
ln -s /Volumes/tests tests/tmp
- name: Run tests
if: matrix.arch == 'aarch64'
timeout-minutes: 120
@@ -1141,7 +1167,7 @@ jobs:
- name: Setup Node.js
id: setup-node
if: ${{ !cancelled() && (needs.maybe-release.outputs.addon_draft || needs.maybe-release.outputs.publish) }}
if: ${{ !cancelled() && (github.event_name == 'pull_request' || needs.maybe-release.outputs.addon_draft || needs.maybe-release.outputs.publish) }}
uses: actions/setup-node@v7
with:
node-version: 22
@@ -1153,6 +1179,14 @@ jobs:
with:
python-version: "3.12"
- name: Package tests (pull request)
if: ${{ !cancelled() && steps.setup-python.outcome == 'success' && github.event_name == 'pull_request' }}
timeout-minutes: 30
shell: bash
run: |
scripts/ci/package-tests.sh build macos-${{ matrix.arch }}
scripts/ci/package-tests.sh test
- name: Add-on
if: ${{ !cancelled() && steps.setup-python.outcome == 'success' && needs.maybe-release.outputs.addon_draft }}
timeout-minutes: 30
@@ -1402,7 +1436,7 @@ jobs:
- name: Setup Node.js
id: setup-node
if: ${{ !cancelled() && (needs.maybe-release.outputs.addon_draft || needs.maybe-release.outputs.publish) }}
if: ${{ !cancelled() && (github.event_name == 'pull_request' || needs.maybe-release.outputs.addon_draft || needs.maybe-release.outputs.publish) }}
uses: actions/setup-node@v7
with:
node-version: 22
@@ -1414,6 +1448,14 @@ jobs:
with:
python-version: "3.12"
- name: Package tests (pull request)
if: ${{ !cancelled() && steps.setup-python.outcome == 'success' && github.event_name == 'pull_request' }}
timeout-minutes: 30
shell: bash
run: |
scripts/ci/package-tests.sh build windows-x86_64
scripts/ci/package-tests.sh test
- name: Add-on
if: ${{ !cancelled() && steps.setup-python.outcome == 'success' && needs.maybe-release.outputs.addon_draft }}
timeout-minutes: 30
+4 -20
View File
@@ -1,14 +1,6 @@
name: desktop-linux
# A push runs the jobs only when its head commit's message asks for them: see scripts/ci/desktop-linux/README.md.
on:
push:
branches:
# only f/ branches, so other branches' pull requests don't list skipped runs; elsewhere, run it by hand
- "f/*"
# a squash merge's message lists the merged commits' subjects, with their tags
- "!master"
- "!stable"
workflow_dispatch:
inputs:
claude:
@@ -29,8 +21,7 @@ permissions:
actions: read
concurrency:
# a push that asks for no run must not cancel one that does
group: desktop-linux-${{ github.ref }}-${{ (github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[app-ci]') || contains(github.event.head_commit.message, '[app-ci:desktop]') || contains(github.event.head_commit.message, '[claude-test]') || contains(github.event.head_commit.message, '[claude-test:desktop]')) && 'requested' || github.run_id }}
group: desktop-linux-${{ github.ref }}
cancel-in-progress: true
env:
@@ -43,13 +34,6 @@ env:
jobs:
plan:
name: choose jobs, find earlier builds
# the concurrency group repeats this test, as it can't read job outputs
if: >-
github.event_name == 'workflow_dispatch' ||
contains(github.event.head_commit.message, '[app-ci]') ||
contains(github.event.head_commit.message, '[app-ci:desktop]') ||
contains(github.event.head_commit.message, '[claude-test]') ||
contains(github.event.head_commit.message, '[claude-test:desktop]')
runs-on: ubuntu-24.04
timeout-minutes: 10
outputs:
@@ -65,9 +49,9 @@ jobs:
- name: Choose jobs
id: jobs
env:
CLAUDE: ${{ inputs.claude || contains(github.event.head_commit.message, '[claude-test]') || contains(github.event.head_commit.message, '[claude-test:desktop]') }}
BACKEND: ${{ inputs.backend || contains(github.event.head_commit.message, '[ci-backend]') }}
RELEASE: ${{ inputs.release || contains(github.event.head_commit.message, '[ci-release]') }}
CLAUDE: ${{ inputs.claude }}
BACKEND: ${{ inputs.backend }}
RELEASE: ${{ inputs.release }}
run: |
echo "claude=$CLAUDE" | tee -a "$GITHUB_OUTPUT"
echo "backend=$BACKEND" | tee -a "$GITHUB_OUTPUT"
+3 -18
View File
@@ -1,13 +1,6 @@
name: ios-sim
on:
push:
branches:
# only f/ branches, so other branches' pull requests don't list skipped runs; elsewhere, run it by hand
- 'f/*'
# a squash merge's message lists the merged commits' subjects, with their tags
- '!master'
- '!stable'
workflow_dispatch:
inputs:
claude:
@@ -24,8 +17,7 @@ permissions:
actions: read
concurrency:
# a push that asks for no run must not cancel one that does
group: ios-sim-${{ github.ref }}-${{ (github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[app-ci]') || contains(github.event.head_commit.message, '[app-ci:ios]') || contains(github.event.head_commit.message, '[claude-test]') || contains(github.event.head_commit.message, '[claude-test:ios]')) && 'requested' || github.run_id }}
group: ios-sim-${{ github.ref }}
cancel-in-progress: true
env:
@@ -41,13 +33,6 @@ env:
jobs:
plan:
name: choose jobs, find earlier builds
# the concurrency group repeats this test, as it can't read job outputs
if: >-
github.event_name == 'workflow_dispatch' ||
contains(github.event.head_commit.message, '[app-ci]') ||
contains(github.event.head_commit.message, '[app-ci:ios]') ||
contains(github.event.head_commit.message, '[claude-test]') ||
contains(github.event.head_commit.message, '[claude-test:ios]')
runs-on: ubuntu-24.04
timeout-minutes: 10
outputs:
@@ -62,8 +47,8 @@ jobs:
- name: Choose jobs
id: jobs
env:
CLAUDE: ${{ inputs.claude || contains(github.event.head_commit.message, '[claude-test]') || contains(github.event.head_commit.message, '[claude-test:ios]') }}
BACKEND: ${{ inputs.backend || contains(github.event.head_commit.message, '[ci-backend]') }}
CLAUDE: ${{ inputs.claude }}
BACKEND: ${{ inputs.backend }}
run: |
echo "claude=$CLAUDE" | tee -a "$GITHUB_OUTPUT"
echo "backend=$BACKEND" | tee -a "$GITHUB_OUTPUT"
-2
View File
@@ -26,7 +26,6 @@ jobs:
runs-on: ubuntu-latest
outputs:
platforms: ${{ steps.plan.outputs.platforms }}
test: ${{ steps.plan.outputs.test }}
steps:
- name: Checkout Code
uses: actions/checkout@v6
@@ -70,5 +69,4 @@ jobs:
run: scripts/ci/package-tests.sh build ${{ matrix.platform }}
- name: Test simplex-chat (npm and PyPI)
if: needs.addon.outputs.test
run: scripts/ci/package-tests.sh test
File diff suppressed because it is too large. Load diff
@@ -11093,6 +11093,42 @@ last received msg: %2$@</source>
<source>Crowdfunding investors can reserve names before the sale starts: [simplex.domains](https://simplex.domains/)</source>
<target state="translated">Kütləvi maliyyələşdirmə investorları satış başlamazdan əvvəl adları sifariş edə bilərlər: [simplex.domains](https://simplex.domains/)</target>
</trans-unit>
<trans-unit id="forwarded from" xml:space="preserve">
<source>forwarded from</source>
</trans-unit>
<trans-unit id="SimpleX name sale starts in" xml:space="preserve">
<source>SimpleX name sale starts in</source>
</trans-unit>
<trans-unit id="Update the app to register a SimpleX domain" xml:space="preserve">
<source>Update the app to register a SimpleX domain</source>
</trans-unit>
<trans-unit id="You are a moderator" xml:space="preserve">
<source>You are a moderator</source>
</trans-unit>
<trans-unit id="until you can register a SimpleX domain" xml:space="preserve">
<source>until you can register a SimpleX domain</source>
</trans-unit>
<trans-unit id="You are a member" xml:space="preserve">
<source>You are a member</source>
</trans-unit>
<trans-unit id="You are an admin" xml:space="preserve">
<source>You are an admin</source>
</trans-unit>
<trans-unit id="You are a contributor" xml:space="preserve">
<source>You are a contributor</source>
</trans-unit>
<trans-unit id="saved from" xml:space="preserve">
<source>saved from</source>
</trans-unit>
<trans-unit id="You are an owner" xml:space="preserve">
<source>You are an owner</source>
</trans-unit>
<trans-unit id="You are a subscriber" xml:space="preserve">
<source>You are a subscriber</source>
</trans-unit>
<trans-unit id="You are an observer" xml:space="preserve">
<source>You are an observer</source>
</trans-unit>
</body>
</file>
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="az" datatype="plaintext">
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -4002,7 +4002,7 @@ SimpleX 伺服器並不會看到你的個人檔案。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Join as %@" xml:space="preserve" approved="no">
<source>join as %@</source>
<source>Join as %@</source>
<target state="translated">以 %@ 身份加入</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -11597,6 +11597,274 @@ If you cancel, the channel will be deleted - you can create it again.</source>
<source>https://</source>
<target state="translated">https://</target>
</trans-unit>
<trans-unit id="The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner." xml:space="preserve">
<source>The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner.</source>
</trans-unit>
<trans-unit id="forwarded from" xml:space="preserve">
<source>forwarded from</source>
</trans-unit>
<trans-unit id="Require signing messages." xml:space="preserve">
<source>Require signing messages.</source>
</trans-unit>
<trans-unit id="The SimpleX name %@ is registered, but it has no valid link." xml:space="preserve">
<source>The SimpleX name %@ is registered, but it has no valid link.</source>
</trans-unit>
<trans-unit id="Error sharing address" xml:space="preserve">
<source>Error sharing address</source>
</trans-unit>
<trans-unit id="contributor" xml:space="preserve">
<source>contributor</source>
</trans-unit>
<trans-unit id="SimpleX name sale starts in" xml:space="preserve">
<source>SimpleX name sale starts in</source>
</trans-unit>
<trans-unit id="Update the app to register a SimpleX domain" xml:space="preserve">
<source>Update the app to register a SimpleX domain</source>
</trans-unit>
<trans-unit id="SimpleX name not verified" xml:space="preserve">
<source>SimpleX name not verified</source>
</trans-unit>
<trans-unit id="%d owners" xml:space="preserve">
<source>%d owners</source>
</trans-unit>
<trans-unit id="You are a moderator" xml:space="preserve">
<source>You are a moderator</source>
</trans-unit>
<trans-unit id="None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link." xml:space="preserve">
<source>None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link.</source>
</trans-unit>
<trans-unit id="until you can register a SimpleX domain" xml:space="preserve">
<source>until you can register a SimpleX domain</source>
</trans-unit>
<trans-unit id="Your contact removed this link, or it was a one-time link that was already used.&#10;To connect, ask your contact to create a new link." xml:space="preserve">
<source>Your contact removed this link, or it was a one-time link that was already used.
To connect, ask your contact to create a new link.</source>
</trans-unit>
<trans-unit id="You can now invest in SimpleX Chat! 🚀" xml:space="preserve">
<source>You can now invest in SimpleX Chat! 🚀</source>
</trans-unit>
<trans-unit id="Let people join via name registered with this channel link." xml:space="preserve">
<source>Let people join via name registered with this channel link.</source>
</trans-unit>
<trans-unit id="Resolver error: %@" xml:space="preserve">
<source>Resolver error: %@</source>
</trans-unit>
<trans-unit id="How to register a test name" xml:space="preserve">
<source>How to register a test name</source>
</trans-unit>
<trans-unit id="%02d hrs %02d min %02d sec" xml:space="preserve">
<source>%1$02d hrs %2$02d min %3$02d sec</source>
</trans-unit>
<trans-unit id="Crowdfunding on Wefunder" xml:space="preserve">
<source>Crowdfunding on Wefunder</source>
</trans-unit>
<trans-unit id="Easier to read." xml:space="preserve">
<source>Easier to read.</source>
</trans-unit>
<trans-unit id="Signed &amp; verified" xml:space="preserve">
<source>Signed &amp; verified</source>
</trans-unit>
<trans-unit id="%d day" xml:space="preserve">
<source>%d day</source>
</trans-unit>
<trans-unit id="The channel required this message to be signed, but the signature is missing." xml:space="preserve">
<source>The channel required this message to be signed, but the signature is missing.</source>
</trans-unit>
<trans-unit id="Add contributors." xml:space="preserve">
<source>Add contributors.</source>
</trans-unit>
<trans-unit id="Verify name" xml:space="preserve">
<source>Verify name</source>
</trans-unit>
<trans-unit id="Crowdfunding on Wefunder." xml:space="preserve">
<source>Crowdfunding on Wefunder.</source>
</trans-unit>
<trans-unit id="Your SimpleX name" xml:space="preserve">
<source>Your SimpleX name</source>
</trans-unit>
<trans-unit id="Contact requests in groups" xml:space="preserve">
<source>Contact requests in groups</source>
</trans-unit>
<trans-unit id="You are a member" xml:space="preserve">
<source>You are a member</source>
</trans-unit>
<trans-unit id="To verify keys with this subscriber, compare (or scan) the code on your devices." xml:space="preserve">
<source>To verify keys with this subscriber, compare (or scan) the code on your devices.</source>
</trans-unit>
<trans-unit id="Edit description" xml:space="preserve">
<source>Edit description</source>
</trans-unit>
<trans-unit id="Message signing is required." xml:space="preserve">
<source>Message signing is required.</source>
</trans-unit>
<trans-unit id="Group invitations" xml:space="preserve">
<source>Group invitations</source>
</trans-unit>
<trans-unit id="%d owners &amp; contributors" xml:space="preserve">
<source>%d owners &amp; contributors</source>
</trans-unit>
<trans-unit id="Signature missing" xml:space="preserve">
<source>Signature missing</source>
</trans-unit>
<trans-unit id="Better channels 📢" xml:space="preserve">
<source>Better channels 📢</source>
</trans-unit>
<trans-unit id="File servers: %@" xml:space="preserve">
<source>File servers: %@</source>
</trans-unit>
<trans-unit id="Signed" xml:space="preserve">
<source>Signed</source>
</trans-unit>
<trans-unit id="Public names for your channel or business." xml:space="preserve">
<source>Public names for your channel or business.</source>
</trans-unit>
<trans-unit id="Name not found" xml:space="preserve">
<source>Name not found</source>
</trans-unit>
<trans-unit id="To resolve names" xml:space="preserve">
<source>To resolve names</source>
</trans-unit>
<trans-unit id="Remove name" xml:space="preserve">
<source>Remove name</source>
</trans-unit>
<trans-unit id="Add description" xml:space="preserve">
<source>Add description</source>
</trans-unit>
<trans-unit id="You are an admin" xml:space="preserve">
<source>You are an admin</source>
</trans-unit>
<trans-unit id="Create web preview." xml:space="preserve">
<source>Create web preview.</source>
</trans-unit>
<trans-unit id="Destination server version of %1$@ is incompatible with forwarding server %2$@." xml:space="preserve">
<source>Destination server version of %1$@ is incompatible with forwarding server %2$@.</source>
</trans-unit>
<trans-unit id="The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page." xml:space="preserve">
<source>The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page.</source>
</trans-unit>
<trans-unit id="Don't save" xml:space="preserve">
<source>Don't save</source>
</trans-unit>
<trans-unit id="You are a contributor" xml:space="preserve">
<source>You are a contributor</source>
</trans-unit>
<trans-unit id="Server %@ does not support name resolution. Configure servers, or use a connection link." xml:space="preserve">
<source>Server %@ does not support name resolution. Configure servers, or use a connection link.</source>
</trans-unit>
<trans-unit id="saved from" xml:space="preserve">
<source>saved from</source>
</trans-unit>
<trans-unit id="Connection blocked: %@" xml:space="preserve">
<source>Connection blocked: %@</source>
</trans-unit>
<trans-unit id="Do not require signing messages." xml:space="preserve">
<source>Do not require signing messages.</source>
</trans-unit>
<trans-unit id="File servers" xml:space="preserve">
<source>File servers</source>
</trans-unit>
<trans-unit id="Connect to %@" xml:space="preserve">
<source>Connect to %@</source>
</trans-unit>
<trans-unit id="Connection link removed" xml:space="preserve">
<source>Connection link removed</source>
</trans-unit>
<trans-unit id="No valid link" xml:space="preserve">
<source>No valid link</source>
</trans-unit>
<trans-unit id="Signing proves you authored this message and can't be denied later." xml:space="preserve">
<source>Signing proves you authored this message and can't be denied later.</source>
</trans-unit>
<trans-unit id="You are an owner" xml:space="preserve">
<source>You are an owner</source>
</trans-unit>
<trans-unit id="Change role?" xml:space="preserve">
<source>Change role?</source>
</trans-unit>
<trans-unit id="Destination server address of %1$@ is incompatible with forwarding server %2$@ settings." xml:space="preserve">
<source>Destination server address of %1$@ is incompatible with forwarding server %2$@ settings.</source>
</trans-unit>
<trans-unit id="You are a subscriber" xml:space="preserve">
<source>You are a subscriber</source>
</trans-unit>
<trans-unit id="Let people connect to you via name registered with your SimpleX address." xml:space="preserve">
<source>Let people connect to you via name registered with your SimpleX address.</source>
</trans-unit>
<trans-unit id="You can now invest in SimpleX Chat" xml:space="preserve">
<source>You can now invest in SimpleX Chat</source>
</trans-unit>
<trans-unit id="Verify SimpleX names" xml:space="preserve">
<source>Verify SimpleX names</source>
</trans-unit>
<trans-unit id="Error saving name" xml:space="preserve">
<source>Error saving name</source>
</trans-unit>
<trans-unit id="This SimpleX name is not registered. Please check the name." xml:space="preserve">
<source>This SimpleX name is not registered. Please check the name.</source>
</trans-unit>
<trans-unit id="%d owner" xml:space="preserve">
<source>%d owner</source>
</trans-unit>
<trans-unit id="Message signing is not required." xml:space="preserve">
<source>Message signing is not required.</source>
</trans-unit>
<trans-unit id="SimpleX name" xml:space="preserve">
<source>SimpleX name</source>
</trans-unit>
<trans-unit id="SimpleX public names (BETA)" xml:space="preserve">
<source>SimpleX public names (BETA)</source>
</trans-unit>
<trans-unit id="Unconfirmed name" xml:space="preserve">
<source>Unconfirmed name</source>
</trans-unit>
<trans-unit id="Save SimpleX name?" xml:space="preserve">
<source>Save SimpleX name?</source>
</trans-unit>
<trans-unit id="Crowdfunding investors can reserve names before the sale starts: [simplex.domains](https://simplex.domains/)" xml:space="preserve">
<source>Crowdfunding investors can reserve names before the sale starts: [simplex.domains](https://simplex.domains/)</source>
</trans-unit>
<trans-unit id="Show encryption" xml:space="preserve">
<source>Show encryption</source>
</trans-unit>
<trans-unit id="Enter description (optional)" xml:space="preserve">
<source>Enter description (optional)</source>
</trans-unit>
<trans-unit id="The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page." xml:space="preserve">
<source>The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page.</source>
</trans-unit>
<trans-unit id="You are an observer" xml:space="preserve">
<source>You are an observer</source>
</trans-unit>
<trans-unit id="Get SimpleX name (BETA)" xml:space="preserve">
<source>Get SimpleX name (BETA)</source>
</trans-unit>
<trans-unit id="Join channel %@" xml:space="preserve">
<source>Join channel %@</source>
</trans-unit>
<trans-unit id="Sign message" xml:space="preserve">
<source>Sign message</source>
</trans-unit>
<trans-unit id="SimpleX name error" xml:space="preserve">
<source>SimpleX name error</source>
</trans-unit>
<trans-unit id="subscriber" xml:space="preserve">
<source>subscriber</source>
</trans-unit>
<trans-unit id="No servers to resolve names." xml:space="preserve">
<source>No servers to resolve names.</source>
</trans-unit>
<trans-unit id="Sign messages" xml:space="preserve">
<source>Sign messages</source>
</trans-unit>
<trans-unit id="Manage your relays." xml:space="preserve">
<source>Manage your relays.</source>
</trans-unit>
<trans-unit id="Channel SimpleX name" xml:space="preserve">
<source>Channel SimpleX name</source>
</trans-unit>
<trans-unit id="Role will be changed to &quot;%@&quot;. All subscribers will be notified." xml:space="preserve">
<source>Role will be changed to "%@". All subscribers will be notified.</source>
</trans-unit>
</body>
</file>
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="zh-Hant" datatype="plaintext">
@@ -11849,4 +12117,14 @@ If you cancel, the channel will be deleted - you can create it again.</source>
</trans-unit>
</body>
</file>
<file original="en.lproj/SimpleXChat-InfoPlist.strings" source-language="en" target-language="zh-Hant" datatype="plaintext">
<body>
<trans-unit id="CFBundleName" translate="no" xml:space="preserve">
<source>SimpleXChat</source>
</trans-unit>
<trans-unit id="NSHumanReadableCopyright" translate="no" xml:space="preserve">
<source>Copyright © 2022 SimpleX Chat. All rights reserved.</source>
</trans-unit>
</body>
</file>
</xliff>
@@ -16,9 +16,6 @@
/* No comment provided by engineer. */
"Comment" = "Kommentieren";
/* No comment provided by engineer. */
"Currently maximum supported file size is %@." = "Die maximal erlaubte Dateigröße beträgt aktuell %@.";
/* No comment provided by engineer. */
"Database downgrade required" = "Datenbank-Herunterstufung ist erforderlich";
@@ -16,9 +16,6 @@
/* No comment provided by engineer. */
"Comment" = "Comentario";
/* No comment provided by engineer. */
"Currently maximum supported file size is %@." = "El tamaño máximo de archivo admitido es %@.";
/* No comment provided by engineer. */
"Database downgrade required" = "Se requiere volver a versión anterior de la base de datos";
@@ -16,9 +16,6 @@
/* No comment provided by engineer. */
"Comment" = "Commenter";
/* No comment provided by engineer. */
"Currently maximum supported file size is %@." = "Actuellement, la taille maximale des fichiers supportés est de %@.";
/* No comment provided by engineer. */
"Database downgrade required" = "Mise à jour de la base de données nécessaire";
@@ -16,9 +16,6 @@
/* No comment provided by engineer. */
"Comment" = "Megjegyzés";
/* No comment provided by engineer. */
"Currently maximum supported file size is %@." = "Jelenleg támogatott legnagyobb fájlméret: %@.";
/* No comment provided by engineer. */
"Database downgrade required" = "Adatbázis visszafejlesztése szükséges";
@@ -16,9 +16,6 @@
/* No comment provided by engineer. */
"Comment" = "Commento";
/* No comment provided by engineer. */
"Currently maximum supported file size is %@." = "Attualmente la dimensione massima supportata è di %@.";
/* No comment provided by engineer. */
"Database downgrade required" = "Downgrade del database necessario";
@@ -16,9 +16,6 @@
/* No comment provided by engineer. */
"Comment" = "Opmerking";
/* No comment provided by engineer. */
"Currently maximum supported file size is %@." = "De momenteel maximaal ondersteunde bestandsgrootte is %@.";
/* No comment provided by engineer. */
"Database downgrade required" = "Database downgrade vereist";
@@ -16,9 +16,6 @@
/* No comment provided by engineer. */
"Comment" = "Komentarz";
/* No comment provided by engineer. */
"Currently maximum supported file size is %@." = "Obecnie maksymalny obsługiwany rozmiar pliku to %@.";
/* No comment provided by engineer. */
"Database downgrade required" = "Wymagane obniżenie wersji bazy danych";
@@ -16,9 +16,6 @@
/* No comment provided by engineer. */
"Comment" = "Комментарий";
/* No comment provided by engineer. */
"Currently maximum supported file size is %@." = "В настоящее время максимальный поддерживаемый размер файла составляет %@.";
/* No comment provided by engineer. */
"Database downgrade required" = "Требуется откат базы данных";
@@ -16,9 +16,6 @@
/* No comment provided by engineer. */
"Comment" = "Yorum";
/* No comment provided by engineer. */
"Currently maximum supported file size is %@." = "Şu anki maksimum desteklenen dosya boyutu %@ kadardır.";
/* No comment provided by engineer. */
"Database downgrade required" = "Veritabanı sürüm düşürme gerekli";
@@ -16,9 +16,6 @@
/* No comment provided by engineer. */
"Comment" = "Коментар";
/* No comment provided by engineer. */
"Currently maximum supported file size is %@." = "Наразі максимальний підтримуваний розмір файлу - %@.";
/* No comment provided by engineer. */
"Database downgrade required" = "Потрібне оновлення бази даних";
@@ -16,9 +16,6 @@
/* No comment provided by engineer. */
"Comment" = "评论";
/* No comment provided by engineer. */
"Currently maximum supported file size is %@." = "当前支持的最大文件大小为 %@。";
/* No comment provided by engineer. */
"Database downgrade required" = "需要数据库降级";
+24 -24
View File
@@ -168,11 +168,8 @@
647C9E293061362A0032110E /* GetStakeBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647C9E283061362A0032110E /* GetStakeBanner.swift */; };
647C9E2C30666C640032110E /* BadgesChooseBadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647C9E2A30666C640032110E /* BadgesChooseBadgeView.swift */; };
647C9E2D30666C640032110E /* BadgesHowLongView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647C9E2B30666C640032110E /* BadgesHowLongView.swift */; };
64D1B0A23070AA0000C0DE01 /* BadgesCheckOrderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D1B0A13070AA0000C0DE01 /* BadgesCheckOrderView.swift */; };
64D1B0A43070AA0000C0DE01 /* BadgesPurchaseStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D1B0A33070AA0000C0DE01 /* BadgesPurchaseStateView.swift */; };
647F090E288EA27B00644C40 /* GroupMemberInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647F090D288EA27B00644C40 /* GroupMemberInfoView.swift */; };
648010AB281ADD15009009B9 /* CIFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648010AA281ADD15009009B9 /* CIFileView.swift */; };
6F3B2A1C2E8D4F0A9B7C1D21 /* MarkdownFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3B2A1C2E8D4F0A9B7C1D22 /* MarkdownFileView.swift */; };
648679AB2BC96A74006456E7 /* ChatItemForwardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */; };
6495D7042F48CFC50060512B /* ChannelMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6495D7032F48CFC50060512B /* ChannelMembersView.swift */; };
6495D7062F48CFFD0060512B /* ChannelRelaysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6495D7052F48CFFD0060512B /* ChannelRelaysView.swift */; };
@@ -196,12 +193,14 @@
64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; };
64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; };
64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; };
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.11-LxOKYAQgszM3XevVjyMHum-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.11-LxOKYAQgszM3XevVjyMHum-ghc9.6.3.a */; };
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.11-LxOKYAQgszM3XevVjyMHum.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.11-LxOKYAQgszM3XevVjyMHum.a */; };
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.14-FQI9ONxXS3x5DhsgnqtwJs-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.14-FQI9ONxXS3x5DhsgnqtwJs-ghc9.6.3.a */; };
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.14-FQI9ONxXS3x5DhsgnqtwJs.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.14-FQI9ONxXS3x5DhsgnqtwJs.a */; };
64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; };
64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; };
64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; };
64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */; };
64D1B0A23070AA0000C0DE01 /* BadgesCheckOrderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D1B0A13070AA0000C0DE01 /* BadgesCheckOrderView.swift */; };
64D1B0A43070AA0000C0DE01 /* BadgesPurchaseStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D1B0A33070AA0000C0DE01 /* BadgesPurchaseStateView.swift */; };
64E5E3632DF71A4E00A4D530 /* ContextContactRequestActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E5E3622DF71A4E00A4D530 /* ContextContactRequestActionsView.swift */; };
64E5E3672DFC16A900A4D530 /* ContextProfilePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E5E3662DFC16A900A4D530 /* ContextProfilePickerView.swift */; };
64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */; };
@@ -210,6 +209,7 @@
64EEB0F72C353F1C00972D62 /* ServersSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */; };
64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */; };
64FC8F9D2E3B6DEF0068F384 /* ContextMemberContactActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FC8F9C2E3B6DEF0068F384 /* ContextMemberContactActionsView.swift */; };
6F3B2A1C2E8D4F0A9B7C1D21 /* MarkdownFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3B2A1C2E8D4F0A9B7C1D22 /* MarkdownFileView.swift */; };
8C01E9C12C8EFC33008A4B0A /* objc.m in Sources */ = {isa = PBXBuildFile; fileRef = 8C01E9C02C8EFC33008A4B0A /* objc.m */; };
8C01E9C22C8EFF8F008A4B0A /* objc.h in Headers */ = {isa = PBXBuildFile; fileRef = 8C01E9BF2C8EFBB6008A4B0A /* objc.h */; };
8C69FE7D2B8C7D2700267E38 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */; };
@@ -565,11 +565,8 @@
647C9E283061362A0032110E /* GetStakeBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetStakeBanner.swift; sourceTree = "<group>"; };
647C9E2A30666C640032110E /* BadgesChooseBadgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgesChooseBadgeView.swift; sourceTree = "<group>"; };
647C9E2B30666C640032110E /* BadgesHowLongView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgesHowLongView.swift; sourceTree = "<group>"; };
64D1B0A13070AA0000C0DE01 /* BadgesCheckOrderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgesCheckOrderView.swift; sourceTree = "<group>"; };
64D1B0A33070AA0000C0DE01 /* BadgesPurchaseStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgesPurchaseStateView.swift; sourceTree = "<group>"; };
647F090D288EA27B00644C40 /* GroupMemberInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberInfoView.swift; sourceTree = "<group>"; };
648010AA281ADD15009009B9 /* CIFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIFileView.swift; sourceTree = "<group>"; };
6F3B2A1C2E8D4F0A9B7C1D22 /* MarkdownFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownFileView.swift; sourceTree = "<group>"; };
648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemForwardingView.swift; sourceTree = "<group>"; };
6493D667280ED77F007A76FB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
6495D7032F48CFC50060512B /* ChannelMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelMembersView.swift; sourceTree = "<group>"; };
@@ -594,12 +591,14 @@
64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = "<group>"; };
64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.11-LxOKYAQgszM3XevVjyMHum-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.1.0.11-LxOKYAQgszM3XevVjyMHum-ghc9.6.3.a"; sourceTree = "<group>"; };
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.11-LxOKYAQgszM3XevVjyMHum.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.1.0.11-LxOKYAQgszM3XevVjyMHum.a"; sourceTree = "<group>"; };
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.14-FQI9ONxXS3x5DhsgnqtwJs-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.1.0.14-FQI9ONxXS3x5DhsgnqtwJs-ghc9.6.3.a"; sourceTree = "<group>"; };
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.14-FQI9ONxXS3x5DhsgnqtwJs.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.1.0.14-FQI9ONxXS3x5DhsgnqtwJs.a"; sourceTree = "<group>"; };
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = "<group>"; };
64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = "<group>"; };
64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactLearnMore.swift; sourceTree = "<group>"; };
64D1B0A13070AA0000C0DE01 /* BadgesCheckOrderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgesCheckOrderView.swift; sourceTree = "<group>"; };
64D1B0A33070AA0000C0DE01 /* BadgesPurchaseStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgesPurchaseStateView.swift; sourceTree = "<group>"; };
64DAE1502809D9F5000DA960 /* FileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = "<group>"; };
64E5E3622DF71A4E00A4D530 /* ContextContactRequestActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextContactRequestActionsView.swift; sourceTree = "<group>"; };
64E5E3662DFC16A900A4D530 /* ContextProfilePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextProfilePickerView.swift; sourceTree = "<group>"; };
@@ -609,6 +608,7 @@
64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServersSummaryView.swift; sourceTree = "<group>"; };
64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncognitoHelp.swift; sourceTree = "<group>"; };
64FC8F9C2E3B6DEF0068F384 /* ContextMemberContactActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMemberContactActionsView.swift; sourceTree = "<group>"; };
6F3B2A1C2E8D4F0A9B7C1D22 /* MarkdownFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownFileView.swift; sourceTree = "<group>"; };
8C01E9BF2C8EFBB6008A4B0A /* objc.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = objc.h; sourceTree = "<group>"; };
8C01E9C02C8EFC33008A4B0A /* objc.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = objc.m; sourceTree = "<group>"; };
8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
@@ -769,8 +769,8 @@
64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */,
64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */,
64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */,
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.11-LxOKYAQgszM3XevVjyMHum-ghc9.6.3.a in Frameworks */,
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.11-LxOKYAQgszM3XevVjyMHum.a in Frameworks */,
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.14-FQI9ONxXS3x5DhsgnqtwJs-ghc9.6.3.a in Frameworks */,
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.14-FQI9ONxXS3x5DhsgnqtwJs.a in Frameworks */,
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -857,8 +857,8 @@
64C829992D54AEEE006B9E89 /* libffi.a */,
64C829982D54AEED006B9E89 /* libgmp.a */,
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */,
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.11-LxOKYAQgszM3XevVjyMHum-ghc9.6.3.a */,
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.11-LxOKYAQgszM3XevVjyMHum.a */,
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.14-FQI9ONxXS3x5DhsgnqtwJs-ghc9.6.3.a */,
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.14-FQI9ONxXS3x5DhsgnqtwJs.a */,
);
path = Libraries;
sourceTree = "<group>";
@@ -2157,7 +2157,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 362;
CURRENT_PROJECT_VERSION = 363;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -2207,7 +2207,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 362;
CURRENT_PROJECT_VERSION = 363;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -2249,7 +2249,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 362;
CURRENT_PROJECT_VERSION = 363;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
@@ -2269,7 +2269,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 362;
CURRENT_PROJECT_VERSION = 363;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
@@ -2294,7 +2294,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 362;
CURRENT_PROJECT_VERSION = 363;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = s;
@@ -2331,7 +2331,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 362;
CURRENT_PROJECT_VERSION = 363;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_CODE_COVERAGE = NO;
@@ -2368,7 +2368,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 362;
CURRENT_PROJECT_VERSION = 363;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2419,7 +2419,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 362;
CURRENT_PROJECT_VERSION = 363;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2473,7 +2473,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 362;
CURRENT_PROJECT_VERSION = 363;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -2507,7 +2507,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 362;
CURRENT_PROJECT_VERSION = 363;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
+2 -2
View File
@@ -75,7 +75,7 @@ public let groupAppDefaults: [String: Any] = [
GROUP_DEFAULT_NTF_ENABLE_PERIODIC: false,
GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS: OnionHosts.no.rawValue,
GROUP_DEFAULT_NETWORK_SESSION_MODE: TransportSessionMode.session.rawValue,
GROUP_DEFAULT_NETWORK_SMP_PROXY_MODE: SMPProxyMode.unknown.rawValue,
GROUP_DEFAULT_NETWORK_SMP_PROXY_MODE: SMPProxyMode.always.rawValue,
GROUP_DEFAULT_NETWORK_SMP_PROXY_FALLBACK: SMPProxyFallback.allowProtected.rawValue,
GROUP_DEFAULT_NETWORK_SMP_WEB_PORT_SERVERS: SMPWebPortServers.preset.rawValue,
GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT_BACKGROUND: NetCfg.defaults.tcpConnectTimeout.backgroundTimeout,
@@ -258,7 +258,7 @@ public let networkSessionModeGroupDefault = EnumDefault<TransportSessionMode>(
public let networkSMPProxyModeGroupDefault = EnumDefault<SMPProxyMode>(
defaults: groupDefaults,
forKey: GROUP_DEFAULT_NETWORK_SMP_PROXY_MODE,
withDefault: .unknown
withDefault: .always
)
public let networkSMPProxyFallbackGroupDefault = EnumDefault<SMPProxyFallback>(
+4 -7
View File
@@ -2043,7 +2043,7 @@ chat item action */
/* No comment provided by engineer. */
"Error saving user password" = "Грешка при запазване на потребителска парола";
/* No comment provided by engineer. */
/* alert message */
"Error scanning code: %@" = "Грешка при сканиране на кода: %@";
/* No comment provided by engineer. */
@@ -2503,7 +2503,7 @@ server test error */
/* alert title */
"Invalid name!" = "Невалидно име!";
/* No comment provided by engineer. */
/* alert title */
"Invalid QR code" = "Невалиден QR код";
/* No comment provided by engineer. */
@@ -2617,7 +2617,7 @@ server test error */
/* No comment provided by engineer. */
"KeyChain error" = "KeyChain грешка";
/* No comment provided by engineer. */
/* file alert title */
"Large file!" = "Голям файл!";
/* badge alert button */
@@ -3317,7 +3317,7 @@ alert button */
"Receiving via" = "Получаване чрез";
/* No comment provided by engineer. */
"Recent history and improved [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)." = "Скорошна история и подобрен [bot за директория за групи](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPd jdLW3%23%2F%3Fv%3D1-2% 26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).";
"Recent history and improved [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)." = "Скорошна история и подобрен [bot за директория за групи](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).";
/* No comment provided by engineer. */
"Recipient(s) can't see who this message is from." = "Получателят(ите) не могат да видят от кого е това съобщение.";
@@ -4582,9 +4582,6 @@ server test failure */
/* No comment provided by engineer. */
"Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "Освен ако вашият контакт не е изтрил връзката или този линк вече е бил използван, това може да е грешка - моля, докладвайте.\nЗа да се свържете, моля, помолете вашия контакт да създаде друг линк за връзка и проверете дали имате стабилна мрежова връзка.";
/* No comment provided by engineer. */
"Your contact sent a file that is larger than currently supported maximum size (%@)." = "Вашият контакт изпрати файл, който е по-голям от поддържания в момента максимален размер (%@).";
/* No comment provided by engineer. */
"Your contacts can allow full message deletion." = "Вашите контакти могат да позволят пълното изтриване на съобщението.";
+92 -7
View File
@@ -10,6 +10,9 @@
/* No comment provided by engineer. */
"- more stable message delivery.\n- a bit better groups.\n- and more!" = "- více stabilní doručování zpráv.\n- o trochu lepší skupiny.\n- a více!";
/* No comment provided by engineer. */
"- opt-in to send link previews.\n- prevent hyperlink phishing.\n- remove link tracking." = "- opt-in odesílání náhledu odkazů.\n- zabránění phishingu hyperlinků.\n- odstranění sledování odkazů.";
/* No comment provided by engineer. */
"- optionally notify deleted contacts.\n- profile names with spaces.\n- and more!" = "- volitelně informuje smazané kontakty.\n- profilová jména s mezerami.\n- a více!";
@@ -19,6 +22,9 @@
/* No comment provided by engineer. */
"!1 colored!" = "!1 barevný!";
/* chat link info line */
"(from owner)" = "(od majitele)";
/* No comment provided by engineer. */
"(new)" = "(nový)";
@@ -46,6 +52,9 @@
/* No comment provided by engineer. */
"**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." = "**Nejsoukromější**: nepoužívejte server oznámení SimpleX Chat, pravidelně kontrolujte zprávy na pozadí (závisí na tom, jak často aplikaci používáte).";
/* No comment provided by engineer. */
"**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**Čtěte prosím**: Jako ochrana zabezpečení, použití stejné databáze na dvou zařízeních rozbije dešifrování zpráv z vašich připojení.";
/* No comment provided by engineer. */
"**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Upozornění**: Pokud heslo ztratíte, NEBUDETE jej moci obnovit ani změnit.";
@@ -106,6 +115,9 @@
/* No comment provided by engineer. */
"%@ downloaded" = "%@ staženo";
/* badge alert */
"%@ invested in SimpleX Chat crowdfunding." = "%@ investoval do SimpleX Chat crowdfunding.";
/* notification title */
"%@ is connected!" = "%@ je připojen!";
@@ -121,6 +133,9 @@
/* No comment provided by engineer. */
"%@ servers" = "%@ servery";
/* badge alert */
"%@ supports SimpleX Chat." = "%@ podporuje SimpleX Chat.";
/* No comment provided by engineer. */
"%@ uploaded" = "%@ nahrán";
@@ -139,6 +154,12 @@
/* copied message info */
"%@:" = "%@:";
/* countdown */
"%02d hrs %02d min %02d sec" = "%1$02d hod %2$02d min %3$02d sek";
/* time interval */
"%d day" = "%d den";
/* time interval */
"%d days" = "%d dní";
@@ -166,6 +187,27 @@
/* time interval */
"%d months" = "%d měsíce";
/* channel owners count */
"%d owner" = "%d majitel";
/* channel owners count */
"%d owners" = "%d majitelů";
/* channel members count */
"%d owners & contributors" = "%d majitelů a přispěvatelů";
/* channel relay bar
channel subscriber relay bar */
"%d relays failed" = "%d relé selhalo";
/* channel relay bar
channel subscriber relay bar */
"%d relays not active" = "%d relé není aktivní";
/* channel relay bar
channel subscriber relay bar */
"%d relays removed" = "%d relé odstraněno";
/* time interval */
"%d sec" = "%d sek";
@@ -175,15 +217,43 @@
/* integrity error chat item */
"%d skipped message(s)" = "%d přeskočené zprávy";
/* channel subscriber count */
"%d subscribers" = "%d odběratelů";
/* time interval */
"%d weeks" = "%d týdnů";
/* channel relay bar */
"%d/%d relays active, %d errors" = "%1$d/%2$d relé aktivní, %3$d chyb";
/* channel creation progress with errors
channel relay bar */
"%d/%d relays active, %d failed" = "%1$d/%2$d relé aktivní, %3$d selhalo";
/* channel relay bar */
"%d/%d relays active, %d removed" = "%1$d/%2$d relé aktivní, %3$d odstraněno";
/* channel subscriber relay bar progress */
"%d/%d relays connected" = "%1$d/%2$d relé připojeno";
/* channel subscriber relay bar */
"%d/%d relays connected, %d errors" = "%1$d/%2$d relé připojeno, %3$d chyb";
/* channel subscriber relay bar */
"%d/%d relays connected, %d failed" = "%1$d/%2$d relé připojeno, %3$d selhalo";
/* channel subscriber relay bar */
"%d/%d relays connected, %d removed" = "%1$d/%2$d relé připojeno, %3$d odstraněno";
/* No comment provided by engineer. */
"%lld" = "%lld";
/* No comment provided by engineer. */
"%lld %@" = "%lld %@";
/* No comment provided by engineer. */
"%lld channel events" = "%lld událostí kanálu";
/* No comment provided by engineer. */
"%lld contact(s) selected" = "%lld kontakt(y) vybrané";
@@ -212,7 +282,7 @@
"%lld minutes" = "%lld minut";
/* No comment provided by engineer. */
"%lld new interface languages" = "%d nové jazyky rozhraní";
"%lld new interface languages" = "%lld nové jazyky rozhraní";
/* No comment provided by engineer. */
"%lld seconds" = "%lld vteřin";
@@ -280,6 +350,9 @@ time interval */
/* delete after time */
"1 year" = "1 rok";
/* No comment provided by engineer. */
"1-time link" = "Jednorázový odkaz";
/* No comment provided by engineer. */
"1-time link can be used *with one contact only* - share in person or via any messenger." = "Jednorázový odkaz lze použít *pouze s jedním kontaktem* - sdílejte osobně nebo prostřednictvím libovolného komunikátoru.";
@@ -295,6 +368,9 @@ time interval */
/* No comment provided by engineer. */
"A few more things" = "Ještě pár věcí";
/* No comment provided by engineer. */
"A link for one person to connect" = "Odkaz k připojení pro jednu osobu";
/* notification title */
"A new contact" = "Nový kontakt";
@@ -325,6 +401,9 @@ time interval */
/* No comment provided by engineer. */
"above, then choose:" = "výše, pak vyberte:";
/* No comment provided by engineer. */
"Accent" = "Zbarvení";
/* accept contact request via notification
accept incoming call via notification
alert action
@@ -362,12 +441,21 @@ swipe action */
/* No comment provided by engineer. */
"Accepted conditions" = "Přijaté podmínky";
/* No comment provided by engineer. */
"Acknowledged" = "Potvrzeno";
/* No comment provided by engineer. */
"Acknowledgement errors" = "Chyba potvrzení";
/* token status text */
"Active" = "Aktivní";
/* No comment provided by engineer. */
"Active connections" = "Aktivní spojení";
/* No comment provided by engineer. */
"Add" = "Přidat";
/* No comment provided by engineer. */
"Add friends" = "Přidat přátele";
@@ -846,7 +934,7 @@ set passcode view */
"changed address for you" = "změnila se vaše adresa";
/* rcv group event chat item */
"changed role of %@ to %@" = "Role %1$@ byla změněna na %2$ @";
"changed role of %@ to %@" = "Role %1$@ byla změněna na %2$@";
/* rcv group event chat item */
"changed your role to %@" = "změnil vaši roli na %@";
@@ -2086,7 +2174,7 @@ server test error */
/* No comment provided by engineer. */
"KeyChain error" = "Chyba klíčenky";
/* No comment provided by engineer. */
/* file alert title */
"Large file!" = "Velký soubor!";
/* badge alert button */
@@ -2943,7 +3031,7 @@ chat item action */
"Sent at" = "Posláno v";
/* copied message info */
"Sent at: %@" = "Posláno v: % @";
"Sent at: %@" = "Posláno v: %@";
/* message info title */
"Sent message" = "Poslaná zpráva";
@@ -3658,9 +3746,6 @@ server test failure */
/* No comment provided by engineer. */
"Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "Pokud váš kontakt neodstranil připojení nebo tento odkaz již nebyl použit, může se jednat o chybu – nahlaste ji.\nChcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu na připojení a zkontrolujte, zda máte stabilní připojení k síti.";
/* No comment provided by engineer. */
"Your contact sent a file that is larger than currently supported maximum size (%@)." = "Kontakt odeslal soubor, který je větší než aktuálně podporovaná maximální velikost (%@).";
/* No comment provided by engineer. */
"Your contacts can allow full message deletion." = "Vaše kontakty mohou povolit úplné mazání zpráv.";
+24 -9
View File
@@ -160,6 +160,12 @@
/* badge alert */
"%1$@ supported SimpleX Chat. The badge expired on %2$@." = "%1$@ hat SimpleX Chat unterstützt. Das Abzeichen ist am %2$@ abgelaufen.";
/* countdown */
"%02d hrs %02d min %02d sec" = "%1$02d h %2$02d min %3$02d s";
/* time interval */
"%d day" = "%d Tag";
/* time interval */
"%d days" = "%d Tage";
@@ -1814,6 +1820,9 @@ server test step */
/* No comment provided by engineer. */
"creator" = "Ersteller";
/* No comment provided by engineer. */
"Crowdfunding investors can reserve names before the sale starts: [simplex.domains](https://simplex.domains/)" = "Crowdfunding-Investoren können vor Verkaufsbeginn Namen reservieren: [simplex.domains](https://simplex.domains/)";
/* No comment provided by engineer. */
"Crowdfunding on Wefunder" = "Crowdfunding auf Wefunder";
@@ -2757,7 +2766,7 @@ chat item action */
/* No comment provided by engineer. */
"Error saving user password" = "Fehler beim Speichern des Benutzer-Passworts";
/* No comment provided by engineer. */
/* alert message */
"Error scanning code: %@" = "Fehler beim Scannen des Codes: %@";
/* No comment provided by engineer. */
@@ -3063,6 +3072,9 @@ servers warning */
/* No comment provided by engineer. */
"Forwarded" = "Weitergeleitet";
/* No comment provided by engineer. */
"forwarded from" = "weitergeleitet aus";
/* No comment provided by engineer. */
"Forwarded from" = "Weitergeleitet aus";
@@ -3465,7 +3477,7 @@ servers warning */
/* alert title */
"Invalid name!" = "Ungültiger Name!";
/* No comment provided by engineer. */
/* alert title */
"Invalid QR code" = "Ungültiger QR-Code";
/* alert title */
@@ -3618,7 +3630,7 @@ servers warning */
/* No comment provided by engineer. */
"KeyChain error" = "KeyChain Fehler";
/* No comment provided by engineer. */
/* file alert title */
"Large file!" = "Große Datei!";
/* badge alert button */
@@ -5896,6 +5908,9 @@ copied message info */
/* alert title */
"SimpleX name not verified" = "SimpleX-Name ist nicht verifiziert";
/* No comment provided by engineer. */
"SimpleX name sale starts in" = "Der Verkauf von SimpleX-Namen startet in";
/* simplex link type */
"SimpleX one-time invitation" = "SimpleX-Einmal-Einladung";
@@ -6598,6 +6613,9 @@ alert subtitle */
/* conn error description */
"Unsupported connection link" = "Verbindungs-Link wird nicht unterstützt";
/* No comment provided by engineer. */
"until you can register a SimpleX domain" = "bis Sie eine SimpleX-Domain registrieren können";
/* badge alert title */
"Unverified badge" = "Abzeichen nicht verifiziert";
@@ -6619,6 +6637,9 @@ alert subtitle */
/* No comment provided by engineer. */
"Update settings?" = "Einstellungen aktualisieren?";
/* No comment provided by engineer. */
"Update the app to register a SimpleX domain" = "Aktualisieren Sie die App, um eine SimpleX-Domain registrieren zu können";
/* rcv group event chat item */
"updated channel profile" = "Kanalprofil aktualisiert";
@@ -7142,9 +7163,6 @@ alert title */
/* No comment provided by engineer. */
"You can still view conversation with %@ in the list of chats." = "Sie können in der Chat-Liste weiterhin die Unterhaltung mit %@ einsehen.";
/* badge alert */
"You can support SimpleX starting from v7 of the app." = "Sie können SimpleX ab der App-Version v7 unterstützen.";
/* No comment provided by engineer. */
"You can turn on SimpleX Lock via Settings." = "Sie können die SimpleX-Sperre über die Einstellungen aktivieren.";
@@ -7310,9 +7328,6 @@ alert title */
/* No comment provided by engineer. */
"Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "Ihr Kontakt hat diesen Link entfernt oder es war ein Einmal‑Link, welcher bereits verwendet wurde.\nUm sich zu verbinden, bitten Sie Ihren Kontakt, einen neuen Link zu erstellen.";
/* No comment provided by engineer. */
"Your contact sent a file that is larger than currently supported maximum size (%@)." = "Ihr Kontakt hat eine Datei gesendet, die größer ist als die derzeit unterstützte maximale Größe (%@).";
/* No comment provided by engineer. */
"Your contacts can allow full message deletion." = "Ihre Kontakte können die unwiederbringliche Löschung von Nachrichten erlauben.";
+27 -12
View File
@@ -160,6 +160,12 @@
/* badge alert */
"%1$@ supported SimpleX Chat. The badge expired on %2$@." = "%1$@ ha apoyado a SimpleX Chat. La insignia caducó el %2$@.";
/* countdown */
"%02d hrs %02d min %02d sec" = "%1$02d hrs %2$02d min %3$02d seg";
/* time interval */
"%d day" = "%d día";
/* time interval */
"%d days" = "%d día(s)";
@@ -794,7 +800,7 @@ swipe action */
"App icon" = "Icono de la aplicación";
/* No comment provided by engineer. */
"App passcode" = "Código de acceso de la aplicación";
"App passcode" = "Código de acceso";
/* No comment provided by engineer. */
"App passcode is replaced with self-destruct passcode." = "El código de acceso será reemplazado por código de autodestrucción.";
@@ -1638,7 +1644,7 @@ server test step */
"Connection with desktop stopped" = "La conexión con el escritorio (desktop) se ha parado";
/* connection information */
"connection:%@" = "conexión: % @";
"connection:%@" = "conexión: %@";
/* No comment provided by engineer. */
"Connections" = "Conexiones";
@@ -1814,6 +1820,9 @@ server test step */
/* No comment provided by engineer. */
"creator" = "creador";
/* No comment provided by engineer. */
"Crowdfunding investors can reserve names before the sale starts: [simplex.domains](https://simplex.domains/)" = "Los inversores de crowdfunding pueden reservar nombres antes del comienzo de la venta: [simplex.domains](https://simplex.domains/)";
/* No comment provided by engineer. */
"Crowdfunding on Wefunder" = "Crowdfunding en Wefunder";
@@ -2136,7 +2145,7 @@ alert button */
"Desktop address" = "Dirección ordenador";
/* No comment provided by engineer. */
"Desktop app version %@ is not compatible with this app." = "La versión de aplicación del ordenador %\" no es compatible con esta aplicación.";
"Desktop app version %@ is not compatible with this app." = "La versión de aplicación del ordenador %@ no es compatible con esta aplicación.";
/* No comment provided by engineer. */
"Desktop devices" = "Ordenadores";
@@ -2757,7 +2766,7 @@ chat item action */
/* No comment provided by engineer. */
"Error saving user password" = "Error al guardar contraseña de usuario";
/* No comment provided by engineer. */
/* alert message */
"Error scanning code: %@" = "Error al escanear código: %@";
/* No comment provided by engineer. */
@@ -3063,6 +3072,9 @@ servers warning */
/* No comment provided by engineer. */
"Forwarded" = "Reenviado";
/* No comment provided by engineer. */
"forwarded from" = "reenviado desde";
/* No comment provided by engineer. */
"Forwarded from" = "Reenviado por";
@@ -3465,7 +3477,7 @@ servers warning */
/* alert title */
"Invalid name!" = "¡Nombre no válido!";
/* No comment provided by engineer. */
/* alert title */
"Invalid QR code" = "Código QR no válido";
/* alert title */
@@ -3618,7 +3630,7 @@ servers warning */
/* No comment provided by engineer. */
"KeyChain error" = "Error Keychain";
/* No comment provided by engineer. */
/* file alert title */
"Large file!" = "¡Archivo grande!";
/* badge alert button */
@@ -5896,6 +5908,9 @@ copied message info */
/* alert title */
"SimpleX name not verified" = "Nombre SimpleX no verificado";
/* No comment provided by engineer. */
"SimpleX name sale starts in" = "La venta de nombres SimpleX comienza en";
/* simplex link type */
"SimpleX one-time invitation" = "Invitación SimpleX de un uso";
@@ -6598,6 +6613,9 @@ alert subtitle */
/* conn error description */
"Unsupported connection link" = "Enlace de conexión no compatible";
/* No comment provided by engineer. */
"until you can register a SimpleX domain" = "hasta que puedas registrar un dominio SimpleX";
/* badge alert title */
"Unverified badge" = "Insignia sin verificar";
@@ -6619,6 +6637,9 @@ alert subtitle */
/* No comment provided by engineer. */
"Update settings?" = "¿Actualizar configuración?";
/* No comment provided by engineer. */
"Update the app to register a SimpleX domain" = "Actualiza la app para registrar un dominio SimpleX";
/* rcv group event chat item */
"updated channel profile" = "perfil del canal actualizado";
@@ -7142,9 +7163,6 @@ alert title */
/* No comment provided by engineer. */
"You can still view conversation with %@ in the list of chats." = "Aún puedes ver la conversación con %@ en la lista de chats.";
/* badge alert */
"You can support SimpleX starting from v7 of the app." = "Puedes apoyar SimpleX desde la versión 7.";
/* No comment provided by engineer. */
"You can turn on SimpleX Lock via Settings." = "Puedes activar el Bloqueo SimpleX a través de Configuración.";
@@ -7310,9 +7328,6 @@ alert title */
/* No comment provided by engineer. */
"Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "A menos que tu contacto haya eliminado la conexión o el enlace se haya usado, podría ser un error. Por favor, notifícalo.\nPara conectarte pide a tu contacto que cree otro enlace y comprueba la conexión de red.";
/* No comment provided by engineer. */
"Your contact sent a file that is larger than currently supported maximum size (%@)." = "El contacto ha enviado un archivo mayor al máximo admitido (%@).";
/* No comment provided by engineer. */
"Your contacts can allow full message deletion." = "Tus contactos pueden permitir la eliminación completa de mensajes.";
+5 -8
View File
@@ -53,19 +53,19 @@
"#secret#" = "#salaisuus#";
/* No comment provided by engineer. */
"%@" = "% @";
"%@" = "%@";
/* No comment provided by engineer. */
"%@ (current)" = "%@ (nykyinen)";
/* copied message info */
"%@ (current):" = "% (nykyinen):";
"%@ (current):" = "%@ (nykyinen):";
/* No comment provided by engineer. */
"%@ / %@" = "%@ / % @";
"%@ / %@" = "%@ / %@";
/* No comment provided by engineer. */
"%@ %@" = "%@ % @";
"%@ %@" = "%@ %@";
/* No comment provided by engineer. */
"%@ and %@ connected" = "%@ ja %@ yhdistetty";
@@ -1753,7 +1753,7 @@ server test error */
/* No comment provided by engineer. */
"KeyChain error" = "Avainnipun virhe";
/* No comment provided by engineer. */
/* file alert title */
"Large file!" = "Suuri tiedosto!";
/* badge alert button */
@@ -3291,9 +3291,6 @@ server test failure */
/* No comment provided by engineer. */
"Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "Ellei yhteyshenkilösi poistanut yhteyttä tai tämä linkki oli jo käytössä, se voi olla virhe - ilmoita siitä.\nJos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja tarkista, että verkkoyhteytesi on vakaa.";
/* No comment provided by engineer. */
"Your contact sent a file that is larger than currently supported maximum size (%@)." = "Yhteyshenkilösi lähetti tiedoston, joka on suurempi kuin tällä hetkellä tuettu enimmäiskoko (%@).";
/* No comment provided by engineer. */
"Your contacts can allow full message deletion." = "Kontaktisi voivat sallia viestien täydellisen poistamisen.";
+24 -9
View File
@@ -160,6 +160,12 @@
/* badge alert */
"%1$@ supported SimpleX Chat. The badge expired on %2$@." = "%1$@ a soutenu SimpleX Chat. Le badge a expiré le %2$@.";
/* countdown */
"%02d hrs %02d min %02d sec" = "%1$02d h %2$02d min %3$02d s";
/* time interval */
"%d day" = "%d jour";
/* time interval */
"%d days" = "%d jours";
@@ -1814,6 +1820,9 @@ server test step */
/* No comment provided by engineer. */
"creator" = "créateur";
/* No comment provided by engineer. */
"Crowdfunding investors can reserve names before the sale starts: [simplex.domains](https://simplex.domains/)" = "Les investisseurs du financement participatif peuvent réserver des noms avant le début de la vente : [simplex.domains](https://simplex.domains/)";
/* No comment provided by engineer. */
"Crowdfunding on Wefunder" = "Financement participatif sur Wefunder";
@@ -2757,7 +2766,7 @@ chat item action */
/* No comment provided by engineer. */
"Error saving user password" = "Erreur d'enregistrement du mot de passe de l'utilisateur";
/* No comment provided by engineer. */
/* alert message */
"Error scanning code: %@" = "Erreur lors du scan du code : %@";
/* No comment provided by engineer. */
@@ -3063,6 +3072,9 @@ servers warning */
/* No comment provided by engineer. */
"Forwarded" = "Transféré";
/* No comment provided by engineer. */
"forwarded from" = "transféré depuis";
/* No comment provided by engineer. */
"Forwarded from" = "Transféré depuis";
@@ -3465,7 +3477,7 @@ servers warning */
/* alert title */
"Invalid name!" = "Nom invalide !";
/* No comment provided by engineer. */
/* alert title */
"Invalid QR code" = "Code QR invalide";
/* alert title */
@@ -3618,7 +3630,7 @@ servers warning */
/* No comment provided by engineer. */
"KeyChain error" = "Erreur du trousseau de clés";
/* No comment provided by engineer. */
/* file alert title */
"Large file!" = "Fichier trop lourd !";
/* badge alert button */
@@ -5896,6 +5908,9 @@ copied message info */
/* alert title */
"SimpleX name not verified" = "Nom SimpleX non vérifié";
/* No comment provided by engineer. */
"SimpleX name sale starts in" = "La vente du nom SimpleX commence dans";
/* simplex link type */
"SimpleX one-time invitation" = "Invitation unique SimpleX";
@@ -6598,6 +6613,9 @@ alert subtitle */
/* conn error description */
"Unsupported connection link" = "Lien de connexion non pris en charge";
/* No comment provided by engineer. */
"until you can register a SimpleX domain" = "jusqu’à ce que vous puissiez enregistrer un domaine SimpleX";
/* badge alert title */
"Unverified badge" = "Badge non vérifié";
@@ -6619,6 +6637,9 @@ alert subtitle */
/* No comment provided by engineer. */
"Update settings?" = "Mettre à jour les paramètres ?";
/* No comment provided by engineer. */
"Update the app to register a SimpleX domain" = "Mettez à jour l’application pour enregistrer un domaine SimpleX";
/* rcv group event chat item */
"updated channel profile" = "profil du canal mis à jour";
@@ -7142,9 +7163,6 @@ alert title */
/* No comment provided by engineer. */
"You can still view conversation with %@ in the list of chats." = "Vous pouvez toujours voir la conversation avec %@ dans la liste des conversations.";
/* badge alert */
"You can support SimpleX starting from v7 of the app." = "Vous pouvez soutenir SimpleX à partir de la version 7 de l'application.";
/* No comment provided by engineer. */
"You can turn on SimpleX Lock via Settings." = "Vous pouvez activer le Verrouillage SimpleX dans les Paramètres.";
@@ -7310,9 +7328,6 @@ alert title */
/* No comment provided by engineer. */
"Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "A moins que votre contact ait supprimé la connexion ou que ce lien ait déjà été utilisé, il peut s'agir d'un bug - veuillez le signaler.\nPour vous connecter, veuillez demander à votre contact de créer un autre lien de connexion et vérifiez que vous disposez d'une connexion réseau stable.";
/* No comment provided by engineer. */
"Your contact sent a file that is larger than currently supported maximum size (%@)." = "Votre contact a envoyé un fichier plus grand que la taille maximale supportée actuellement(%@).";
/* No comment provided by engineer. */
"Your contacts can allow full message deletion." = "Vos contacts peuvent autoriser la suppression complète des messages.";
+26 -11
View File
@@ -160,6 +160,12 @@
/* badge alert */
"%1$@ supported SimpleX Chat. The badge expired on %2$@." = "%1$@ támogatta a SimpleX Chatet. A kitűző lejárt ekkor: %2$@.";
/* countdown */
"%02d hrs %02d min %02d sec" = "%1$02d óra %2$02d perc %3$02d mp";
/* time interval */
"%d day" = "%d nap";
/* time interval */
"%d days" = "%d nap";
@@ -283,7 +289,7 @@ channel relay bar */
"%lld messages marked deleted" = "%lld üzenet megjelölve törlésre";
/* No comment provided by engineer. */
"%lld messages moderated by %@" = "%@ %lld üzenetet moderált";
"%lld messages moderated by %@" = "%2$@ %1$lld üzenetet moderált";
/* No comment provided by engineer. */
"%lld minutes" = "%lld perc";
@@ -1814,6 +1820,9 @@ server test step */
/* No comment provided by engineer. */
"creator" = "készítő";
/* No comment provided by engineer. */
"Crowdfunding investors can reserve names before the sale starts: [simplex.domains](https://simplex.domains/)" = "A közösségi finanszírozásban részt vevő befektetők már az értékesítés megkezdése előtt lefoglalhatnak SimpleX-neveket: [simplex.domains](https://simplex.domains/)";
/* No comment provided by engineer. */
"Crowdfunding on Wefunder" = "Közösségi finanszírozás a Wefunder oldalon";
@@ -2757,7 +2766,7 @@ chat item action */
/* No comment provided by engineer. */
"Error saving user password" = "Hiba történt a felhasználó jelszavának mentésekor";
/* No comment provided by engineer. */
/* alert message */
"Error scanning code: %@" = "Hiba történt a kód beolvasásakor: %@";
/* No comment provided by engineer. */
@@ -3063,6 +3072,9 @@ servers warning */
/* No comment provided by engineer. */
"Forwarded" = "Továbbított";
/* No comment provided by engineer. */
"forwarded from" = "továbbítva innen";
/* No comment provided by engineer. */
"Forwarded from" = "Továbbítva innen";
@@ -3465,7 +3477,7 @@ servers warning */
/* alert title */
"Invalid name!" = "Érvénytelen név!";
/* No comment provided by engineer. */
/* alert title */
"Invalid QR code" = "Érvénytelen QR-kód";
/* alert title */
@@ -3618,7 +3630,7 @@ servers warning */
/* No comment provided by engineer. */
"KeyChain error" = "Kulcstartóhiba";
/* No comment provided by engineer. */
/* file alert title */
"Large file!" = "Nagy fájl!";
/* badge alert button */
@@ -5896,6 +5908,9 @@ copied message info */
/* alert title */
"SimpleX name not verified" = "Nincs ellenőrizve a SimpleX-név";
/* No comment provided by engineer. */
"SimpleX name sale starts in" = "SimpleX-nevek értékesítésének kezdete:";
/* simplex link type */
"SimpleX one-time invitation" = "Egyszer használható SimpleX meghívó";
@@ -6598,6 +6613,9 @@ alert subtitle */
/* conn error description */
"Unsupported connection link" = "Nem támogatott kapcsolattartási hivatkozás";
/* No comment provided by engineer. */
"until you can register a SimpleX domain" = "egy SimpleX-domainnév regisztrációjáig";
/* badge alert title */
"Unverified badge" = "Ellenőrizetlen kitűző";
@@ -6619,6 +6637,9 @@ alert subtitle */
/* No comment provided by engineer. */
"Update settings?" = "Frissíti a beállításokat?";
/* No comment provided by engineer. */
"Update the app to register a SimpleX domain" = "Frissítse az alkalmazást egy SimpleX-domainnév regisztrálásához";
/* rcv group event chat item */
"updated channel profile" = "frissítette a csatorna profilját";
@@ -6954,7 +6975,7 @@ alert title */
"When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Ha egy inkognitóprofilt oszt meg valamelyik partnerével, a rendszer ezt az inkognitóprofilt fogja használni azokban a csoportokban, ahová az adott partnere meghívja Önt.";
/* No comment provided by engineer. */
"Why SimpleX is built." = "Miért jött létre a SimpleX?";
"Why SimpleX is built." = "Miért jött létre a SimpleX.";
/* No comment provided by engineer. */
"WiFi" = "Wi-Fi";
@@ -7142,9 +7163,6 @@ alert title */
/* No comment provided by engineer. */
"You can still view conversation with %@ in the list of chats." = "A(z) %@ nevű partnerével folytatott beszélgetéseit továbbra is megtekintheti a csevegések listájában.";
/* badge alert */
"You can support SimpleX starting from v7 of the app." = "A SimpleXet az alkalmazás v7-es verziójától kezdve támogathatja.";
/* No comment provided by engineer. */
"You can turn on SimpleX Lock via Settings." = "A SimpleX-zár az „Adatvédelem és biztonság” menüben kapcsolható be.";
@@ -7310,9 +7328,6 @@ alert title */
/* No comment provided by engineer. */
"Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "Hacsak a partnere nem törölte a kapcsolatot, vagy ez a hivatkozás már használatban volt egyszer, lehet hogy ez egy hiba – jelentse a problémát.\nA kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcsolattartási hivatkozást, és ellenőrizze, hogy a hálózati kapcsolat stabil-e.";
/* No comment provided by engineer. */
"Your contact sent a file that is larger than currently supported maximum size (%@)." = "A partnere a jelenleg támogatott legnagyobb (%@) fájlméretnél nagyobbat küldött.";
/* No comment provided by engineer. */
"Your contacts can allow full message deletion." = "A partnerei engedélyezhetik a teljes üzenet törlését.";
+28 -13
View File
@@ -160,6 +160,12 @@
/* badge alert */
"%1$@ supported SimpleX Chat. The badge expired on %2$@." = "%1$@ ha sostenuto SimpleX Chat. La targhetta è scaduta il %2$@.";
/* countdown */
"%02d hrs %02d min %02d sec" = "%1$02d ore %2$02d min %3$02d sec";
/* time interval */
"%d day" = "%d giorno";
/* time interval */
"%d days" = "%d giorni";
@@ -1638,7 +1644,7 @@ server test step */
"Connection with desktop stopped" = "Connessione con il desktop fermata";
/* connection information */
"connection:%@" = "connessione:% @";
"connection:%@" = "connessione:%@";
/* No comment provided by engineer. */
"Connections" = "Connessioni";
@@ -1814,6 +1820,9 @@ server test step */
/* No comment provided by engineer. */
"creator" = "creatore";
/* No comment provided by engineer. */
"Crowdfunding investors can reserve names before the sale starts: [simplex.domains](https://simplex.domains/)" = "Gli investitori della raccolta fondi possono prenotare i nomi prima che inizi la vendita: [simplex.domains](https://simplex.domains/)";
/* No comment provided by engineer. */
"Crowdfunding on Wefunder" = "Raccolta fondi su Wefunder";
@@ -2757,7 +2766,7 @@ chat item action */
/* No comment provided by engineer. */
"Error saving user password" = "Errore nel salvataggio della password utente";
/* No comment provided by engineer. */
/* alert message */
"Error scanning code: %@" = "Errore di scansione del codice: %@";
/* No comment provided by engineer. */
@@ -3063,6 +3072,9 @@ servers warning */
/* No comment provided by engineer. */
"Forwarded" = "Inoltrato";
/* No comment provided by engineer. */
"forwarded from" = "inoltrato da";
/* No comment provided by engineer. */
"Forwarded from" = "Inoltrato da";
@@ -3465,7 +3477,7 @@ servers warning */
/* alert title */
"Invalid name!" = "Nome non valido!";
/* No comment provided by engineer. */
/* alert title */
"Invalid QR code" = "Codice QR non valido";
/* alert title */
@@ -3544,10 +3556,10 @@ servers warning */
"It allows having many anonymous connections without any shared data between them in a single chat profile." = "Permette di avere molte connessioni anonime senza dati condivisi tra di loro in un unico profilo di chat.";
/* No comment provided by engineer. */
"It can happen when you or your connection used the old database backup." = "Può accadere quando tu o il tuo contatto avete usato il backup del database vecchio.";
"It can happen when you or your connection used the old database backup." = "Può accadere quando tu o il tuo contatto avete usato il backup del vecchio database.";
/* No comment provided by engineer. */
"It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Può accadere quando:\n1. I messaggi sono scaduti sul client mittente dopo 2 giorni o sul server dopo 30 giorni.\n2. La decifrazione del messaggio è fallita, perché tu o il tuo contatto avete usato un backup del database vecchio.\n3. La connessione è stata compromessa.";
"It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Può accadere quando:\n1. I messaggi sono scaduti sul client mittente dopo 2 giorni o sul server dopo 30 giorni.\n2. La decifrazione del messaggio è fallita, perché tu o il tuo contatto avete usato un backup del vecchio database.\n3. La connessione è stata compromessa.";
/* No comment provided by engineer. */
"It protects your IP address and connections." = "Protegge il tuo indirizzo IP e le connessioni.";
@@ -3618,7 +3630,7 @@ servers warning */
/* No comment provided by engineer. */
"KeyChain error" = "Errore del portachiavi";
/* No comment provided by engineer. */
/* file alert title */
"Large file!" = "File grande!";
/* badge alert button */
@@ -5896,6 +5908,9 @@ copied message info */
/* alert title */
"SimpleX name not verified" = "Nome SimpleX non verificato";
/* No comment provided by engineer. */
"SimpleX name sale starts in" = "La vendita di nomi SimpleX inizia in";
/* simplex link type */
"SimpleX one-time invitation" = "Invito SimpleX una tantum";
@@ -6598,6 +6613,9 @@ alert subtitle */
/* conn error description */
"Unsupported connection link" = "Link di connessione non supportato";
/* No comment provided by engineer. */
"until you can register a SimpleX domain" = "fino a quando potrai registrare un dominio SimpleX";
/* badge alert title */
"Unverified badge" = "Targhetta non verificata";
@@ -6619,6 +6637,9 @@ alert subtitle */
/* No comment provided by engineer. */
"Update settings?" = "Aggiornare le impostazioni?";
/* No comment provided by engineer. */
"Update the app to register a SimpleX domain" = "Aggiorna l'app per registrare un dominio SimpleX";
/* rcv group event chat item */
"updated channel profile" = "profilo del canale aggiornato";
@@ -6629,7 +6650,7 @@ alert subtitle */
"updated group profile" = "ha aggiornato il profilo del gruppo";
/* profile update event chat item */
"updated profile" = "profilo aggiornato";
"updated profile" = "ha aggiornato il profilo";
/* No comment provided by engineer. */
"Updating settings will re-connect the client to all servers." = "L'aggiornamento delle impostazioni riconnetterà il client a tutti i server.";
@@ -7142,9 +7163,6 @@ alert title */
/* No comment provided by engineer. */
"You can still view conversation with %@ in the list of chats." = "Puoi ancora vedere la conversazione con %@ nell'elenco delle chat.";
/* badge alert */
"You can support SimpleX starting from v7 of the app." = "Puoi sostenere SimpleX dalla versione 7 dell'app.";
/* No comment provided by engineer. */
"You can turn on SimpleX Lock via Settings." = "Puoi attivare SimpleX Lock tramite le impostazioni.";
@@ -7310,9 +7328,6 @@ alert title */
/* No comment provided by engineer. */
"Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "A meno che il tuo contatto non abbia eliminato la connessione o che questo link non sia già stato usato, potrebbe essere un errore; per favore segnalalo.\nPer connetterti, chiedi al tuo contatto di creare un altro link di connessione e controlla di avere una connessione di rete stabile.";
/* No comment provided by engineer. */
"Your contact sent a file that is larger than currently supported maximum size (%@)." = "Il tuo contatto ha inviato un file più grande della dimensione massima attualmente supportata (%@).";
/* No comment provided by engineer. */
"Your contacts can allow full message deletion." = "I tuoi contatti possono consentire l'eliminazione completa dei messaggi.";
+2 -5
View File
@@ -239,7 +239,7 @@ channel relay bar */
"%lld messages marked deleted" = "%lld 件のメッセージが削除されました";
/* No comment provided by engineer. */
"%lld messages moderated by %@" = "%@ により%lld 件のメッセージが検閲されました";
"%lld messages moderated by %@" = "%2$@ により%1$lld 件のメッセージが検閲されました";
/* No comment provided by engineer. */
"%lld minutes" = "%lld 分";
@@ -2098,7 +2098,7 @@ server test error */
/* No comment provided by engineer. */
"KeyChain error" = "キーチェーンのエラー";
/* No comment provided by engineer. */
/* file alert title */
"Large file!" = "大きなファイル!";
/* badge alert button */
@@ -3628,9 +3628,6 @@ server test failure */
/* No comment provided by engineer. */
"Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "連絡先が接続を削除したか、このリンクがすでに使用されている場合を除き、バグである可能性がありますので、報告してください。\n接続するには、連絡先に別の接続リンクを作成するよう依頼し、ネットワーク接続が安定していることを確認してください。";
/* No comment provided by engineer. */
"Your contact sent a file that is larger than currently supported maximum size (%@)." = "連絡先が現在サポートされている最大サイズ (%@) より大きいファイルを送信しました。";
/* No comment provided by engineer. */
"Your contacts can allow full message deletion." = "連絡先がメッセージの完全削除を許可できます。";
+10 -13
View File
@@ -1392,7 +1392,7 @@ server test step */
"Copy error" = "Kopieerfout";
/* No comment provided by engineer. */
"Core version: v%@" = "Core versie: v% @";
"Core version: v%@" = "Core versie: v%@";
/* No comment provided by engineer. */
"Corner" = "Hoek";
@@ -2032,19 +2032,19 @@ chat item action */
"encryption agreed" = "versleuteling overeengekomen";
/* chat item text */
"encryption agreed for %@" = "versleuteling overeengekomen voor % @";
"encryption agreed for %@" = "versleuteling overeengekomen voor %@";
/* chat item text */
"encryption ok" = "versleuteling ok";
/* chat item text */
"encryption ok for %@" = "versleuteling ok voor % @";
"encryption ok for %@" = "versleuteling ok voor %@";
/* chat item text */
"encryption re-negotiation allowed" = "versleuteling heronderhandeling toegestaan";
/* chat item text */
"encryption re-negotiation allowed for %@" = "versleuteling heronderhandeling toegestaan voor % @";
"encryption re-negotiation allowed for %@" = "versleuteling heronderhandeling toegestaan voor %@";
/* message decrypt error item */
"Encryption re-negotiation error" = "Fout bij heronderhandeling van codering";
@@ -2056,7 +2056,7 @@ chat item action */
"encryption re-negotiation required" = "heronderhandeling van versleuteling vereist";
/* chat item text */
"encryption re-negotiation required for %@" = "heronderhandeling van versleuteling vereist voor % @";
"encryption re-negotiation required for %@" = "heronderhandeling van versleuteling vereist voor %@";
/* No comment provided by engineer. */
"Encryption renegotiation in progress." = "Er wordt opnieuw onderhandeld over de encryptie.";
@@ -2274,7 +2274,7 @@ chat item action */
/* No comment provided by engineer. */
"Error saving user password" = "Fout bij opslaan gebruikers wachtwoord";
/* No comment provided by engineer. */
/* alert message */
"Error scanning code: %@" = "Fout bij het scannen van code: %@";
/* No comment provided by engineer. */
@@ -2900,7 +2900,7 @@ servers warning */
/* alert title */
"Invalid name!" = "Ongeldige naam!";
/* No comment provided by engineer. */
/* alert title */
"Invalid QR code" = "Ongeldige QR-code";
/* No comment provided by engineer. */
@@ -3029,7 +3029,7 @@ servers warning */
/* No comment provided by engineer. */
"KeyChain error" = "Keychain fout";
/* No comment provided by engineer. */
/* file alert title */
"Large file!" = "Groot bestand!";
/* badge alert button */
@@ -4573,13 +4573,13 @@ chat item action */
"Sending receipts is disabled for %lld contacts" = "Het verzenden van ontvangst bevestiging is uitgeschakeld voor %lld-contactpersonen";
/* No comment provided by engineer. */
"Sending receipts is disabled for %lld groups" = "Het verzenden van bevestigingen is uitgeschakeld voor %LLD -groepen";
"Sending receipts is disabled for %lld groups" = "Het verzenden van bevestigingen is uitgeschakeld voor %lld groepen";
/* No comment provided by engineer. */
"Sending receipts is enabled for %lld contacts" = "Het verzenden van ontvangst bevestiging is ingeschakeld voor %lld-contactpersonen";
/* No comment provided by engineer. */
"Sending receipts is enabled for %lld groups" = "Het verzenden van bevestigingen is ingeschakeld voor %LLD -groepen";
"Sending receipts is enabled for %lld groups" = "Het verzenden van bevestigingen is ingeschakeld voor %lld groepen";
/* No comment provided by engineer. */
"Sending via" = "Verzenden via";
@@ -5943,9 +5943,6 @@ server test failure */
/* No comment provided by engineer. */
"Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "Tenzij uw contact de verbinding heeft verwijderd of deze link al is gebruikt, kan het een bug zijn. Meld het alstublieft.\nOm verbinding te maken, vraagt u uw contact om een andere verbinding link te maken en te controleren of u een stabiele netwerkverbinding heeft.";
/* No comment provided by engineer. */
"Your contact sent a file that is larger than currently supported maximum size (%@)." = "Uw contact heeft een bestand verzonden dat groter is dan de momenteel ondersteunde maximale grootte (%@).";
/* No comment provided by engineer. */
"Your contacts can allow full message deletion." = "Uw contacten kunnen volledige verwijdering van berichten toestaan.";
+3 -6
View File
@@ -2364,7 +2364,7 @@ chat item action */
/* No comment provided by engineer. */
"Error saving user password" = "Błąd zapisu hasła użytkownika";
/* No comment provided by engineer. */
/* alert message */
"Error scanning code: %@" = "Błąd skanowanie kodu: %@";
/* No comment provided by engineer. */
@@ -3027,7 +3027,7 @@ servers warning */
/* alert title */
"Invalid name!" = "Nieprawidłowa nazwa!";
/* No comment provided by engineer. */
/* alert title */
"Invalid QR code" = "Nieprawidłowy kod QR";
/* No comment provided by engineer. */
@@ -3162,7 +3162,7 @@ servers warning */
/* No comment provided by engineer. */
"KeyChain error" = "Błąd pęku kluczy";
/* No comment provided by engineer. */
/* file alert title */
"Large file!" = "Duży plik!";
/* badge alert button */
@@ -6311,9 +6311,6 @@ alert title */
/* No comment provided by engineer. */
"Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "O ile Twój kontakt nie usunął połączenia lub ten link był już użyty, może to być błąd - zgłoś go.\nAby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połączenia i sprawdź, czy masz stabilne połączenie z siecią.";
/* No comment provided by engineer. */
"Your contact sent a file that is larger than currently supported maximum size (%@)." = "Twój kontakt wysłał plik, który jest większy niż obecnie obsługiwany maksymalny rozmiar (%@).";
/* No comment provided by engineer. */
"Your contacts can allow full message deletion." = "Twoje kontakty mogą zezwolić na pełne usunięcie wiadomości.";
+3 -9
View File
@@ -2751,7 +2751,7 @@ chat item action */
/* No comment provided by engineer. */
"Error saving user password" = "Ошибка при сохранении пароля пользователя";
/* No comment provided by engineer. */
/* alert message */
"Error scanning code: %@" = "Ошибка сканирования кода: %@";
/* No comment provided by engineer. */
@@ -3456,7 +3456,7 @@ servers warning */
/* alert title */
"Invalid name!" = "Неверное имя!";
/* No comment provided by engineer. */
/* alert title */
"Invalid QR code" = "Ошибка QR-кода";
/* alert title */
@@ -3609,7 +3609,7 @@ servers warning */
/* No comment provided by engineer. */
"KeyChain error" = "Ошибка KeyChain";
/* No comment provided by engineer. */
/* file alert title */
"Large file!" = "Большой файл!";
/* badge alert button */
@@ -7127,9 +7127,6 @@ alert title */
/* No comment provided by engineer. */
"You can still view conversation with %@ in the list of chats." = "Вы по-прежнему можете просмотреть разговор с %@ в списке чатов.";
/* badge alert */
"You can support SimpleX starting from v7 of the app." = "Вы можете поддержать SimpleX начиная с версии приложения v7.";
/* No comment provided by engineer. */
"You can turn on SimpleX Lock via Settings." = "Вы можете включить Блокировку SimpleX через Настройки.";
@@ -7295,9 +7292,6 @@ alert title */
/* No comment provided by engineer. */
"Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "Возможно, Ваш контакт удалил ссылку, или она уже была использована. Если это не так, то это может быть ошибкой - пожалуйста, сообщите нам об этом.\nЧтобы установить соединение, попросите Ваш контакт создать ещё одну ссылку и проверьте Ваше соединение с сетью.";
/* No comment provided by engineer. */
"Your contact sent a file that is larger than currently supported maximum size (%@)." = "Ваш контакт отправил файл, размер которого превышает максимальный размер (%@).";
/* No comment provided by engineer. */
"Your contacts can allow full message deletion." = "Ваши контакты могут разрешить окончательное удаление сообщений.";
+5 -8
View File
@@ -104,7 +104,7 @@
"%lld %@" = "%lld %@";
/* No comment provided by engineer. */
"%lld contact(s) selected" = "% ผู้ติดต่อ LLD ที่เลือกไว้";
"%lld contact(s) selected" = "%lld ผู้ติดต่อที่เลือกไว้";
/* No comment provided by engineer. */
"%lld file(s) with total size of %@" = "%lld ไฟล์ที่มีขนาดรวม %@";
@@ -1105,7 +1105,7 @@ alert button */
"encryption agreed" = "ตกลง encryption";
/* chat item text */
"encryption agreed for %@" = "ตกลง encryption สําหรับ % @";
"encryption agreed for %@" = "ตกลง encryption สําหรับ %@";
/* chat item text */
"encryption ok" = "encryptionใช้ได้";
@@ -1288,7 +1288,7 @@ alert button */
conn error description
file error text
snd error text */
"Error: %@" = "ข้อผิดพลาด: % @";
"Error: %@" = "ข้อผิดพลาด: %@";
/* No comment provided by engineer. */
"Error: no database file" = "เกิดข้อผิดพลาด: ไม่มีแฟ้มฐานข้อมูล";
@@ -1333,7 +1333,7 @@ snd error text */
"File will be received when your contact is online, please wait or check later!" = "จะได้รับไฟล์เมื่อผู้ติดต่อของคุณออนไลน์ โปรดรอหรือตรวจสอบในภายหลัง!";
/* No comment provided by engineer. */
"File: %@" = "ไฟล์: % @";
"File: %@" = "ไฟล์: %@";
/* No comment provided by engineer. */
"Files & media" = "ไฟล์และสื่อ";
@@ -1699,7 +1699,7 @@ server test error */
/* No comment provided by engineer. */
"KeyChain error" = "ข้อผิดพลาดของ Keychain";
/* No comment provided by engineer. */
/* file alert title */
"Large file!" = "ไฟล์ขนาดใหญ่!";
/* badge alert button */
@@ -3192,9 +3192,6 @@ server test failure */
/* No comment provided by engineer. */
"Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "เว้นแต่ผู้ติดต่อของคุณลบการเชื่อมต่อหรือลิงก์นี้ถูกใช้ไปแล้ว อาจเป็นข้อผิดพลาด โปรดรายงาน\nในการเชื่อมต่อ โปรดขอให้ผู้ติดต่อของคุณสร้างลิงก์การเชื่อมต่ออื่น และตรวจสอบว่าคุณมีการเชื่อมต่อเครือข่ายที่เสถียร";
/* No comment provided by engineer. */
"Your contact sent a file that is larger than currently supported maximum size (%@)." = "ผู้ติดต่อของคุณส่งไฟล์ที่ใหญ่กว่าขนาดสูงสุดที่รองรับในปัจจุบัน (%@)";
/* No comment provided by engineer. */
"Your contacts can allow full message deletion." = "ผู้ติดต่อของคุณสามารถอนุญาตให้ลบข้อความทั้งหมดได้";
+5 -8
View File
@@ -101,7 +101,7 @@
"%@ and %@ connected" = "%@ ve %@ bağlandı";
/* copied message info, <sender> at <time> */
"%@ at %@:" = "1$@, %2$@'de:";
"%@ at %@:" = "%1$@, %2$@'de:";
/* No comment provided by engineer. */
"%@ connected" = "%@ bağlandı";
@@ -1053,7 +1053,7 @@ set passcode view */
"changed address for you" = "senin için adres değiştirildi";
/* rcv group event chat item */
"changed role of %@ to %@" = "1$@ rolünü %2$@ olarak değiştirdi";
"changed role of %@ to %@" = "%1$@ rolünü %2$@ olarak değiştirdi";
/* rcv group event chat item */
"changed your role to %@" = "rolünü %@ olarak değiştirdi";
@@ -2393,7 +2393,7 @@ chat item action */
/* No comment provided by engineer. */
"Error saving user password" = "Kullanıcı şifresi kaydedilirken hata oluştu";
/* No comment provided by engineer. */
/* alert message */
"Error scanning code: %@" = "Kod taranırken hata oluştu: %@";
/* No comment provided by engineer. */
@@ -3031,7 +3031,7 @@ servers warning */
/* alert title */
"Invalid name!" = "Geçersiz isim!";
/* No comment provided by engineer. */
/* alert title */
"Invalid QR code" = "Geçersiz QR kodu";
/* No comment provided by engineer. */
@@ -3163,7 +3163,7 @@ servers warning */
/* No comment provided by engineer. */
"KeyChain error" = "Anahtar Zinciri hatası";
/* No comment provided by engineer. */
/* file alert title */
"Large file!" = "Büyük dosya!";
/* badge alert button */
@@ -6255,9 +6255,6 @@ alert title */
/* No comment provided by engineer. */
"Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "Kişiniz bağlantıyı silmediyse veya bu bağlantı kullanılmadıysa, bu bir hata olabilir - lütfen bildirin.\nBağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını isteyin ve sabit bir ağ bağlantınız olduğunu kontrol edin.";
/* No comment provided by engineer. */
"Your contact sent a file that is larger than currently supported maximum size (%@)." = "Kişiniz şu anda desteklenen maksimum boyuttan (%@) daha büyük bir dosya gönderdi.";
/* No comment provided by engineer. */
"Your contacts can allow full message deletion." = "Kişileriniz tam mesaj silme işlemine izin verebilir.";
+7 -10
View File
@@ -164,13 +164,13 @@
"%d days" = "%d днів";
/* forward confirmation reason */
"%d file(s) are still being downloaded." = "%их файл(ів) ще досі завантажуються.";
"%d file(s) are still being downloaded." = "%d файл(ів) ще досі завантажуються.";
/* forward confirmation reason */
"%d file(s) failed to download." = "%их файлів не вийшло завантажити.";
"%d file(s) failed to download." = "%d файлів не вийшло завантажити.";
/* forward confirmation reason */
"%d file(s) were deleted." = "%их файл(ів) було видалено.";
"%d file(s) were deleted." = "%d файл(ів) було видалено.";
/* forward confirmation reason */
"%d file(s) were not downloaded." = "%d файл(и) не було завантажено.";
@@ -2447,7 +2447,7 @@ chat item action */
/* No comment provided by engineer. */
"Error saving user password" = "Помилка збереження пароля користувача";
/* No comment provided by engineer. */
/* alert message */
"Error scanning code: %@" = "Код помилки сканування: %@";
/* No comment provided by engineer. */
@@ -3079,7 +3079,7 @@ servers warning */
/* alert title */
"Invalid name!" = "Неправильне ім'я!";
/* No comment provided by engineer. */
/* alert title */
"Invalid QR code" = "Неправильний QR-код";
/* No comment provided by engineer. */
@@ -3211,7 +3211,7 @@ servers warning */
/* No comment provided by engineer. */
"KeyChain error" = "помилка KeyChain";
/* No comment provided by engineer. */
/* file alert title */
"Large file!" = "Великий файл!";
/* badge alert button */
@@ -6037,7 +6037,7 @@ alert title */
"You are already connecting to %@." = "Ви вже з'єднані з %@.";
/* new chat sheet message */
"You are already connecting via this one-time link!" = "Ви вже підключаєтеся до %@.Ви вже підключаєтеся за цим одноразовим посиланням!";
"You are already connecting via this one-time link!" = "Ви вже підключаєтеся за цим одноразовим посиланням!";
/* No comment provided by engineer. */
"You are already in group %@." = "Ви вже перебуваєте в групі %@.";
@@ -6270,9 +6270,6 @@ alert title */
/* No comment provided by engineer. */
"Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "Якщо ваш контакт не видалив з'єднання або якщо це посилання вже використовувалося, це може бути помилкою - будь ласка, повідомте про це.\nЩоб підключитися, попросіть вашого контакта створити інше посилання і перевірте, чи маєте ви стабільне з'єднання з мережею.";
/* No comment provided by engineer. */
"Your contact sent a file that is larger than currently supported maximum size (%@)." = "Ваш контакт надіслав файл, розмір якого перевищує підтримуваний на цей момент максимальний розмір (%@).";
/* No comment provided by engineer. */
"Your contacts can allow full message deletion." = "Ваші контакти можуть дозволити повне видалення повідомлень.";
+6 -12
View File
@@ -131,10 +131,10 @@
"%@ is verified" = "%@ 已认证";
/* No comment provided by engineer. */
"%@ server" = "服务器";
"%@ server" = "%@ 服务器";
/* No comment provided by engineer. */
"%@ servers" = "服务器";
"%@ servers" = "%@ 服务器";
/* badge alert */
"%@ supports SimpleX Chat." = "%@ 是 SimpleX Chat 支持者。";
@@ -2715,7 +2715,7 @@ chat item action */
/* No comment provided by engineer. */
"Error saving user password" = "保存用户密码时出错";
/* No comment provided by engineer. */
/* alert message */
"Error scanning code: %@" = "扫描代码时出错:%@";
/* No comment provided by engineer. */
@@ -2785,7 +2785,7 @@ chat item action */
conn error description
file error text
snd error text */
"Error: %@" = "错误: %@";
"Error: %@" = "错误:%@";
/* relay test error
server test error */
@@ -3405,7 +3405,7 @@ servers warning */
/* alert title */
"Invalid name!" = "无效名称!";
/* No comment provided by engineer. */
/* alert title */
"Invalid QR code" = "无效的二维码";
/* alert title */
@@ -3558,7 +3558,7 @@ servers warning */
/* No comment provided by engineer. */
"KeyChain error" = "钥匙串错误";
/* No comment provided by engineer. */
/* file alert title */
"Large file!" = "大文件!";
/* badge alert button */
@@ -6985,9 +6985,6 @@ alert title */
/* No comment provided by engineer. */
"You can still view conversation with %@ in the list of chats." = "您仍然可以在聊天列表中查看与 %@的对话。";
/* badge alert */
"You can support SimpleX starting from v7 of the app." = "从 v7 版本起您可以支持 SimpleX。";
/* No comment provided by engineer. */
"You can turn on SimpleX Lock via Settings." = "您可以通过设置开启 SimpleX 锁定。";
@@ -7153,9 +7150,6 @@ alert title */
/* No comment provided by engineer. */
"Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "除非您的联系人已删除此连接或此链接已被使用,否则它可能是一个错误——请报告。\n如果要连接,请让您的联系人创建另一个连接链接,并检查您的网络连接是否稳定。";
/* No comment provided by engineer. */
"Your contact sent a file that is larger than currently supported maximum size (%@)." = "您的联系人发送的文件大于当前支持的最大大小 (%@)。";
/* No comment provided by engineer. */
"Your contacts can allow full message deletion." = "您的联系人可以允许完全删除消息。";
@@ -414,6 +414,13 @@ object ChatModel {
}
}
suspend fun addSentChatItem(activeCtx: ChatsContext, rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) {
activeCtx.addChatItem(rhId, cInfo, cItem)
if (activeCtx.secondaryContextFilter != null && cInfo.inMainChatList) {
chatsContext.addChatItem(rhId, cInfo, cItem)
}
}
// Spec: spec/state.md#ChatsContext
class ChatsContext(val secondaryContextFilter: SecondaryContextFilter?) {
val chats = mutableStateOf(SnapshotStateList<Chat>())
@@ -584,9 +591,9 @@ object ChatModel {
val i = getChatIndex(rhId, cInfo.id)
val chat: Chat
if (i >= 0) {
chat = chatsContext.chats[i]
chat = chats[i]
// update preview (for chat from main scope to show new items for invitee in pending status)
if (cInfo.groupChatScope() == null || cInfo.groupInfo_?.membership?.memberPending == true) {
if (cInfo.inMainChatList) {
val newPreviewItem = when (cInfo) {
is ChatInfo.Group -> {
val currentPreviewItem = chat.chatItems.firstOrNull()
@@ -604,20 +611,20 @@ object ChatModel {
else -> cItem
}
val wasUnread = chat.unreadTag
chatsContext.chats[i] = chat.copy(
chats[i] = chat.copy(
chatItems = arrayListOf(newPreviewItem),
chatStats =
if (cItem.meta.itemStatus is CIStatus.RcvNew) {
if (cItem.meta.itemStatus is CIStatus.RcvNew && secondaryContextFilter == null) {
increaseUnreadCounter(rhId, currentUser.value!!)
chat.chatStats.copy(unreadCount = chat.chatStats.unreadCount + 1, unreadMentions = if (cItem.meta.userMention) chat.chatStats.unreadMentions + 1 else chat.chatStats.unreadMentions)
} else
chat.chatStats
)
updateChatTagReadInPrimaryContext(chatsContext.chats[i], wasUnread)
updateChatTagReadInPrimaryContext(chats[i], wasUnread)
}
// pop chat
if (appPlatform.isDesktop && cItem.chatDir.sent) {
reorderChat(chatsContext.chats[i], 0)
reorderChat(chats[i], 0)
} else {
popChatCollector.throttlePopChat(chat.remoteHostId, chat.id, currentPosition = i)
}
@@ -1872,6 +1879,9 @@ sealed class ChatInfo: SomeChat, NamedChat {
val isChannel: Boolean
get() = groupInfo_?.useRelays == true
val inMainChatList: Boolean
get() = groupChatScope() == null || groupInfo_?.membership?.memberPending == true
}
@Serializable
@@ -723,35 +723,39 @@ fun ChatView(
ciInfo
}
groupMembersJob.cancel()
groupMembersJob = scope.launch(Dispatchers.Default) {
var initialCiInfo = loadChatItemInfo() ?: return@launch
groupMembersJob = scope.launch {
var initialCiInfo = withContext(Dispatchers.Default) { loadChatItemInfo() } ?: return@launch
if (!ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
ModalManager.end.closeModals()
} else if (ModalManager.end.isTopModalOpen(ModalViewId.CHAT_ITEM_INFO)) {
ModalManager.end.closeModal()
}
ModalManager.end.showModalCloseable(endButtons = {
ModalManager.end.showModalCloseable(id = ModalViewId.CHAT_ITEM_INFO, endButtons = {
ShareButton {
clipboard.shareText(itemInfoShareText(chatModel, initialCiInfo.first, initialCiInfo.second, chatModel.controller.appPrefs.developerTools.get()))
}
}) { close ->
var ciInfo by remember(cItem.id) { mutableStateOf(initialCiInfo) }
val (item, info) = ciInfo
ChatItemInfoView(chatRh, item, info, devTools = chatModel.controller.appPrefs.developerTools.get(), chatInfo)
LaunchedEffect(cItem.id) {
withContext(Dispatchers.Default) {
for (msg in controller.messagesChannel) {
if (msg.rhId == chatRh &&
msg is API.Result &&
msg.res is CR.ChatItemsStatusesUpdated &&
msg.res.chatItems.any { it.chatItem.id == cItem.id }
) {
ciInfo = loadChatItemInfo() ?: return@withContext
initialCiInfo = ciInfo
key(this) {
var ciInfo by remember(cItem.id) { mutableStateOf(initialCiInfo) }
val (item, info) = ciInfo
ChatItemInfoView(chatRh, item, info, devTools = chatModel.controller.appPrefs.developerTools.get(), chatInfo)
LaunchedEffect(cItem.id) {
withContext(Dispatchers.Default) {
for (msg in controller.messagesChannel) {
if (msg.rhId == chatRh &&
msg is API.Result &&
msg.res is CR.ChatItemsStatusesUpdated &&
msg.res.chatItems.any { it.chatItem.id == cItem.id }
) {
ciInfo = loadChatItemInfo() ?: return@withContext
initialCiInfo = ciInfo
}
}
}
}
}
KeyChangeEffect(chatModel.chatId.value) {
close()
KeyChangeEffect(chatModel.chatId.value) {
close()
}
}
}
}
@@ -333,7 +333,7 @@ fun showLargeFileAlert(fileSize: Long, incognito: Boolean, senderProfile: LocalP
text = text,
confirmText = generalGetString(MR.strings.ok),
dismissText = generalGetString(MR.strings.badges_support_simplex_title),
onDismiss = ::openBadgesView
onDismiss = { if (!appPlatform.isDesktop || !ModalManager.end.hasModalOpen(ModalViewId.BADGES)) openBadgesView() }
)
} else {
AlertManager.shared.showAlertMsg(title, text)
@@ -599,7 +599,7 @@ fun ComposeView(
if (!chatItems.isNullOrEmpty()) {
chatItems.forEach { aChatItem ->
withContext(Dispatchers.Main) {
chatsCtx.addChatItem(chat.remoteHostId, aChatItem.chatInfo, aChatItem.chatItem)
chatModel.addSentChatItem(chatsCtx, chat.remoteHostId, aChatItem.chatInfo, aChatItem.chatItem)
}
}
return chatItems.first().chatItem
@@ -741,7 +741,7 @@ fun ComposeView(
withContext(Dispatchers.Main) {
chatItems?.forEach { chatItem ->
chatsCtx.addChatItem(rhId, chat.chatInfo, chatItem)
chatModel.addSentChatItem(chatsCtx, rhId, chat.chatInfo, chatItem)
}
}
@@ -494,7 +494,7 @@ fun sendCommandMsg(chatsCtx: ChatModel.ChatsContext, chat: Chat, msg: String) {
if (!chatItems.isNullOrEmpty()) {
chatItems.forEach { aChatItem ->
withContext(Dispatchers.Main) {
chatsCtx.addChatItem(chat.remoteHostId, aChatItem.chatInfo, aChatItem.chatItem)
chatModel.addSentChatItem(chatsCtx, chat.remoteHostId, aChatItem.chatInfo, aChatItem.chatItem)
}
}
}
@@ -96,11 +96,14 @@ fun showMarkdownFile(file: CIFile) {
formatted,
bidiControlChar.containsMatchIn(text)
)
ModalManager.center.showModal(endButtons = {
if (formatted != null) MarkdownViewSwitch()
MarkdownFileSaveButton(file, bytes)
}) {
MarkdownFileView(file.fileName, document)
withContext(Dispatchers.Main) {
if (appPlatform.isDesktop && ModalManager.center.isTopModalOpen(ModalViewId.MARKDOWN_FILE)) ModalManager.center.closeModal()
ModalManager.center.showModal(id = ModalViewId.MARKDOWN_FILE, endButtons = {
if (formatted != null) MarkdownViewSwitch()
MarkdownFileSaveButton(file, bytes)
}) {
key(this) { MarkdownFileView(file.fileName, document) }
}
}
}
}
@@ -65,21 +65,24 @@ private fun openMarkdownModal(modal: Format.Modal) {
}
private fun showFullProfileDescription(description: String) {
ModalManager.end.showModalCloseable { _ ->
ColumnWithScrollBar {
AppBarTitle(generalGetString(MR.strings.profile_description__field))
val parsed = parseToMarkdownText(description)
MarkdownText(
description,
parsed?.formattedText,
parsed?.markdownBlocks,
toggleSecrets = true,
style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground, lineHeight = 22.sp),
uriHandler = LocalUriHandler.current,
linkMode = chatModel.simplexLinkMode.value,
modifier = Modifier.padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING),
blockLayout = true,
)
if (ModalManager.end.isTopModalOpen(ModalViewId.PROFILE_DESCRIPTION)) ModalManager.end.closeModal()
ModalManager.end.showModalCloseable(id = ModalViewId.PROFILE_DESCRIPTION) { _ ->
key(this) {
ColumnWithScrollBar {
AppBarTitle(generalGetString(MR.strings.profile_description__field))
val parsed = parseToMarkdownText(description)
MarkdownText(
description,
parsed?.formattedText,
parsed?.markdownBlocks,
toggleSecrets = true,
style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground, lineHeight = 22.sp),
uriHandler = LocalUriHandler.current,
linkMode = chatModel.simplexLinkMode.value,
modifier = Modifier.padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING),
blockLayout = true,
)
}
}
}
}
@@ -874,10 +874,10 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState
} else {
val candidate = nameSearchCandidate(it.trim())
connectNameCandidate.value = candidate
// clear the previous match immediately so the list falls back to text search during the debounce,
// instead of showing a stale filtered chat while the new search runs
searchChatFilteredBySimplexLink.value = emptySet()
if (candidate != null) {
// clear the previous match immediately so the list falls back to text search during the debounce,
// instead of showing a stale filtered chat while the new search runs
searchChatFilteredBySimplexLink.value = emptySet()
// resolve the name locally on each keystroke, debounced; collectLatest cancels the in-flight
// search when the next keystroke arrives. A bare name can be a contact or a channel, so search
// both and filter every known chat found; drop the row only when both types are already known.
@@ -274,8 +274,12 @@ fun UserPicker(
painterResource(MR.images.ic_manage_accounts),
generalGetString(MR.strings.create_chat_profile),
{
if (ModalManager.center.isTopModalOpen(ModalViewId.CREATE_PROFILE)) {
userPickerState.value = AnimatedViewState.HIDING
return@UserPickerOptionRow
}
doWithAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) {
ModalManager.center.showModalCloseable { close ->
ModalManager.center.showModalCloseable(id = ModalViewId.CREATE_PROFILE) { close ->
LaunchedEffect(Unit) {
userPickerState.value = AnimatedViewState.HIDING
}
@@ -252,7 +252,7 @@ fun showBadgeInfoAlert(name: String, badge: LocalBadge, uriHandler: UriHandler)
text = supports,
confirmText = generalGetString(MR.strings.ok),
dismissText = generalGetString(MR.strings.badges_support_simplex_title),
onDismiss = ::openBadgesView
onDismiss = { if (!appPlatform.isDesktop || !ModalManager.end.hasModalOpen(ModalViewId.BADGES)) openBadgesView() }
)
} else {
AlertManager.shared.showAlertMsg(title = title, text = supports)
@@ -9,6 +9,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.*
import LocalCardScreen
@@ -93,7 +94,11 @@ enum class ModalViewId {
CONTEXT_USER_PICKER_INCOGNITO,
BADGE_LINK,
BADGE_REDEEM_CODE,
BADGES
BADGES,
CREATE_PROFILE,
PROFILE_DESCRIPTION,
MARKDOWN_FILE,
CHAT_ITEM_INFO
}
class ModalManager(private val placement: ModalPlacement? = null) {
@@ -118,6 +123,8 @@ class ModalManager(private val placement: ModalPlacement? = null) {
fun isLastModalOpen(id: ModalViewId): Boolean = modalViews.lastOrNull()?.id == id
fun isTopModalOpen(id: ModalViewId): Boolean = modalViews.getOrNull(modalViews.size - toRemove.size - 1)?.id == id
fun isLastModal(data: ModalData): Boolean = modalViews.getOrNull(modalViews.size - toRemove.size - 1)?.data === data
fun showModal(settings: Boolean = false, showClose: Boolean = true, id: ModalViewId? = null, forceAnimated: Boolean = false, cardScreen: Boolean = false, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) {
@@ -224,6 +231,10 @@ class ModalManager(private val placement: ModalPlacement? = null) {
if (toRemove.isNotEmpty() && it == modalCount.value && transition.currentState == EnterExitState.Visible && !transition.isRunning) {
runAtomically { toRemove.removeIf { elem -> modalViews.removeAt(elem); true } }
}
// Until the opening modal covers the page that opened it, or the view under the first modal, a second click there would open it again
if (transition.targetState == EnterExitState.PostExit && it < modalCount.value) {
Box(Modifier.fillMaxSize().pointerInput(Unit) {})
}
}
}
@@ -66,8 +66,9 @@ fun ContactConnectionInfoView(
deleteConnection = { deleteContactConnectionAlert(rhId, contactConnection, chatModel, close) },
onLocalAliasChanged = { setContactAlias(rhId, contactConnection, it, chatModel) },
learnMore = {
if (appPlatform.isDesktop) ModalManager.end.closeModals()
ModalManager.end.showModalCloseable { close ->
AddContactLearnMore(close)
key(this) { AddContactLearnMore(close) }
}
}
)
@@ -90,7 +91,10 @@ private fun ContactConnectionInfoLayout(
SettingsActionItemWithContent(
icon = painterResource(MR.images.ic_theater_comedy_filled),
text = null,
click = { ModalManager.end.showModal { IncognitoView() } },
click = {
if (appPlatform.isDesktop) ModalManager.end.closeModals()
ModalManager.end.showModal { key(this) { IncognitoView() } }
},
iconColor = Indigo,
extraPadding = false
) {
@@ -47,8 +47,9 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden:
showHiddenProfilesNotice = m.controller.appPrefs.showHiddenProfilesNotice,
visibleUsersCount = visibleUsersCount(m),
addUser = {
if (ModalManager.center.isTopModalOpen(ModalViewId.CREATE_PROFILE)) return@UserProfilesLayout
withAuth {
ModalManager.center.showModalCloseable { close ->
ModalManager.center.showModalCloseable(id = ModalViewId.CREATE_PROFILE) { close ->
CreateProfile(m, close)
}
}
@@ -1,3 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>
</resources>
@@ -43,4 +43,4 @@
<string name="accept_contact_incognito_button">গুপ্ত-পরিচয় গ্রহণ করুন</string>
<string name="search_verb">সার্চ</string>
<string name="la_mode_off">বন্ধ আছে</string>
</resources>
</resources>
+4 -4
View File
@@ -24,11 +24,11 @@ android.nonTransitiveRClass=true
kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.jvm.target=11
android.version_name=7.1-beta.6
android.version_code=386
android.version_name=7.1-beta.7
android.version_code=387
desktop.version_name=7.1-beta.6
desktop.version_code=167
desktop.version_name=7.1-beta.7
desktop.version_code=168
kotlin.version=2.1.20
gradle.plugin.version=8.10.1
+2 -4
View File
@@ -148,7 +148,5 @@ Versions unpublished after a failure: published by the next push to the branch.
## Pull requests
- `packages.yml`, for package changes:
- core unchanged: add-on built and packages tested on 5 platforms
- core changed: add-on built on 5 platforms when its sources changed
- `build.yml`, for core changes: package tests on Linux x86_64 with the library built from the pull request
- `build.yml`, for every pull request it runs for: add-on built and packages tested on 4 platforms (Linux aarch64 excluded); Linux x86_64 uses the library built from the pull request when the core changed, other platforms the pinned library
- `packages.yml`, for package changes in a pull request without `build.yml` paths: add-on built and packages tested on 5 platforms
+472
View File
@@ -0,0 +1,472 @@
Revision 1, 2026-10-19, draft for comments
Evgeny Poberezkin
# Agent protocols over SimpleX
## Table of contents
- [Abstract](#abstract)
- [Conventions](#conventions)
- [Overview](#overview)
- [Addresses in llms.txt](#addresses-in-llmstxt)
- [Profile declarations](#profile-declarations)
- [protocols](#protocols)
- [Public group audience and update frequency](#public-group-audience-and-update-frequency)
- [Contact address peers](#contact-address-peers)
- [Protocol names](#protocol-names)
- [Chat transport](#chat-transport)
- [Commands](#commands)
- [Replies](#replies)
- [Message size](#message-size)
- [Service RPC transport](#service-rpc-transport)
- [Exchange](#exchange)
- [Request](#request)
- [Response](#response)
- [Signed requests](#signed-requests)
- [MCP binding](#mcp-binding)
- [Messages](#messages)
- [Versions and capabilities](#versions-and-capabilities)
- [Examples](#examples)
- [Security considerations](#security-considerations)
- [Privacy considerations](#privacy-considerations)
- [Implementations](#implementations)
- [References](#references)
## Abstract
This document defines how software agents find SimpleX addresses and call the tools of other agents and bots over SimpleX:
- an `llms.txt` section with communication addresses, beside the link relation type and the well-known document of [Publishing SimpleX addresses on websites](./web-discovery.md);
- declarations: the protocols a profile serves and their transports, the peer types a channel or a contact address is made for, and the update frequency of a channel;
- the chat transport: the command `/_call` in a conversation;
- the Service RPC transport: one request and one response, without a conversation, in a generic envelope;
- the binding of the Model Context Protocol (MCP) [MCP] to both transports.
## Conventions
The key words "MUST", "MUST NOT", "SHOULD", "SHOULD NOT" and "MAY" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals.
JSON schemas use JSON Type Definition [RFC8927]. Whitespace in JSON examples is for readability.
**Bot**: a SimpleX profile operated by software: a bot of the bot library, or an AI agent through `@simplex-chat/mcp`.
## Overview
A website owner publishes channels for updates and contact addresses for requests [WEB]. An agent:
1. reads the addresses from the website: link relations, `/.well-known/simplex.json` or `llms.txt`;
2. retrieves the link data with the link, without connecting;
3. reads the declarations;
4. subscribes to a channel, connects to a contact address, or sends a Service RPC request to it.
```mermaid
sequenceDiagram
participant A as Agent
participant W as Website
participant R as SMP router
participant B as Bot
A->>W: GET /.well-known/simplex.json
W-->>A: channels and contacts
A->>R: retrieve link data
R-->>A: link data with declarations
alt chat transport
A->>B: connect, then /_call messages
B-->>A: replies
else Service RPC transport
A->>B: request in the connection request
B-->>A: response to the reply queue
end
```
Two transports carry protocol messages:
- **chat**: a conversation in a direct chat or a group. The caller connects once and sends commands as messages. The conversation persists between calls.
- **rpc**: Service RPC. The request is sent in the connection request to a contact address, and the response to the reply queue of that request. Both sides then delete the connection. A call needs one round trip, and each call uses a new connection.
## Addresses in llms.txt
An `llms.txt` file [LLMSTXT] lists communication addresses in a section with the heading `## Communications`. The section is a file list: each item is a Markdown link, then a colon and notes.
The notes begin with the network and the type of the address, then the audience and, for updates, the update frequency or, for contacts, the protocols. Free text follows:
```
<network> updates for <audience>[, <frequency>]. <info>
<network> group for <audience>. <info>
<network> [business ]contact for <audience>[, <protocol>]*. <info>
```
- `<network>`: the name of the network, `SimpleX` for SimpleX links.
- `updates`: an address that publishes updates. `group`: an address of a group, where members converse. `contact`: an address that accepts conversations and requests. `business contact`: a business address [BUSINESS].
- `<audience>`: `people`, `agents`, or `people and agents`.
- `<frequency>`: a `changefreq` value of the Sitemaps protocol [SITEMAPS].
- `<protocol>`: the name of a protocol the address serves ([protocol names](#protocol-names)), in any letter case.
- `<info>`: what the address is about.
The network is named in the notes because the host in a SimpleX link is the router that hosts the address, and that router can have any domain name.
For SimpleX entries, the type `updates` requires a channel link, the type `group` a group link, and the types `contact` and `business contact` a contact address link [WEB]. The link text is the title of the address.
```
## Communications
- [Acme news](https://smp12.simplex.im/c#…): SimpleX updates for people and agents, weekly. Product news and offers.
- [Acme customers](https://smp12.simplex.im/g#…): SimpleX group for people. Questions and tips from customers.
- [Ask Acme](https://smp12.simplex.im/a#…): SimpleX business contact for agents, MCP. Orders and availability.
- [Acme support](mailto:support@acme.example): Email contact for people.
```
## Profile declarations
A profile or a group declares the protocols it serves in its preferences. A channel or a public group declares its audience in its public group record, and a channel also its update frequency. A contact address declares the peers it serves in its link data.
### protocols
`protocols` is a key of the preferences: `preferences` of a user or contact profile, and `groupPreferences` of a group profile [CHAT]. Clients store the preferences of other profiles as received, so a client that does not know the key decodes it after an upgrade.
JSON Type Definition schema [RFC8927] of the `protocols` key:
```json
{
"values": {
"properties": {
"versions": {"elements": {"type": "string"}},
"transports": {
"optionalProperties": {
"chat": {"type": "boolean"},
"rpc": {"type": "boolean"}
},
"additionalProperties": true
}
},
"additionalProperties": true
}
}
```
- Keys: protocol names.
- `versions`: required. The protocol versions the bot serves, in the bot's order of preference.
- `transports`: required. The transports on which the bot serves the protocol.
- `transports.chat`: the bot answers `/_call` for the protocol in a conversation.
- `transports.rpc`: the bot answers Service RPC requests for the protocol on its contact address. It is valid in `preferences` only.
- An absent transport is false.
- Clients MUST ignore unknown keys.
A declaration in `preferences` applies to direct chats with the profile and to Service RPC requests to its contact address. A declaration in `groupPreferences` states that the group's owners serve the protocol in the group chat. Other members can serve a protocol in a group without a declaration, because the core removes preferences from member profiles in groups. In a business chat, `protocols` of the business profile MUST be copied into the chat's `groupPreferences`, as `commands` are.
`commands` remains the command menu for people. `/_call` is not added to it. The underscore marks a command for software, as in the API of the core, and leaves `/call` free for bots' commands for people.
Example, the `preferences` of a contact profile:
```json
{
"protocols": {
"mcp": {
"versions": ["2026-07-28"],
"transports": {"chat": true, "rpc": true}
}
}
}
```
### Public group audience and update frequency
Two fields of the public group record, `publicGroup` of the group profile of a channel or a public group [CHAT]. JSON Type Definition schema [RFC8927]:
```json
{
"optionalProperties": {
"audience": {"elements": {"type": "string"}},
"changeFrequency": {"enum": ["always", "hourly", "daily", "weekly", "monthly", "yearly", "never"]}
}
}
```
- `audience`: the peer types the channel or the group is made for. The values are the values of a profile's `peerType`: `human`, `bot`, `business`, `agent`. An absent `audience` is `["human"]`. Agents also read content made for people, so a channel or a group declares `agent` when it is made for agents, or has dedicated support for them.
- `changeFrequency`: in a channel only, how often the channel is updated. The values are the `changefreq` values of the Sitemaps protocol [SITEMAPS]: `always`, `hourly`, `daily`, `weekly`, `monthly`, `yearly`, `never`.
### Contact address peers
One field of the data published with a contact address link, beside `business`, which marks a business address [BUSINESS]. JSON Type Definition schema [RFC8927]:
```json
{
"optionalProperties": {
"serves": {"elements": {"type": "string"}}
}
}
```
- `serves`: the peer types the address is made for, with the values and the default of `audience`.
Clients MUST ignore unknown values of `audience` and `serves`. The values in the public group record and in the address link data take precedence over the values on a website [WEB].
### Protocol names
A protocol name consists of lowercase ASCII letters, digits and hyphens.
A binding defines the protocol's request and response messages, including how, and whether, a peer discovers the protocol's capabilities. Both transports carry these messages unchanged.
| Name | Binding |
|---|---|
| `mcp` | [MCP binding](#mcp-binding) |
## Chat transport
### Commands
```
/_call <protocol> <request>
/_call @<member> <protocol> <request>
```
- `<protocol>`: the name of a protocol the recipient serves.
- `<request>`: the protocol's request message, a JSON object.
- `@<member>`: optional, in a group: a mention of the called member [CHAT].
- In a group, a command with a mention is answered by the mentioned member. A command without a mention is a broadcast: each member that serves the protocol answers it.
- A command is the text of a text message. One space separates its parts.
- A bot that declares `transports.chat` for a protocol MUST answer each `/_call` for it that mentions the bot or mentions nobody.
### Replies
- A bot sends one reply to each command it answers. The reply quotes the command, with a truncated quote. Its text is the protocol's response message, a JSON object.
- A caller matches each reply to its command by the quoted message. Replies can arrive in any order.
- In a group, a broadcast can receive several replies, one from each member that serves the protocol. A caller collects them all, each with its sender.
- In a direct chat, a caller accepts replies only from the recipient. In a group, a caller of a command with a mention accepts only the mentioned member's reply.
- Commands and replies are ordinary messages: people see them in the conversation.
### Message size
A command and a reply are one message each. The core refuses a message whose encoded JSON exceeds 15,602 bytes [CHAT]. The command or the reply is a string inside that JSON, so its escape characters count towards the limit. A bot whose reply exceeds the limit replies with the binding's error instead.
## Service RPC transport
### Exchange
The requester's core sends the request with:
```
/_service_request <userId> <target> [timeout=<seconds>] [sign_key=<key>] <json>
```
- `<target>`: a contact address link, or the SimpleX name of a contact address [NAMES].
- `timeout`: how long the requester waits for the response. The default is 30 seconds.
- `sign_key`: the requester's Ed25519 private key, for a [signed request](#signed-requests).
- `<json>`: the [request](#request), a JSON object.
- The command returns `serviceResponse` with `responseData`, the [response](#response).
The responder's core reports the request as the event `serviceRequest`, with `requestId`, `signerKey` and `requestData`. The responder sends the response with:
```
/_service_response <userId> <requestId> <json>
```
Requirements:
- The responder's core MUST run with service requests enabled, the option `service_requests=on` of `/_start`. Otherwise the core drops each request, and the requester waits until its timeout.
- The responder's contact address MUST have ratchet keys. The core adds them to an address created or updated with the option `pq_ratchet=on` or `pq_ratchet=off` (`/_address`, `/_short_link_address`, `/_address_settings`). A request to an address without ratchet keys fails at once.
- The core accepts a response within 180 seconds of receiving the request. A requester that expects a slow response sets `timeout` accordingly.
- Before encryption, the agent pads a request or a response, its own framing included, to 11,106 bytes, and a longer one fails. A request and a response SHOULD NOT exceed 10,000 bytes of JSON.
### Request
JSON Type Definition schema [RFC8927] of the request:
```json
{
"properties": {
"protocol": {"type": "string"},
"request": {}
},
"optionalProperties": {
"version": {},
"requestKey": {"type": "string"}
},
"additionalProperties": true
}
```
- `protocol`: the protocol name.
- `version`: the protocol version, for a protocol whose messages do not include one. Its type is defined by the binding.
- `requestKey`: the requester's Ed25519 public key, in the encoding of the core's `signerKey`: base64url of the X.509 DER encoding of the key. A request with `requestKey` MUST be signed with that key.
- `request`: the protocol's request message, the same as after `/_call <protocol>` in a conversation. The fields of the envelope are never added to it.
### Response
JSON Type Definition schema [RFC8927] of the response:
```json
{
"optionalProperties": {
"response": {},
"error": {
"properties": {
"code": {"type": "string"}
},
"optionalProperties": {
"message": {"type": "string"},
"retryAfter": {"type": "uint32"}
},
"additionalProperties": true
}
},
"additionalProperties": true
}
```
- A response has exactly one of `response` and `error`.
- `response`: the protocol's response message, the same as the reply in a conversation.
- `error`: the request was not passed to the protocol.
- `message`: a description for the requester's logs.
- `retryAfter`: the number of seconds after which a repeated request can succeed.
Error codes:
- `unsupported_protocol`: the responder does not serve the protocol over Service RPC.
- `unsupported_version`: the responder does not serve the requested version.
- `bad_request`: the envelope is invalid, or `requestKey` differs from the verified signer key.
- `conversation_required`: the request needs a conversation. The requester connects to the address and uses the chat transport.
- `too_large`: the response exceeds the size limit.
- `rate_limited`: the requester repeats the request after `retryAfter`.
- `internal`: the responder failed.
A requester treats an unknown code as final, unless the error has `retryAfter`.
### Signed requests
- A signed request has the requester's Ed25519 signature over the request payload, bound to the request's ratchet session: the agent signs `SHA3-256("SimpleXService" ‖ AD) ‖ payload`, where AD is the ratchet's associated data. The responder's agent verifies the signature, and the core reports the key as `signerKey`. A signature copied into another session fails verification.
- A responder MUST answer `bad_request` to a request whose `requestKey` differs from `signerKey`, and to an unsigned request with `requestKey`.
- A requester signs when the responder needs a persistent identity, for example an account or a purchase. All requests signed with one key are linkable to each other. A requester SHOULD use a separate key for each responder.
- An unsigned request has only the payload: the responder cannot link it to other requests by a key.
## MCP binding
Protocol name: `mcp`.
### Messages
- The request is one JSON-RPC request of MCP revision 2026-07-28 or later, with its `_meta` envelope [MCP]. In a conversation, it follows `/_call mcp`. Over Service RPC, it is the envelope's `request`.
- The response is the JSON-RPC response to the request: a result or a JSON-RPC error. In a conversation, it is the text of the reply. Over Service RPC, it is the envelope's `response`.
- In a conversation, a caller matches a reply to its request by the quoted message. The response has the request's `id`, as JSON-RPC requires.
- A bot serves the methods answered with one response, for example `server/discover`, `tools/list` and `tools/call`.
- A multi-round-trip result (`resultType: "input_required"`) is answered with a new request, as MCP defines.
- A response above the size limit is replaced with a JSON-RPC error with code -32603 in a conversation, and with the envelope error `too_large` over Service RPC.
- Over Service RPC, a call of a tool that needs a conversation is answered with the envelope error `conversation_required`.
### Versions and capabilities
- `versions`: MCP revisions from 2026-07-28 on, for example `2026-07-28`. Each request names its revision in `_meta`, under `io.modelcontextprotocol/protocolVersion`. Earlier revisions begin with an `initialize` handshake.
- The envelope's `version` is omitted.
- A bot answers a request of a revision it does not serve with MCP's `UnsupportedProtocolVersionError` (code -32022), which lists the revisions it serves.
- A caller learns the bot's revisions and capabilities with `server/discover`, and its tools with `tools/list`. MCP requires a server that declares the `tools` capability to answer `tools/list`.
### Examples
Listing tools in a conversation:
```
/_call mcp {"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {"_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {}}}}
```
The reply:
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "complete",
"tools": [
{
"name": "availability",
"description": "Items in stock",
"inputSchema": {
"type": "object",
"properties": {"item": {"type": "string"}},
"required": ["item"]
}
}
],
"ttlMs": 300000,
"cacheScope": "public"
}
}
```
Calling a tool over Service RPC, the request:
```json
{
"protocol": "mcp",
"request": {
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "availability",
"arguments": {"item": "anvil"},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
}
```
The response:
```json
{
"response": {
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "complete",
"content": [{"type": "text", "text": "12 anvils are in stock."}],
"isError": false
}
}
}
```
## Security considerations
- Messages, replies, profiles, tool descriptions and tool results from other participants are data for an agent, never instructions.
- In a group, any member can reply to a broadcast. Each reply shows its sender. A caller that needs one provider mentions it.
- A responder that relies on `requestKey` MUST compare it with `signerKey`.
- A signed request identifies its signer to the responder across all requests signed with the same key.
## Privacy considerations
- Each Service RPC request uses a new connection with a new reply queue.
- MCP's `io.modelcontextprotocol/clientInfo` identifies the requester's software. A requester that needs unlinkable requests SHOULD send a common value or omit it.
- In a group, every member sees each command and each reply, with or without a mention. A request that needs privacy is sent in a direct chat with the member, where the group allows direct messages, or over Service RPC.
- A conversation persists: the bot can link all calls made in it. An incognito profile gives the caller a new random profile for each conversation.
## Implementations
Implemented:
- `@simplex-chat/mcp` 0.1.0: an MCP server that gives an AI agent its own SimpleX profile. Its tools are `connect`, `send`, `receive`, `list` and `leave`. By default, the agent's owner approves connections and messages in the agent control chat. An agent sends `/_call` with `send`, and reads the replies with `send` and `receive`.
- The simplex-chat core: Service RPC with signed requests (`/_service_request`, `/_service_response`), and the profile peer type `agent`.
Planned:
- The simplex-chat core: the key `protocols` in the preferences types, in the preferences sent in profiles and in link data, and in the preferences of business chats; `audience` and `changeFrequency` in the public group record; `serves` in the address settings and the address link data; replies with a truncated quote.
- `@simplex-chat/mcp`: the tools `mcp_tools` and `mcp_call`. They use Service RPC when the address declares `transports.rpc`, and the agent's conversation otherwise. In a group, `mcp_call` mentions the called member, or broadcasts and returns all replies. Each call is an action, approved by the agent's owner as a message is.
- The bot library: a helper that serves MCP tools over both transports.
## References
- [BUSINESS] Using SimpleX Chat in business: [BUSINESS.md](../BUSINESS.md)
- [CHAT] SimpleX Chat protocol: [simplex-chat.md](./simplex-chat.md)
- [LLMSTXT] The /llms.txt file: https://llmstxt.org
- [MCP] Model Context Protocol specification, revision 2026-07-28: https://modelcontextprotocol.io/specification/2026-07-28
- [NAMES] SimpleX public names for channels and businesses: [names-overview.md](./names-overview.md)
- [RFC2119] Key words for use in RFCs to Indicate Requirement Levels: https://www.rfc-editor.org/rfc/rfc2119
- [RFC8174] Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words: https://www.rfc-editor.org/rfc/rfc8174
- [RFC8927] JSON Type Definition: https://www.rfc-editor.org/rfc/rfc8927
- [SITEMAPS] Sitemaps XML format: https://www.sitemaps.org/protocol.html
- [WEB] Publishing SimpleX addresses on websites: [web-discovery.md](./web-discovery.md)
+251
View File
@@ -0,0 +1,251 @@
Revision 1, 2026-10-19, draft
Evgeny Poberezkin
# Publishing SimpleX addresses on websites
## Table of contents
- [Abstract](#abstract)
- [Conventions](#conventions)
- [Terms](#terms)
- [Link relation type](#link-relation-type)
- [simplex](#simplex)
- [Owner's and described addresses](#owners-and-described-addresses)
- [Target attributes](#target-attributes)
- [HTML](#html)
- [HTTP](#http)
- [Well-known URI](#well-known-uri)
- [Retrieval](#retrieval)
- [Format](#format)
- [Example](#example)
- [Processing](#processing)
- [Security considerations](#security-considerations)
- [Privacy considerations](#privacy-considerations)
- [IANA considerations](#iana-considerations)
- [Link relation type registration](#link-relation-type-registration)
- [Well-known URI registration](#well-known-uri-registration)
- [HTML link type extensions](#html-link-type-extensions)
- [References](#references)
## Abstract
This document defines how a website publishes SimpleX channels, groups and contact addresses:
- the link relation type `simplex`, for the HTML elements `link`, `a` and `area`, and for the HTTP `Link` header field. Its targets are the addresses of the website's owner, or the addresses a page describes, such as the entries of a directory;
- the well-known URI `simplex.json`: a JSON document with the owner's channels, groups and contact addresses, their audience, the update frequency of each channel, and the kind and the protocols of each contact address.
People and software agents subscribe to a channel to receive updates, join a group to take part in its conversations, and connect to a contact address to send requests. [Agent protocols over SimpleX](./agent-protocols.md) defines the requests an agent sends to a contact address, and an `llms.txt` section with the same addresses.
## Conventions
The key words "MUST", "MUST NOT", "SHOULD", "SHOULD NOT" and "MAY" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals.
JSON schemas use JSON Type Definition [RFC8927].
## Terms
- **SimpleX link**: a short connection link of the SimpleX Agent protocol, with the `https` or the `simplex` scheme [AGENT]. The type in the link is the kind of the address.
- **Channel link**: a SimpleX link of type `c`, for example `https://smp12.simplex.im/c#<key>`.
- **Group link**: a SimpleX link of type `g`, for example `https://smp12.simplex.im/g#<key>`.
- **Contact address link**: a SimpleX link of type `a`, for example `https://smp12.simplex.im/a#<key>`.
- **SimpleX name**: a name in the SimpleX name service [NAMES]. `#example.simplex` is the name of a channel, `@example.simplex` the name of a contact address.
- **Owner**: the person or organization that publishes the website.
## Link relation type
### simplex
The target of a `simplex` link is a SimpleX channel, group or contact address of the owner of the link context, or described by the link context. The target MUST be a channel link, a group link or a contact address link. The type in the link tells which.
### Owner's and described addresses
A page that describes the addresses of others, such as a directory or a catalogue, uses the relation type on its entries. The owner's own addresses are listed in the [well-known document](#well-known-uri).
### Target attributes
- `title`: the name of the channel, the group or the address, for display. In HTTP, a title with non-ASCII characters is sent as `title*` [RFC8187].
- `hreflang`: the language of the channel's posts, or of the conversations [BCP47].
A link context MAY have several `simplex` links of one kind. Their order is the owner's order of preference: document order in HTML, and the order of link values in HTTP.
### HTML
In the document head:
```html
<link rel="simplex" href="https://smp12.simplex.im/c#…" hreflang="en" title="Acme news">
<link rel="simplex" href="https://smp12.simplex.im/a#…" hreflang="en" title="Ask Acme">
```
In the document body, the relation type is used on visible links, for example on the entries of a directory:
```html
<a rel="simplex" href="https://smp12.simplex.im/a#…">Chat with us</a>
<a rel="simplex" href="https://smp12.simplex.im/g#…">Acme customers</a>
```
### HTTP
```
Link: <https://smp12.simplex.im/c#…>; rel="simplex"; hreflang="en"; title="Acme news"
Link: <https://smp12.simplex.im/a#…>; rel="simplex"; hreflang="en"; title="Ask Acme"
```
## Well-known URI
### Retrieval
The document is located at the path `/.well-known/simplex.json` of an origin [RFC8615]. A client retrieves it with an HTTPS `GET` request. A server responds with status 200, and SHOULD send the media type `application/json`. A client parses the response body as JSON, whatever media type the server sends.
The entries of the document are the channels, groups and contact addresses of the origin's owner. A site that controls only a path of its origin publishes link relations instead.
### Format
The document MUST conform to this JSON Type Definition schema [RFC8927]:
```json
{
"optionalProperties": {
"channels": {"elements": {"ref": "address"}},
"groups": {"elements": {"ref": "address"}},
"contacts": {"elements": {"ref": "address"}}
},
"additionalProperties": true,
"definitions": {
"address": {
"properties": {
"link": {"type": "string"}
},
"optionalProperties": {
"name": {"type": "string"},
"title": {"type": "string"},
"lang": {"type": "string"},
"audience": {"elements": {"type": "string"}},
"changeFrequency": {"enum": ["always", "hourly", "daily", "weekly", "monthly", "yearly", "never"]},
"business": {"type": "boolean"},
"protocols": {"elements": {"type": "string"}}
},
"additionalProperties": true
}
}
}
```
- `channels`: the owner's channels, in the owner's order of preference.
- `groups`: the owner's groups, in the owner's order of preference.
- `contacts`: the owner's contact addresses, in the owner's order of preference.
- `link`: required. A channel link in `channels`, a group link in `groups`, a contact address link in `contacts`.
- `name`: the SimpleX name of the channel, the group or the contact address of `link`.
- `title`: the name of the channel, the group or the address, for display.
- `lang`: the language of the channel's posts, or of the conversations [BCP47].
- `audience`: who the channel, the group or the address serves: `people`, `agents`, or both.
- `changeFrequency`: in `channels` only, how often the channel is updated. The values are the `changefreq` values of the Sitemaps protocol [SITEMAPS]: `always`, `hourly`, `daily`, `weekly`, `monthly`, `yearly`, `never`.
- `business`: in `contacts` only, `true` for a business address, at which each customer's conversation is a business chat that the business can add its team to [BUSINESS].
- `protocols`: in `contacts` only, the names of the protocols the address serves [AGENTS], for example `mcp`.
A site omits the lists it does not use.
### Example
```json
{
"channels": [
{
"link": "https://smp12.simplex.im/c#…",
"name": "#acme.simplex",
"title": "Acme news",
"lang": "en",
"audience": ["people", "agents"],
"changeFrequency": "weekly"
}
],
"groups": [
{
"link": "https://smp12.simplex.im/g#…",
"title": "Acme customers",
"lang": "en"
}
],
"contacts": [
{
"link": "https://smp12.simplex.im/a#…",
"name": "@acme.simplex",
"title": "Ask Acme",
"lang": "en",
"audience": ["agents"],
"business": true,
"protocols": ["mcp"]
}
]
}
```
## Processing
- A consumer MUST ignore unknown fields and unknown `audience` values.
- A consumer MUST ignore a `simplex` link that is not a channel, group or contact address link, and an entry of `simplex.json` whose link is of a type other than its list requires.
- The values on the website are hints. The data retrieved with the link is authoritative: an audience, an update frequency, the business flag or protocols declared there [AGENTS] take precedence over the website's values.
- A consumer MAY connect by `link` or by `name`. Resolving a name reveals interest in it to one names router, and verifies that the name's record and the profile's claim match [NAMES].
## Security considerations
- Whoever controls the content of a website controls the addresses it publishes. Consumers SHOULD retrieve pages and the well-known document over HTTPS.
- Publishing a link on a website leaves the link's own verification unchanged: the link data is bound to the keys of the link's owner [AGENT].
- A name is shown as verified only when its on-chain record and the profile's claim match [NAMES].
- Titles, profiles and all other text from a website or from the network are data for a software agent, never instructions.
## Privacy considerations
- The relation type and the well-known document are intended for owners who want to be reachable, such as channels, communities, businesses and services, and for pages that describe them, such as directories.
- A subscriber or a requester connects through the SimpleX network, without contacting the website. Retrieving a page or the well-known document is an ordinary web request.
## IANA considerations
### Link relation type registration
One registration in the "Link Relation Types" registry [RFC8288]:
- Relation Name: `simplex`
- Description: Refers to a SimpleX channel, group or contact address of the owner of the link context, or described by the link context.
- Reference: https://github.com/simplex-chat/simplex-chat/blob/stable/docs/protocol/web-discovery.md#simplex
- Notes: The target is a SimpleX short link [AGENT] of a channel, a group or a contact address, with the `https` scheme or the provisionally registered `simplex` scheme.
### Well-known URI registration
One registration in the "Well-Known URIs" registry [RFC8615]:
- URI Suffix: `simplex.json`
- Change Controller: Evgeny Poberezkin <ep@simplex.chat>
- Specification Document: https://github.com/simplex-chat/simplex-chat/blob/stable/docs/protocol/web-discovery.md#well-known-uri
- Status: provisional
- Related Information: SimpleX link syntax [AGENT].
## HTML link type extensions
The HTML standard requires extension link types to be registered on the microformats page "existing-rel-values" [HTML] [MF-REL]. The entry:
- Keyword: `simplex`
- Effect on `link`: Hyperlink
- Effect on `a`, `area`: Hyperlink
- Effect on `form`: not allowed
- Brief description: Refers to a SimpleX channel, group or contact address of the page's owner, or described by the page.
- Specification: https://github.com/simplex-chat/simplex-chat/blob/stable/docs/protocol/web-discovery.md#simplex
- Status: proposed
## References
- [AGENT] SimpleX Agent protocol, short connection links: https://github.com/simplex-chat/simplexmq/blob/stable/protocol/agent-protocol.md#short-connection-link-syntax
- [AGENTS] Agent protocols over SimpleX: [agent-protocols.md](./agent-protocols.md)
- [BCP47] Tags for Identifying Languages: https://www.rfc-editor.org/info/bcp47
- [BUSINESS] Using SimpleX Chat in business: [BUSINESS.md](../BUSINESS.md)
- [HTML] HTML Living Standard, other link types: https://html.spec.whatwg.org/multipage/links.html#other-link-types
- [MF-REL] Microformats, existing rel values: https://microformats.org/wiki/existing-rel-values
- [NAMES] SimpleX public names for channels and businesses: [names-overview.md](./names-overview.md)
- [RFC2119] Key words for use in RFCs to Indicate Requirement Levels: https://www.rfc-editor.org/rfc/rfc2119
- [RFC8174] Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words: https://www.rfc-editor.org/rfc/rfc8174
- [RFC8187] Indicating Character Encoding and Language for HTTP Header Field Parameters: https://www.rfc-editor.org/rfc/rfc8187
- [RFC8288] Web Linking: https://www.rfc-editor.org/rfc/rfc8288
- [RFC8615] Well-Known Uniform Resource Identifiers (URIs): https://www.rfc-editor.org/rfc/rfc8615
- [RFC8927] JSON Type Definition: https://www.rfc-editor.org/rfc/rfc8927
- [SITEMAPS] Sitemaps XML format: https://www.sitemaps.org/protocol.html
@@ -20,7 +20,7 @@
"repository": {
"type": "git",
"url": "git+https://github.com/simplex-chat/simplex-chat.git",
"directory": "/packages/simplex-chat-client/types/typescript"
"directory": "packages/simplex-chat-client/types/typescript"
},
"keywords": [
"simplex",
+3
View File
@@ -0,0 +1,3 @@
node_modules/
dist/
coverage/
+661
View File
@@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.
File diff suppressed because it is too large. Load diff
+74
View File
@@ -0,0 +1,74 @@
# SimpleX MCP server
Your Claude Code can now chat on SimpleX.
SimpleX Network is the only messaging network without any user identifiers - no phone numbers, usernames, account IDs or even random IDs. Your agent needs no account, no platform controls its identity, and nobody, not even the servers, can see who it talks to. The app was downloaded more than 3M times, and over 600,000 people use it every month.
With this package, Claude Code can join channels and groups, talk to bots, and even receive your prompts via the SimpleX app. It asks you on your phone before it connects or sends - one tap to approve.
<p>
<img src="https://raw.githubusercontent.com/simplex-chat/simplex-chat/master/packages/simplex-chat-mcp/images/approval.png" alt="The agent asks in its chat before it sends a message; one tap approves it" width="320">
<img src="https://raw.githubusercontent.com/simplex-chat/simplex-chat/master/packages/simplex-chat-mcp/images/prompts.png" alt="Prompting the agent from the SimpleX app" width="320">
</p>
## Try it
You need Node.js 22 or later, Claude Code and the [SimpleX Chat app](https://simplex.chat/downloads/).
1. Start your agent and keep it running:
```
npx -y @simplex-chat/mcp serve my-agent --display-name "My Agent"
```
Scan the QR code it prints with the SimpleX app, and tap Join. The chat "My Agent" appears: your agent asks there before it acts.
2. In another terminal, add it to Claude Code:
```
npx -y @simplex-chat/mcp register my-agent
```
3. Add `"mcp__simplex-my-agent"` to `permissions.allow` in `~/.claude/settings.json`. You approve in the app, so Claude Code doesn't need to ask too.
4. Start `claude` and ask:
```
Search the SimpleX Directory for security groups: https://smp4.simplex.im/a#lXUjJW5vHYQzoLYgmi8GbxkGP41_kjefFvBrdwg-0Ok
```
Approve its cards in "My Agent" with one tap.
## Prompt your agent from the app
Give Claude Code your SimpleX address:
```
Connect to me as yourself at <your SimpleX address>, with the message:
"I'm your agent, connected as you asked in Claude Code. I'll act on your prompts here, except security-sensitive ones."
Then take my prompts from that chat and answer there: wait for my messages with receive (wait 3600),
answer each one in that chat, and call receive again after every answer, until I say stop.
```
Approve the connection in "My Agent", and accept the request in the app if it asks. On the card of the agent's first answer, tap `all`: its later answers to you need no approval.
## Safety
- The agent asks in "My Agent" before it connects, sends or leaves; you approve or reject.
- `all` approves every message to one chat; `/revoke` ends it.
- Messages in your prompt chat can become prompts for your Claude Code session: use a chat where only you write.
- The agent runs on your computer and has no address in SimpleX Network, so nobody can contact it first.
## Only Claude Code, for now
The agent is tested with Claude Code. Tell us which other apps and models you want to use in [an issue](https://github.com/simplex-chat/simplex-chat/issues), or contribute.
## Reference
Approval modes, owners, several agents, other MCP hosts, recovery and all options: [REFERENCE.md](https://github.com/simplex-chat/simplex-chat/blob/master/packages/simplex-chat-mcp/REFERENCE.md).
## Own a part of SimpleX
Business is moving into messages, and AI agents follow: an agent can ask a company's agent in a chat instead of opening the company's website, and buy there too. These chats hold plans, purchases and accounts, so with AI everyone feels the need for private and secure communication.
SimpleX Chat, the company that builds SimpleX Network, is raising a community round on Wefunder: [wefunder.com/simplex.chat](https://wefunder.com/simplex.chat?utm_source=mcp)
+371
View File
@@ -0,0 +1,371 @@
# SimpleX MCP server: reference
[README](README.md) has the short version.
`@simplex-chat/mcp` is a local server that gives an AI agent its own presence on the SimpleX network. The agent connects to people, bots, groups, channels and directories, as itself or incognito, sends messages and receives all the responses. Bots tell it which commands they understand, so every bot on the network becomes a service the agent can use. You supervise the agent from its control chat in SimpleX: it asks there before it connects, sends or leaves, and you approve or reject from your phone with one tap, or approve all its messages to a chat you trust. Five tools cover it: `connect`, `send`, `receive`, `list`, `leave`. Setup is one command to start the agent, a scan to join its control chat, and one command to add it to Claude Code.
What it isn't:
- **Not a directory or a search service.** The agent reaches chats through links, SimpleX names and the directories it connects to.
- **Not a push channel into the agent.** An MCP server can't start a turn of the agent, so news reaches it only as results of calls it makes: waits inside `send` and `receive`, or polling.
- **Not an always-on service.** The agent acts while its session runs; services that must always be up should be bots.
- **Not hosted.** It runs on your machine, one process per agent.
- **Not in v0.1:**
- an address for the agent: nobody can contact it first;
- your approval of incoming messages;
- files;
- a Claude Code plugin or channel;
- registration in other hosts.
Status: v0.1. [`TESTING.md`](TESTING.md) describes a local build and test. The design, the tools and the scenarios are in [`PLAN.md`](PLAN.md).
- [Requirements](#requirements)
- [Quick start](#quick-start)
- [Owner approval](#owner-approval)
- [Add the agent to Claude Code](#add-the-agent-to-claude-code)
- [Approval modes](#approval-modes)
- [Expiry](#expiry)
- [What the owner sees](#what-the-owner-sees)
- [What the agent sees](#what-the-agent-sees)
- [Waiting for approval in Claude Code](#waiting-for-approval-in-claude-code)
- [Several agents](#several-agents)
- [Other MCP hosts](#other-mcp-hosts)
- [Recovery](#recovery)
- [Command reference](#command-reference)
- [Data folder](#data-folder)
- [Security](#security)
## Requirements
- Node.js 22.
- One of the platforms of the `simplex-chat` package: Linux x86-64 or arm64, macOS x86-64 or arm64, Windows x86-64.
- On Linux x86-64: glibc 2.34 or later, and the system libraries `libcrypto.so.3`, `libgmp.so.10` and `libz.so.1`.
- The SimpleX Chat app on a phone or computer, for the owner.
- Claude Code; other MCP hosts with Streamable HTTP and custom headers can be configured by hand.
At its first start the server downloads the SimpleX core library, 29 to 50 MB depending on the platform, into `~/.cache/simplex-chat` (macOS: `~/Library/Caches/simplex-chat`, Windows: `%LOCALAPPDATA%\simplex-chat`). Unpacked on Linux, it takes about 240 MB.
## Quick start
1. Start the agent's server in a terminal and keep it running. NAME, here `my-agent`, names the agent's data folder and its entry in Claude Code; `--display-name` is the name people see in SimpleX, NAME by default:
```
npx -y @simplex-chat/mcp serve my-agent --display-name "My Agent"
```
The first start prints, after two lines about downloading the SimpleX core library and the Node.js add-on:
```
SimpleX MCP server 0.1.0, agent "my-agent"
Data folder: /home/alice/.simplex-mcp/my-agent
Approval mode: requests
Agent control chat "My Agent" created.
Join the agent control chat in the SimpleX app. The link works once:
<QR code>
https://smp4.simplex.im/g#...
If the QR code does not scan, restart with --qr-code large, or paste the link into the app.
Whoever joins by this link becomes an owner of "My Agent" and approves its actions. To approve owners yourself, start with --owners.
Listening on http://127.0.0.1:5226/mcp
```
`-y` answers npm's prompt to install the package. The server asks no questions: every choice is an option. The QR code of such a link is 37 columns wide and 19 lines high; `--qr-code large` draws it twice as wide and twice as high.
2. In the SimpleX app, scan the QR code or open the link, and tap Join. The chat "My Agent" appears in the chat list, and the agent writes: `I'm your agent. I ask here before I connect or send.` The terminal prints:
```
Alice joined (member 3) and is now an owner. The link is deleted.
Add the agent to Claude Code:
npx -y @simplex-chat/mcp register my-agent
```
3. In another terminal, add the agent to Claude Code:
```
$ npx -y @simplex-chat/mcp register my-agent
Added to Claude Code: scope user, server simplex-my-agent.
```
4. Add `"mcp__simplex-my-agent"` to `permissions.allow` in `~/.claude/settings.json`, so Claude Code does not prompt for each call; approval happens in the SimpleX app.
5. Start Claude Code and ask it, for example: `Connect to the SimpleX Directory at https://smp4.simplex.im/a#lXUjJW5vHYQzoLYgmi8GbxkGP41_kjefFvBrdwg-0Ok and search it for trail running shoes.` In the chat "My Agent", the agent writes:
```
I want to connect to bot SimpleX Directory, as myself.
`Find groups, channels, businesses and bots.`
/approve 1 or /reject 1
```
Tap `/approve 1`. The agent then asks to send its search, and you approve that too, or tap the card's `all` command to approve all its messages to the directory. Once the name `@directory.simplex` is registered, it works as the target too, and the card shows `(@directory.simplex)`.
Without `--owners`:
- whoever joins by the link becomes an owner of the agent control chat;
- every owner approves actions, including owners added later in the app;
- the link is deleted when the first person joins.
## Owner approval
With `--owners`, only the owners listed in it approve actions. An owner added later in the app approves actions only after the server is restarted with that owner in the list.
1. Start the server with `--owners` and no list:
```
npx -y @simplex-chat/mcp serve my-agent --owners
```
The output is the same as in the quick start, with the line `The link adds an observer. Its ID and name are printed when it joins.` in place of `Whoever joins by this link ...`.
2. Join the agent control chat in the SimpleX app with the link. The link is deleted. The terminal prints, and the agent posts in the chat:
```
Alice joined (member 3). Restart the server with --owners 3:Alice
```
Until the restart, the agent's `connect`, `send` and `leave` fail.
3. Stop the server with Ctrl-C and start it with the list:
```
$ npx -y @simplex-chat/mcp serve my-agent --owners 3:Alice
Owners: 3:Alice.
Add the agent to Claude Code:
npx -y @simplex-chat/mcp register my-agent
```
4. To add an owner, add the person to the agent control chat in the app and make them an owner there. The terminal prints, and the agent posts in the chat:
```
Bob (member 7) is now an owner and can't approve actions yet. Restart the server with --owners 3:Alice,7:Bob
```
When an owner leaves or is removed, the agent lists the human owners who remain:
```
Alice (member 3) is no longer an owner. Restart the server with --owners 7:Bob
```
When none remains: `No owners remain: restart the server with --reset-owners --owners`.
In the chat, the `--owners ...` part of these lines is shown as code, so a member's name in it cannot form a tappable command.
At every start with a list, the server removes the connected members of the agent control chat other than the listed owners.
## Add the agent to Claude Code
```
npx -y @simplex-chat/mcp register my-agent # all Claude Code projects
npx -y @simplex-chat/mcp register my-agent --scope local # the current project only
```
`register NAME` reads the agent's port from its data folder, runs `claude mcp remove --scope SCOPE simplex-NAME` (SCOPE: `user`, or the `--scope` value), ignoring a missing entry, and then, for `register my-agent`:
```
claude mcp add-json --scope user simplex-my-agent '{"type":"http","url":"http://127.0.0.1:5226/mcp","headersHelper":"cat /home/alice/.simplex-mcp/my-agent/headers.json","timeout":169800000}'
```
- NAME: 1 to 32 lowercase letters, digits and single hyphens, starting with a letter or digit.
- The entry has no token. At every connection Claude Code runs the `headersHelper` command and sends the header it prints from `headers.json` in the data folder; on Windows the command is `type "<folder>\headers.json"`. With `--scope local`, Claude Code runs it only in a folder whose trust dialog was accepted.
- The agent's tools in Claude Code are `mcp__simplex-my-agent__connect` and the rest.
- `timeout` is the expiry plus 24 hours and 10 minutes, in milliseconds: the longest wait for a decision plus the longest wait for responses. After a change of `--expire` or `--port`, register the agent again; an agent on another port needs the same `--port` at every start.
- The server must be running when Claude Code starts a session. If a session started before the server, run `/mcp reconnect all` in it (Claude Code 2.1.284 or later; earlier, reconnect `simplex-my-agent` in `/mcp`).
Errors:
```
situation message
never started Agent "my-agent" was never started. Start it with: npx -y @simplex-chat/mcp serve my-agent
claude not on PATH Add this entry under "mcpServers" in ~/.claude.json: {"simplex-my-agent": {...}}
(--scope local: under "projects" > "<project folder>" > "mcpServers")
```
The token is created at the agent's first start and saved in its data folder; every start writes it to `headers.json`. `serve my-agent --new-token` replaces it, and Claude Code sends the new token from its next connection, without a new `register`.
## Approval modes
`--approve MODE` sets what the owner approves. It is set only by this option.
| Mode | The agent's actions: connect, send, leave | Messages to the agent |
|---|---|---|
| `requests` (default) | Performed after `/approve N`; a send to a chat approved for all messages at once, with a copy posted in the agent control chat. | Passed to the agent. |
| `host` | Performed at once; a copy is posted in the agent control chat. | Passed to the agent. |
Claude Code's own permission check applies to every tool call too. In mode `requests`, add the agent's server, for example `"mcp__simplex-my-agent"`, to `permissions.allow` in `~/.claude/settings.json`; otherwise Claude Code prompts for each call, or in auto mode its classifier decides it.
In `host` mode, approval is left to the MCP host. The server marks `connect`, `send` and `leave` with `_meta["anthropic/requiresUserInteraction"]: true`, so Claude Code shows its permission prompt on every call, in `acceptEdits`, `auto` and `bypassPermissions` modes as well, and allow rules do not skip it. In `dontAsk` mode and in `claude -p` runs Claude Code denies these calls.
## Expiry
A pending action expires after `--expire HOURS`, 23 by default and at most: SimpleX allows edits of a message for 24 hours, and the outcome of a pending action is an edit of its card. Cards don't show the time; `/pending` does. The last line of its card is edited to `Expired.`, and the agent's call returns `{"action": N, "status": "expired"}`. For the same reason, a `connect` waits for the other side at most until 23 hours 50 minutes after the action was created.
A result that no call has returned is dropped after the action's expiry time; a repeated call after that creates a new action. At most 20 actions are pending at once; a further `connect`, `send` or `leave` fails until the owner decides.
Actions last only while the server runs. When the server stops, the last line of each pending card is edited to `Cancelled: the server stopped.`; after a restart, `/approve` of an earlier number replies `Action N is not pending.`, and the agent's repeated call posts a new card. Action numbers continue across restarts.
## What the owner sees
All of it is in the agent control chat, posted by the agent's profile. The chat has the agent's display name; to see it under another name in your chat list, set an alias for the chat in the app. Names that are valid display names are shown plain. All other text written by the agent or by peers, other names included, is shown as code, line by line; names and errors take one line. Commands and SimpleX links inside it cannot be tapped: a valid display name cannot contain `/`, and the app parses each line of a message separately.
```
My Agent: I want to connect to bot SimpleX Directory (@directory.simplex), as myself.
`Find groups, channels, businesses and bots.`
/approve 1 or /reject 1
My Agent: I want to send a message to business Ridgeline, where I am incognito:
`Is the Talus Trail waterproof?`
/approve #9 4, /approve #9 all or /reject #9 4
My Agent: I want to leave business Ridgeline (#9).
/approve #9 12 or /reject #9 12
```
After the decision the card is edited, and its commands removed:
```
My Agent: I sent a message to business Ridgeline, where I am incognito:
`Is the Talus Trail waterproof?`
Approved by Alice.
```
Commands:
| Command | Effect |
|---|---|
| `/approve N`, `/approve #9 N` | Performs action N; the cards of sends and leaves name the chat. |
| `/reject N`, `/reject #9 N` | Rejects action N. |
| `/approve #9 all` | Approves all the agent's messages to chat #9: the pending ones and later ones. |
| `/revoke #9` | Ends that approval: each message to #9 needs approval again. |
| `/pending` | Lists pending actions. |
| `/list` | Lists the agent's 100 most recent chats, one line each, and which of them are approved for all messages; a last line counts the older chats. |
`/pending` and `/list` are in the chat's command menu; the cards carry the others. Each command includes its number, because the app sends a tapped command as a new message, without a quote. Every card form and reply is listed in [`PLAN.md`](PLAN.md), section "What the owner sees: the agent control chat".
### Approval of all messages to a chat
After `/approve #9 all`, the agent's messages to #9 are sent at once, and each one is posted in the agent control chat after it is sent:
```
My Agent: I sent a message to business Ridgeline, where I am incognito:
`Thanks, I'll order.`
Approved for this chat by Alice. /revoke #9
```
- It covers messages only: connecting and leaving are approved one by one.
- It lasts until `/revoke`, until the chat is deleted, or until the approving owner is no longer an approver. It survives restarts: it is kept in the agent's database, as custom data of that chat.
- With it, the agent can send anything it can read, without asking, to everyone in that chat.
- It applies in mode `requests` only: in mode `host`, `list` and `/list` show no approvals.
## What the agent sees
Five tools:
| Tool | What it does |
|---|---|
| `connect` | Joins a contact, bot, business, group or channel by a link or a SimpleX name, as the agent's profile or incognito. Returns the chat, its profile, welcome message, the commands a bot advertises, and the messages so far. |
| `send` | Sends a message and returns every message received in that chat after it, until none arrives for `idle` seconds (30 by default). A directory bot's search results arrive this way. |
| `receive` | Returns the chat's unread messages and those that arrive while it waits, waiting for one when asked. |
| `list` | Lists the agent's chats with unread counts and `approved: true` for chats approved for all messages, and its actions without a result. |
| `leave` | Deletes a contact, or leaves a group, business chat or channel, and deletes the chat; the contact or the members are notified. |
A `send` to a chat that `list` shows with `connected: false` is refused before any card. A `send` is refused with `text: too long for its approval card; shorten it.` when its card, in its longest outcome form, would exceed 15000 bytes in the SimpleX message, which holds at most 15602; a text of many lines or multi-byte characters reaches it below 12000 characters.
Incognito: the core creates a random profile for that one chat, so peers of different chats can't link them. The tools' inputs, results and errors are in [`PLAN.md`](PLAN.md), section "What the agent sees: tools", and the call sequences of the scenarios in section "Sequence diagrams".
## Waiting for approval in Claude Code
An MCP server cannot start a turn of the agent: what the agent reads from the server are the results of its own tool calls. A call that waits for the owner, or for responses, stays open, and Claude Code turns it into a background task:
- A tool call in the main conversation that runs past two minutes moves to a background task. Claude continues working, and the result arrives as a task notification. When it arrives while the session is idle, Claude responds to it without a prompt (checked with Claude Code 2.1.300).
- To move calls to the background sooner, set `CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS`, for example to `10000`, in the `env` block of `~/.claude/settings.json`. This applies to every MCP server.
- Calls from subagents, and calls in `claude -p` runs unless `CLAUDE_AUTO_BACKGROUND_TASKS=1`, stay in the foreground: the subagent or the run waits.
- A background task ends with the session. The action stays in the server while it runs; `list` shows it, and the same call again picks it up.
## Several agents
Each agent is its own process, with its own data folder, port, agent control chat and Claude Code server:
```
npx -y @simplex-chat/mcp serve my-agent-2 --port 5227
npx -y @simplex-chat/mcp register my-agent-2
```
## Other MCP hosts
Add an entry with the URL `http://127.0.0.1:5226/mcp` and the header `Authorization: Bearer <token>`, as in `headers.json` of the data folder; after `--new-token`, update the entry. A host that ends a tool call early returns a timeout to the agent; the agent repeats the call with the same arguments, which picks up the same action. Hosts with a fixed tool-call limit, between 60 and 300 seconds in several hosts, need `wait` below it.
## Recovery
| Situation | What to do |
|---|---|
| The server was not running when Claude Code started | Start the server, then run `/mcp reconnect all` in Claude Code (2.1.284 or later). |
| The server restarted while Claude Code was running | Claude Code reconnects up to five times within about 30 seconds. After that, in an interactive session, run `/mcp reconnect all`; `claude -p` runs and Agent SDK sessions keep retrying for about half an hour. Open calls fail, and pending actions are cancelled; the agent's repeated call creates a new request. |
| A second server was started on the same data folder | It exits with `Another server uses <folder> (PID N).` |
| Every owner left the agent control chat or was removed | `connect`, `send` and `leave` fail. Without `--owners`, the server logs `No owners in the agent control chat.` and prints a new link; join with it. With an `--owners` list, the agent posts and the server logs `No owners remain: restart the server with --reset-owners --owners`. |
| The agent was removed from the agent control chat, the chat was deleted, or the agent lost its owner role | `connect`, `send` and `leave` fail, and the server logs `The agent lost its control chat. Restart the server with --reset-owners.` |
| The QR code does not scan | Start with `--qr-code large`, or paste the link into the app. |
| The wrong person joined by the link | Start with `--reset-owners`. |
| The token leaked | Start with `--new-token`; Claude Code reads the new token at its next connection. |
`--reset-owners` deletes the agent control chat for all its members, creates a new one and prints a new link. Member IDs change in the new chat, so `--reset-owners` takes `--owners` without a list.
## Command reference
`npx -y @simplex-chat/mcp --help` prints:
```
Usage:
simplex-mcp serve NAME [options] run agent NAME
simplex-mcp register NAME [options] add agent NAME to Claude Code
serve options:
--data DIR data folder (default: ~/.simplex-mcp/NAME)
--display-name TEXT the agent's name in SimpleX (default at the first start: NAME)
--approve MODE what the owner approves in the agent control chat:
requests the agent's connections and messages (default)
host nothing; the MCP host asks
--owners [ID:NAME,...] only these owners approve; without a list, the link
adds an observer and its ID is printed at the join.
Without --owners, whoever joins becomes an owner and
every owner approves.
--expire HOURS pending actions expire after HOURS, 1 to 23 (default: 23)
--reset-owners replace the owners: a new agent control chat and link
--new-token replace the access token
--port N HTTP port on 127.0.0.1 (default: 5226)
--qr-code SIZE QR code of the link: small (default) or large
register options:
--scope SCOPE user (default) or local
--data DIR data folder (default: ~/.simplex-mcp/NAME)
--reset-owners takes --owners without a list.
```
## Data folder
```
~/.simplex-mcp/my-agent/
simplex_chat.db SimpleX chat database: the agent's profile and chats
simplex_agent.db SimpleX agent database: connections and keys
token access token, readable only by the user
headers.json the token as the Authorization header for Claude Code, readable only by the user
server.json name, port and expiry of the server, read by register
lock PID of the running server
```
The folder is readable only by the user: every start sets its mode to 0700, except on Windows.
The server keeps its action counter in the chat database, as custom data of the agent control chat, the approvals of all messages to a chat as custom data of that chat, and its actions in memory.
The server keeps no log file: it writes its log to the terminal where `serve` runs, events to stdout and errors to stderr.
One server runs per data folder: a second one exits, because two processes on one database break each other's connections (a SimpleX server delivers a queue's messages only to the last process that subscribed to it).
## Security
- The server listens only on 127.0.0.1. Every request needs the token. Requests from web pages on other hosts, and requests with another `Host`, are refused (the SDK's localhost Origin and Host checks).
- Any process with the token can act as the agent. The token is stored only in the data folder, in `token` and `headers.json`; Claude Code's config and the `claude mcp add-json` command line contain the command that prints `headers.json`, not the token.
- Names written by peers are folded to one line, with control and format characters replaced by spaces, in the terminal and in the agent control chat; there, a name that is not a valid display name is shown as code.
- Without `--owners`, whoever opens the link first becomes an owner. The link works once.
- With `--owners`, only the listed owners approve.
- After `/approve #N all`, the agent sends to that chat without asking; `/revoke #N` ends it.
- Peers of a chat connected as the agent see its profile; peers of an incognito chat see a random profile made for that chat only.
- The agent has no address, so nobody can contact it first.
- The agent uses private message routing: the SMP server that holds a contact's queue does not see the agent's IP address.
+92
View File
@@ -0,0 +1,92 @@
# Testing @simplex-chat/mcp v0.1
Node.js 22. Claude Code runs on the same computer as `serve`: the server listens on 127.0.0.1 only.
## Build
### Here
All commands run in `/code/simplex-chat-6/packages/simplex-chat-mcp`. The dependencies are installed by `npm install`; `simplex-chat` comes from npm and downloads libsimplex and its Node.js add-on to `~/.cache/simplex-chat` at the first start.
```
node node_modules/typescript/bin/tsc
node node_modules/jest/bin/jest.js test/unit # about 10 s
SMP_SERVER=/code/steward/node_modules/.cache/smp-server/smp-server-v7.1.0-beta.3 node node_modules/jest/bin/jest.js test/e2e # about 3 minutes; a local smp-server on a free port
```
`node_modules` is not writable here, so the end-to-end test takes the smp-server binary from `SMP_SERVER` instead of downloading it to `node_modules/.cache`.
### On your own machine
1. Check out simplex-chat master; the package is `packages/simplex-chat-mcp`.
2. In that folder: `npm install`, then `npx tsc`. `tsc` leaves the output of deleted sources in `dist`; `npm pack` and `npm publish` delete `dist` first and build it again (script `prepack`).
3. `node dist/cli.js serve my-agent --display-name "My Agent"`. At the first start, `simplex-chat` downloads libsimplex and its Node.js add-on, with a `Downloading ...` line each on stderr.
4. Tests: `npx jest test/unit`; `npx jest test/e2e` downloads smp-server v7.1.0-beta.3 on Linux, and elsewhere takes `SMP_SERVER=/path/to/smp-server`.
Until the package is published, the printed hint `npx -y @simplex-chat/mcp register my-agent` stands for `node dist/cli.js register my-agent`.
## Start, join, register
1. `node dist/cli.js serve my-agent --display-name "My Agent"`. NAME, here `my-agent`, names the data folder and the Claude Code entry; `--display-name` is the name people see in SimpleX. It prints the data folder (`~/.simplex-mcp/my-agent`), creates the agent control chat "My Agent", and prints a QR code and a link that works once. For a short link the QR code has 33 modules and takes 37 columns × 19 lines; `--qr-code large` prints it at 74 × 37. Keep it running; Ctrl-C stops it. `node dist/cli.js --help` lists the options.
2. In the SimpleX app, scan the QR code, or paste the link into "Connect via link". If the code does not scan, restart with `--qr-code large`. The chat "My Agent" appears with `I'm your agent. I ask here before I connect or send.` The terminal prints `Alice joined (member N) and is now an owner. The link is deleted.` and the register hint.
3. `node dist/cli.js register my-agent`. It prints `Added to Claude Code: scope user, server simplex-my-agent.` Run it in a terminal with the same `CLAUDE_CONFIG_DIR` as the one where `claude` runs: `claude mcp add-json` writes to the config of its own environment.
4. Add `"mcp__simplex-my-agent"` to `permissions.allow` in `~/.claude/settings.json`, so Claude Code does not prompt for each call; approval happens in the SimpleX app.
5. Start `claude`. `/mcp` shows `simplex-my-agent` connected with 5 tools. A session started before `register` does not load the entry; start a new one.
## Try the tools
Prompts for Claude Code, one at a time. A call that waits more than 2 minutes, for example for an approval, moves to a background task, and its result arrives as a notification.
By name, first:
- `Connect to @directory.simplex as yourself.` While the name is not registered: the tool error `Name not found: This SimpleX name is not registered. Please check the name.`, and no card. Once it is registered: the card `I want to connect to bot SimpleX Directory (@directory.simplex), as myself.`; after the approval, step 1 below returns `status: known`, with no card.
The directory by its address (`docs/DIRECTORY.md`):
1. `Connect to the SimpleX Directory as yourself at https://smp4.simplex.im/a#lXUjJW5vHYQzoLYgmi8GbxkGP41_kjefFvBrdwg-0Ok and tell me which commands it advertises.`
Card: `I want to connect to bot SimpleX Directory, as myself.` Tap `/approve 1`. The card becomes `I connected to ...` with `Approved by Alice.`; Claude gets `status: done`, the chat `@N`, `commands` and the welcome message.
2. `Search the directory for hiking groups.` Card: `I want to send a message to bot SimpleX Directory:` with the text. Approve it; Claude gets the bot's answers in `responses`.
3. `Send /help to the directory.` A bot command is sent as text.
4. `Check for new messages from the directory, waiting up to 60 seconds.` `receive` with `wait`.
5. `List your chats and pending actions.` `list`.
6. Reject one card with its `/reject @N A`: the call returns `status: rejected`; the card ends with `Rejected by Alice.` `/pending` lists the waiting cards.
Approval of all messages to a chat, and leaving:
1. `Search the directory for Monero groups.` On the card, tap `/approve @N all`. The reply is `Approved for bot SimpleX Directory (@N) by Alice. /revoke @N`, and the search is sent.
2. `Send /help to the directory.` It is sent without a card; the agent control chat then gets `I sent a message to bot SimpleX Directory:`, `` `/help` `` and `Approved for this chat by Alice. /revoke @N`. `list` shows `approved: true` for the directory.
3. Tap `/list` in the command menu: one line per chat; the directory's line ends with `, approved for this chat by Alice`.
4. Tap `/revoke @N`: `Messages to bot SimpleX Directory (@N) need approval again.` The next send gets a card.
5. `Leave the directory.` Card: `I want to delete bot SimpleX Directory (@N).` Approve it: the call returns `status: done`, and `list` no longer shows the directory.
6. Join a group from the search results, then `Leave that group.` Card: `I want to leave group <name> (#N).` Approve it: the members see that the agent left.
A channel:
1. `Search the directory for channels about trail running.` Take a channel link from the answers.
2. `Connect to <the channel link> as yourself.` Card: `I want to connect to channel <name>, as myself.` Approve it; Claude gets `type: channel`, `role: observer` and the recent posts in `messages`.
3. `Wait up to 10 minutes for a new post in that channel.` `receive` with `wait` returns the next post.
Incognito, as a second agent, because the first one already knows the directory:
1. `node dist/cli.js serve scout --display-name "Scout" --port 5227`; join its control chat "Scout" with the printed link; `node dist/cli.js register scout`; add `"mcp__simplex-scout"` to `permissions.allow`; `/mcp reconnect all`.
2. `Using simplex-scout, connect to the directory incognito: https://smp4.simplex.im/a#lXUjJW5vHYQzoLYgmi8GbxkGP41_kjefFvBrdwg-0Ok` Card: `I want to connect to bot SimpleX Directory, incognito.` Approve it; Claude gets `known_as`, a random name.
3. `Using simplex-scout, list your chats.` The directory has `incognito: true` and the same `known_as`. A send card for it says `where I am incognito`.
Stop and restart:
- Stop the server while a card waits: the card ends with `Cancelled: the server stopped.` After a restart, `/approve` of that number replies `Action N is not pending.`, and the next card has the next number.
- A second `serve my-agent` on the same data folder exits with `Another server uses ... (PID N).` A port in use exits with `Port 5226 is in use. Start with --port N.`, before any link is printed.
- Restart with `--new-token`, without `register`: after the reconnection, `list` returns the chats. Checked with `claude -p` and an `--mcp-config` entry with `headersHelper`, Claude Code 2.1.302, 2026-10-18.
## Claude Code checks
1. C2, protocol: passed with Claude Code 2.1.302 in the default runtime, with `MCP_PROTOCOL_NEGOTIATION=legacy` and with `MCP_SDK_GENERATION=v1`. To repeat it, write `c2.json` with `{"mcpServers": {"simplex-my-agent": {"type": "http", "url": "http://127.0.0.1:5226/mcp", "headersHelper": "cat <your home folder>/.simplex-mcp/my-agent/headers.json"}}}`, and run it once as is and once with each variable before it:
```
claude -p "Call the list tool and show its result." --strict-mcp-config --mcp-config c2.json --allowedTools mcp__simplex-my-agent
```
Each run shows the chats, with the preset contact "Ask SimpleX Team".
2. C3, subagent: `Use a subagent to send "trail shoes" to the directory.` While the card waits, the subagent's call stays in the foreground, with no background task; it returns after `/approve`.
3. C4, reconnect: restart `serve my-agent`; in the running session, `/mcp reconnect all`; `list` returns the chats.
4. C5, register again: `node dist/cli.js register my-agent` prints `Added to Claude Code: scope user, server simplex-my-agent.`; `claude mcp list` shows one `simplex-my-agent`.
5. C6, Esc: ask for a send, and press Esc while its card waits; `List your pending actions.` shows the action with `open: false`; the same request again attaches to it, and `/approve` returns the result to that call.
6. Host mode: restart with `node dist/cli.js serve my-agent --approve host`, then `/mcp reconnect all`. `Send /help to the directory.` makes Claude Code prompt, despite the allow rule; so does `leave`. After you allow it, the agent control chat gets `I sent a message to bot SimpleX Directory:`, `` `/help` `` and `Performed without approval (mode host).` A `connect` to a link not yet joined gets the same prompt and an outcome card; a target already joined returns `status: known`, with no card.
Binary file not shown.

After

Width:  |  Height:  |  Size: 727 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 KiB

+9
View File
@@ -0,0 +1,9 @@
module.exports = {
preset: "ts-jest",
maxWorkers: 1,
testEnvironment: "node",
testTimeout: 30000,
transform: {
"^.+\\.ts$": ["ts-jest", {tsconfig: "test/tsconfig.json", diagnostics: {ignoreCodes: [151002]}}]
}
}
File diff suppressed because it is too large. Load diff
+55
View File
@@ -0,0 +1,55 @@
{
"name": "@simplex-chat/mcp",
"version": "0.1.0",
"description": "MCP server that gives an AI agent its own SimpleX profile",
"bin": {
"simplex-mcp": "dist/cli.js"
},
"files": [
"dist",
"README.md"
],
"engines": {
"node": ">=22"
},
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "tsc",
"prepack": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
"test": "jest test/unit",
"test:e2e": "jest test/e2e"
},
"dependencies": {
"@modelcontextprotocol/node": "^2.1.1",
"@modelcontextprotocol/server": "^2.3.1",
"@simplex-chat/types": "^7.1.0-beta.6.2",
"qrcode": "^1.5",
"simplex-chat": "^7.1.0-beta.6.6"
},
"devDependencies": {
"@types/jest": "^30.0.0",
"@types/node": "^22.0.0",
"jest": "^30.2.0",
"ts-jest": "^29.4.6",
"typescript": "^5.9.3"
},
"repository": {
"type": "git",
"url": "git+https://github.com/simplex-chat/simplex-chat.git",
"directory": "packages/simplex-chat-mcp"
},
"keywords": [
"simplex",
"mcp",
"agent",
"privacy"
],
"author": "SimpleX Chat",
"license": "AGPL-3.0",
"bugs": {
"url": "https://github.com/simplex-chat/simplex-chat/issues"
},
"homepage": "https://github.com/simplex-chat/simplex-chat/tree/master/packages/simplex-chat-mcp#readme"
}
+122
View File
@@ -0,0 +1,122 @@
# SimpleX MCP server: specification
> Technical specification of `@simplex-chat/mcp` 0.1.0 in `packages/simplex-chat-mcp`. Statements are checked against the source at commit `a83005bc4` with the uncommitted source changes of 2026-10-18 20:05 UTC (app error texts in `log.ts`, `fullDomainName` in `messages.ts`), and each section links to the code.
>
> Related: [README](../README.md) (user manual) | [PLAN](../PLAN.md) (design record) | [TESTING](../TESTING.md) (local build and test)
## Table of contents
1. [Executive summary](#executive-summary)
2. [Dependency graph](#dependency-graph)
3. [Specification documents](#specification-documents)
4. [Source entry points](#source-entry-points)
---
## Executive summary
`@simplex-chat/mcp` is a local server that gives an MCP host one SimpleX profile per process. The host reaches it with Streamable HTTP on `127.0.0.1` and a bearer token. The server exposes five tools: `connect`, `send`, `receive`, `list` and `leave`.
The SimpleX core (libsimplex, release `core-7.1.0.12`) runs inside the same Node.js process, loaded by the `simplex-chat` library. The core keeps the profile, chats and connections in two SQLite databases of the data folder. The MCP protocol is served by the MCP TypeScript SDK (`@modelcontextprotocol/server` 2.3.1 and `@modelcontextprotocol/node` 2.1.1).
`connect`, `send` and `leave` are actions. In mode `requests` the server posts each action as a card in the agent control chat, a SimpleX group created and owned by the agent's profile. An owner approves or rejects the card with a command in that chat, and the server performs the action after the approval. An owner can also approve all messages to one chat in advance. In mode `host` the server performs actions at once and marks the three tools for the host's own confirmation.
A tool call is held open by the server until the owner decides and, for `connect` and `send`, until the replies arrive. A repeated call with the same arguments is attached to the same action, so a call that the host ends early is resumed.
The server keeps actions in memory, and they are lost when the process exits. The action counter and the standing approvals are stored as custom data in the chat database. The token, the headers file for the host, the server record and the lock are files in the data folder.
---
## Dependency graph
Solid edges are runtime imports. Dotted edges are type-only imports, removed by the compiler.
```mermaid
graph TD
cli["cli.ts"] --> serve["serve.ts"]
cli --> register["register.ts"]
cli --> messages["messages.ts"]
cli --> log["log.ts"]
cli -.-> actions["actions.ts"]
serve --> control["control.ts"]
serve --> tools["tools.ts"]
serve --> actions
serve --> http["http.ts"]
serve --> files["files.ts"]
serve --> messages
serve --> log
control --> tools
control --> actions
control --> messages
control --> log
control -.-> serve
tools --> actions
tools --> messages
tools --> log
actions --> messages
actions --> log
actions -.-> tools
register --> files
register --> log
files --> log
http --> log
log --> messages
serve --> sc["simplex-chat"]
tools --> sc
control --> sc
messages --> sc
log --> sc
serve --> types["@simplex-chat/types"]
tools --> types
control --> types
messages --> types
log --> types
serve --> mcps["@modelcontextprotocol/server"]
tools --> mcps
http --> mcps
http --> mcpn["@modelcontextprotocol/node"]
control --> qr["qrcode"]
```
| Module | Role |
|---|---|
| [`cli.ts`](../src/cli.ts) | `simplex-mcp` entry point: commands, options, exit codes |
| [`serve.ts`](../src/serve.ts) | start sequence, event dispatch, stop sequence |
| [`http.ts`](../src/http.ts) | HTTP listener, token check, `/mcp` route, SDK handler |
| [`tools.ts`](../src/tools.ts) | tool schemas, descriptions, handlers, connect plans, perform steps |
| [`actions.ts`](../src/actions.ts) | action records, state machine, expiry, stop |
| [`control.ts`](../src/control.ts) | agent control chat: link, owners, owner commands, cards, counter |
| [`messages.ts`](../src/messages.ts) | message items, item watch, waits, unread paging, display names |
| [`files.ts`](../src/files.ts) | data folder, lock, token, `headers.json`, `server.json` |
| [`register.ts`](../src/register.ts) | `register` command: Claude Code entry |
| [`log.ts`](../src/log.ts) | terminal lines, error texts, shell quoting |
---
## Specification documents
| Document | Contents |
|---|---|
| [Architecture](architecture.md) | system diagram, modules and types, state, process lifecycle, approval model, security boundaries, limits |
| [Flows](flows.md) | sequence diagrams of the main flows with source locations |
| [API](api.md) | the five tools, the control chat commands, replies and cards, the CLI |
---
## Source entry points
| Component | File | Key symbol | Line |
|---|---|---|---|
| Binary `simplex-mcp` | [`src/cli.ts`](../src/cli.ts#L191) | `main` | [191](../src/cli.ts#L191) |
| Argument parser | [`src/cli.ts`](../src/cli.ts#L42) | `parseCommand` | [42](../src/cli.ts#L42) |
| Server start | [`src/serve.ts`](../src/serve.ts#L64) | `startServer` | [64](../src/serve.ts#L64) |
| Agent wiring and event dispatch | [`src/serve.ts`](../src/serve.ts#L87) | `startAgent` | [87](../src/serve.ts#L87) |
| HTTP listener | [`src/http.ts`](../src/http.ts#L13) | `startHttp` | [13](../src/http.ts#L13) |
| Tool registration | [`src/tools.ts`](../src/tools.ts#L355) | `AgentTools.register` | [355](../src/tools.ts#L355) |
| Action store | [`src/actions.ts`](../src/actions.ts#L93) | `Actions` | [93](../src/actions.ts#L93) |
| Agent control chat | [`src/control.ts`](../src/control.ts#L67) | `Control` | [67](../src/control.ts#L67) |
| Card text | [`src/control.ts`](../src/control.ts#L417) | `cardText` | [417](../src/control.ts#L417) |
| Item watch | [`src/messages.ts`](../src/messages.ts#L58) | `ChatItemsWatch` | [58](../src/messages.ts#L58) |
| Claude Code entry | [`src/register.ts`](../src/register.ts#L30) | `register` | [30](../src/register.ts#L30) |
| Data folder files | [`src/files.ts`](../src/files.ts#L12) | `createDataFolder`, `acquireLock` | [12](../src/files.ts#L12) |
| Package manifest | [`package.json`](../package.json) | `bin.simplex-mcp` | [6](../package.json#L6) |
+472
View File
@@ -0,0 +1,472 @@
# SimpleX MCP server: API
> The five MCP tools, the agent control chat commands, replies and cards, and the CLI of `@simplex-chat/mcp` 0.1.0.
>
> Related specs: [README](README.md) | [Architecture](architecture.md) | [Flows](flows.md)
**Source:** [`tools.ts`](../src/tools.ts) | [`control.ts`](../src/control.ts) | [`cli.ts`](../src/cli.ts) | [`messages.ts`](../src/messages.ts) | [`actions.ts`](../src/actions.ts)
---
## Table of contents
1. [Tools](#1-tools)
- [1.1 Common rules](#11-common-rules)
- [1.2 `connect`](#12-connect)
- [1.3 `send`](#13-send)
- [1.4 `receive`](#14-receive)
- [1.5 `list`](#15-list)
- [1.6 `leave`](#16-leave)
- [1.7 Message and command objects](#17-message-and-command-objects)
- [1.8 Action refusals](#18-action-refusals)
2. [Agent control chat](#2-agent-control-chat)
- [2.1 Commands and replies](#21-commands-and-replies)
- [2.2 Cards](#22-cards)
- [2.3 Membership messages](#23-membership-messages)
- [2.4 Group profile](#24-group-profile)
3. [CLI](#3-cli)
- [3.1 `serve`](#31-serve)
- [3.2 `register`](#32-register)
- [3.3 Exit codes and messages](#33-exit-codes-and-messages)
- [3.4 Terminal lines](#34-terminal-lines)
---
## 1. Tools
### 1.1 Common rules
| Rule | Code |
|---|---|
| Tools are registered on each per-request `McpServer` with description, input schema, output schema and, in mode `host`, `_meta: {"anthropic/requiresUserInteraction": true}` for `connect`, `send` and `leave` | [tools.ts:355-365](../src/tools.ts#L355-L365) |
| Tool descriptions and server instructions; the instructions name the agent's display name | [tools.ts:301-332](../src/tools.ts#L301-L332), [serve.ts:109](../src/serve.ts#L109) |
| Arguments are validated by the SDK with Ajv; a failure is a result with `isError: true` and the text `Input validation error: Invalid arguments for tool NAME: ERRORS` | [mcp-BumlNux0.cjs:1812-1817](../node_modules/@modelcontextprotocol/server/dist/mcp-BumlNux0.cjs#L1812-L1817) |
| `additionalProperties: false` is set in every input schema | [tools.ts:177](../src/tools.ts#L177), [:192](../src/tools.ts#L192), [:203](../src/tools.ts#L203), [:206](../src/tools.ts#L206), [:214](../src/tools.ts#L214) |
| A result is sent as `structuredContent` and as its JSON in one text item | [tools.ts:370](../src/tools.ts#L370) |
| A tool error is sent as `isError: true` with one text item: the `ToolError` text, or the `errorText` of other errors | [tools.ts:372](../src/tools.ts#L372), [log.ts:39-43](../src/log.ts#L39-L43) |
| With a `progressToken` in `_meta`, `notifications/progress` is sent at the start of each wait and every 60 s, with `progress` 0, 1, 2, ... and a `message` | [tools.ts:1011-1021](../src/tools.ts#L1011-L1021), [messages.ts:200-204](../src/messages.ts#L200-L204) |
| The call is aborted when the client closes the response stream or sends `notifications/cancelled`; the action is kept | [src-BhnGYybN.cjs:6338-6341](../node_modules/@modelcontextprotocol/server/dist/src-BhnGYybN.cjs#L6338-L6341), [:6425](../node_modules/@modelcontextprotocol/server/dist/src-BhnGYybN.cjs#L6425) |
| A call with the same tool and action fields as an action in memory is attached to it | [tools.ts:1003-1005](../src/tools.ts#L1003-L1005) |
| Messages are marked read after the last wait of a call that is not aborted | [tools.ts:460-461](../src/tools.ts#L460-L461), [:598-600](../src/tools.ts#L598-L600), [:679-680](../src/tools.ts#L679-L680) |
Action fields of the key:
| Tool | Fields |
|---|---|
| `connect` | `target`<br>`incognito` (default `false`)<br>`message` (default `null`) |
| `send` | `chat`<br>`text`<br>`reply_to` (default `null`)<br>`mention` (default `null`) |
| `leave` | `chat` |
Result status of the three action tools:
| Status | Meaning | Extra fields |
|---|---|---|
| `done` | performed | per tool |
| `failed` | failure of the card post, the counter write or the perform, or loss of the control chat | `error` |
| `rejected` | an approver sent `/reject` | |
| `expired` | no decision before the expiry time | |
| `known` | `connect` only: the target is an existing chat past the prepared state; no action | chat fields |
### 1.2 `connect`
Handler: [`AgentTools.connect`](../src/tools.ts#L412-L425). Perform: [`performConnect`](../src/tools.ts#L480-L512). Schemas: [input](../src/tools.ts#L167-L178), [output](../src/tools.ts#L220-L230).
Input:
| Field | Type | Range | Default | Rule |
|---|---|---|---|---|
| `target` | string | 1 character or more | required | SimpleX link (`https://...`, `simplex:/...`), contact name `@name.simplex` or channel name `#name.simplex`; whitespace refused |
| `incognito` | boolean | | `false` | `true`: `incognito=on` in the connect command |
| `message` | string | 1 to 2000 characters | | first message of a contact request; plans with `takesMessage` only |
| `wait` | number | 0 to 86400 | 60 | seconds from the approval: for the other side to accept and, with `message`, for its first reply |
| `idle` | number | 1 to 300 | 30 | seconds without a new message, after which the server ends the wait for messages |
Output fields by case:
| Case | Fields | Code |
|---|---|---|
| `known` | `status`, chat fields of the known chat | [tools.ts:417](../src/tools.ts#L417) |
| `done`, chat unknown | `action`, `status`, `type`, `connected: false`, plan fields `name`, `peer_type`, `description`, `welcome`, `commands`, `known_as` when present | [tools.ts:428](../src/tools.ts#L428), [:899-905](../src/tools.ts#L899-L905) |
| `done`, chat known | `action`, `status`, chat fields, `messages` | [tools.ts:444-464](../src/tools.ts#L444-L464) |
| `failed` | `action`, `status`, `error` | [tools.ts:435](../src/tools.ts#L435) |
| `rejected`, `expired` | `action`, `status` | [tools.ts:435](../src/tools.ts#L435) |
Chat fields ([`chatFields`](../src/tools.ts#L740-L767)):
| Field | Contact | Group, business chat or channel |
|---|---|---|
| `chat` | `@contactId` | `#groupId` |
| `type` | `contact` | `business` with `businessChat`; `channel` with `useRelays` or group type `channel`; otherwise `group` |
| `name` | profile display name | group display name |
| `simplex_name` | `@` + `contactDomain.domain` when verified | `#` + `groupDomainClaim.domain` when verified |
| `peer_type` | profile peer type, when present | from the plan, when present |
| `description` | profile `shortDescr` | group `shortDescr` |
| `welcome` | from the plan: link data message | group `description` |
| `commands` | profile `preferences.commands` | `groupPreferences.commands` |
| `known_as` | incognito: `customUserProfile` of `/_info @C` | incognito: the membership's display name |
| `role` | | the agent's member role |
| `connected` | active connection status `ready` | membership status `connected` or `complete` |
`messages`: message items among the last 50 items of the chat when the result is built, replaced by their collected copies, followed by the items collected since the connection; live items are left out; new items are marked read ([tools.ts:446](../src/tools.ts#L446), [:456-463](../src/tools.ts#L456-L463)).
Tool errors:
| Text | Condition | Code |
|---|---|---|
| `Not a SimpleX link or name: TARGET` | whitespace in `target`, or `invalidConnReq` from the plan command | [tools.ts:468](../src/tools.ts#L468), [:474](../src/tools.ts#L474) |
| an app error text, for example `Name not found: This SimpleX name is not registered. Please check the name.`, or `SimpleX error: CHAIN: DETAILS` | other errors of the plan command, or a plan of type `error` | [tools.ts:475](../src/tools.ts#L475), [:826](../src/tools.ts#L826), [log.ts:45-47](../src/log.ts#L45-L47) |
| `TARGET can't be joined now: PLAN.` | `invitationLink ownLink` or `connecting`<br>`contactAddress ownLink` or `connectingProhibit`<br>`groupLink ownLink`, `connectingProhibit`, `noRelays` or `updateRequired` | [tools.ts:817](../src/tools.ts#L817), [:830](../src/tools.ts#L830), [:848](../src/tools.ts#L848), [:870](../src/tools.ts#L870) |
| `message: TARGET is not a contact or business address with short link data, or its contact exists without a connection.` | `message` given and the plan has `takesMessage: false` | [tools.ts:419](../src/tools.ts#L419) |
| refusals | see [1.8](#18-action-refusals) | |
`takesMessage` is `true` for a contact address with short link data, for a known prepared contact, and for a known prepared business group ([tools.ts:821](../src/tools.ts#L821), [:841](../src/tools.ts#L841), [:859](../src/tools.ts#L859)).
### 1.3 `send`
Handler: [`AgentTools.send`](../src/tools.ts#L573-L588). Result: [`sendResult`](../src/tools.ts#L590-L608). Perform: [`performSend`](../src/tools.ts#L610-L629). Schemas: [input](../src/tools.ts#L180-L193), [output](../src/tools.ts#L232-L244).
Input:
| Field | Type | Range | Default | Rule |
|---|---|---|---|---|
| `chat` | string | | required | `@N` or `#N` of the agent |
| `text` | string | 1 to 12000 characters | required | sent as one text message |
| `reply_to` | integer | 1 or more | | item ID in `chat` |
| `mention` | integer array | 1 to 3, unique, each 1 or more | | member IDs of a group; names missing from `text` are prefixed as `@name` |
| `wait` | number | 0 to 86400 | 60 | seconds from the send; from the call when it attaches to a done action |
| `idle` | number | 1 to 300 | 30 | seconds without a new response, once one arrived |
| `limit` | integer | 1 to 100 | 50 | most responses returned |
Output:
| Field | Presence | Value |
|---|---|---|
| `action` | always | action number |
| `status` | always | `done`, `failed`, `rejected`, `expired` |
| `chat` | `done` | the chat reference |
| `item` | `done` | item ID of the sent message |
| `responses` | `done` | received message items with IDs above `item`, in arrival order, live items left out, at most `limit` |
| `more` | `done` | collected responses not returned, live ones and overflow included |
| `error` | `failed` | error text |
Tool errors:
| Text | Condition | Code |
|---|---|---|
| `No chat CHAT. list shows the agent's chats.` | invalid reference, the control chat, or a store error when the chat is read | [tools.ts:714-721](../src/tools.ts#L714-L721) |
| `The agent has role ROLE in CHAT and can't post.` | membership role `observer` or `relay` | [tools.ts:576-579](../src/tools.ts#L576-L579) |
| `reply_to: CHAT has no item N.` | the read of `around=N` fails, or the item is absent from it | [tools.ts:723-728](../src/tools.ts#L723-L728) |
| `mention: CHAT is a contact chat; mentions apply to groups and channels.` | `mention` with `@N` | [tools.ts:731](../src/tools.ts#L731) |
| `mention: CHAT has no member N.` | ID absent from `/_members` | [tools.ts:735](../src/tools.ts#L735) |
| refusals | see [1.8](#18-action-refusals) | |
### 1.4 `receive`
Handler: [`AgentTools.receive`](../src/tools.ts#L653-L686). Schemas: [input](../src/tools.ts#L195-L204), [output](../src/tools.ts#L246-L254).
Input:
| Field | Type | Range | Default | Rule |
|---|---|---|---|---|
| `chat` | string | | required | `@N` or `#N` of the agent |
| `wait` | number | 0 to 86400 | 0 | seconds to wait when no message is new |
| `limit` | integer | 1 to 100 | 50 | most messages returned |
Output (all fields always present):
| Field | Value |
|---|---|
| `chat` | the chat reference |
| `messages` | unread received messages in chat order, then received messages that arrived during the call; live items left out; at most `limit` |
| `more` | new messages not returned, overflow, and unread items left unread by the page reads |
Tool errors: `No chat CHAT. list shows the agent's chats.` for an invalid reference, the control chat, or a store error while the unread items are read ([tools.ts:655](../src/tools.ts#L655), [:659-661](../src/tools.ts#L659-L661)); other chat errors as their error text ([log.ts:39-47](../src/log.ts#L39-L47)).
Messages are read by `receive` without an action or an approval.
### 1.5 `list`
Handler: [`AgentTools.listChats`](../src/tools.ts#L688-L696). Schemas: [input](../src/tools.ts#L206), [output](../src/tools.ts#L256-L288).
Input: an empty object.
Output:
| Field | Item fields |
|---|---|
| `chats` | `chat`<br>`type`<br>`name`<br>`simplex_name`, when verified<br>`peer_type`, when present<br>`incognito`<br>`known_as`, when incognito<br>`connected`<br>`unread`: `chatStats.unreadCount`<br>`approved: true`, only under a standing approval in mode `requests` |
| `actions` | `action`<br>`tool`<br>`input`: the input of the creating call<br>`state`<br>`open`: `true` while a call waits on it |
`chats`: the contacts and groups from `/_get chats USER pcc=off`, paged by 100 with `before=CHAT_TS`, except the control chat ([tools.ts:698-712](../src/tools.ts#L698-L712)). `actions`: every action in memory ([tools.ts:694](../src/tools.ts#L694)).
### 1.6 `leave`
Handler: [`AgentTools.leave`](../src/tools.ts#L631-L640). Perform: [`performLeave`](../src/tools.ts#L646-L651). Schemas: [input](../src/tools.ts#L208-L215), [output](../src/tools.ts#L290-L299).
Input: `chat`, string, required, `@N` or `#N` of the agent.
Output:
| Field | Presence | Value |
|---|---|---|
| `action` | always | action number |
| `status` | always | `done`, `failed`, `rejected`, `expired` |
| `chat` | `done` | the deleted chat |
| `error` | `failed` | error text |
Perform:
| Chat | Commands |
|---|---|
| `@N` | `/_delete @N full` |
| `#N` | `/_leave #N` while the membership is current, `pending_approval` or `pending_review`<br>`/_delete #N full` |
Tool errors: `No chat CHAT. list shows the agent's chats.` when no action with the same key is in memory and the chat is invalid, the control chat or unknown ([tools.ts:634-636](../src/tools.ts#L634-L636)); refusals.
### 1.7 Message and command objects
Message ([`Message`](../src/messages.ts#L11-L22), [`toMessage`](../src/messages.ts#L135-L152), [schema](../src/tools.ts#L119-L139)):
| Field | Type | Presence | Source |
|---|---|---|---|
| `chat` | string | always | chat reference of the item |
| `item` | integer | always | `meta.itemId` |
| `from` | string | always | `groupRcv`: member display name<br>`directRcv`: contact display name<br>`channelRcv`: channel display name<br>sent items in `connect`: the agent's name in that chat |
| `from_member` | integer | `groupRcv` | `groupMemberId` |
| `from_type` | string | when the profile has a peer type | sender's `peerType`; the agent's own for its non-incognito items in `connect` |
| `time` | string | always | `meta.itemTs` |
| `text` | string | always | `msgContent.text` |
| `new` | boolean | always | item status `rcvNew` when read |
| `reply_to` | object | quoted item with an ID | `{chat, item: quotedItem.itemId}` |
| `mentions_me` | boolean | always | `meta.userMention` |
Only received message items (`rcvMsgContent`) are returned by `send` and `receive`; `connect` also returns sent ones (`sndMsgContent`) ([messages.ts:73](../src/messages.ts#L73), [:123-125](../src/messages.ts#L123-L125)).
Command ([`commandsOf`](../src/tools.ts#L985-L992), [schema](../src/tools.ts#L141-L151)):
| Form | Fields |
|---|---|
| command | `keyword`<br>`label`<br>`params`, when present |
| menu | `label`<br>`commands`: commands and menus |
### 1.8 Action refusals
Refusals of a new `connect`, `send` or `leave` action, first match ([actions.ts:136](../src/actions.ts#L136), [control.ts:105-111](../src/control.ts#L105-L111)):
| Text | Condition |
|---|---|
| `The server is stopping.` | `Actions.stop` has started |
| `The agent lost its control chat. The person who runs it has to restart it with --reset-owners.` | control chat lost |
| `20 actions are pending; wait for the owner's decisions.` | 20 actions in state `pending` |
| `text: too long for its approval card; shorten it.` | `send` whose largest card form exceeds 15000 bytes |
| `The agent has no owner. The person who runs it has to join its control chat with the link the server printed.` | no approver, `--owners` absent |
| `The agent has no approving owner. The person who runs it has to restart it with the --owners options it printed.` | no approver, `--owners` given |
Largest card form ([control.ts:476-484](../src/control.ts#L476-L484)): the maximum of the creation form, the `done` form and a `failed` form with an error of 600 × `€`, with the action's own decider or the longest current approver name; each form is measured as JSON-escaped UTF-8: `"`, `\`, `\n`, `\r`, `\t` 2 bytes, other characters below U+0020 6 bytes.
---
## 2. Agent control chat
### 2.1 Commands and replies
Accepted text ([`parseCommand`](../src/control.ts#L410-L415), [`ownerRequest`](../src/control.ts#L521-L530)):
```
message one line, an optional trailing newline
line spaces, "/", KEYWORD, PARAMS
KEYWORD characters up to the first whitespace
PARAMS the rest, trimmed, split on whitespace
CHAT @N or #N, 1 to 15 digits
requests
approve N
approve CHAT N
reject N
reject CHAT N
approve CHAT all
revoke CHAT
pending
list
```
The server ignores other text. It answers a command with a reply that quotes it ([control.ts:142](../src/control.ts#L142)), and it runs the expiry check before each command ([control.ts:164](../src/control.ts#L164)).
| Request | Condition | Reply | Code |
|---|---|---|---|
| one-line `/` text outside the requests | any sender | `Commands: /approve, /reject, /revoke, /pending, /list. Each card shows its commands.` | [control.ts:15](../src/control.ts#L15), [:166](../src/control.ts#L166) |
| any request | sender not an approver, `--owners` absent | `Only owners approve actions.` | [control.ts:167](../src/control.ts#L167) |
| any request | sender not an approver, `--owners` given | `Only the owners listed in --owners approve actions.` | [control.ts:167](../src/control.ts#L167) |
| `approve`, `reject` | action N absent, not `pending`, or without a card | `Action N is not pending.` | [control.ts:182](../src/control.ts#L182) |
| `approve CHAT N`, `reject CHAT N` | action N is a `connect`, or for another chat | `Action N is not for CHAT.` | [control.ts:183](../src/control.ts#L183) |
| `approve`, `reject` | accepted | none; the card is edited at the outcome | [control.ts:184-187](../src/control.ts#L184-L187) |
| `approve CHAT all` | CHAT unknown or the control chat | `No chat CHAT. /list shows the agent's chats.` | [control.ts:194](../src/control.ts#L194) |
| `approve CHAT all` | accepted | `Approved for KIND NAME (CHAT) by APPROVER. /'revoke CHAT'` | [control.ts:202](../src/control.ts#L202) |
| `revoke CHAT` | CHAT unknown, or without a standing approval | `Messages to CHAT have no standing approval.` | [control.ts:207](../src/control.ts#L207) |
| `revoke CHAT` | accepted | `Messages to KIND NAME (CHAT) need approval again.` | [control.ts:210](../src/control.ts#L210) |
| `pending` | no pending action with a card | `No pending actions.` | [control.ts:171](../src/control.ts#L171) |
| `pending` | otherwise | one line per action: `N WHAT expires YYYY-MM-DD HH:MM UTC` | [control.ts:170](../src/control.ts#L170), [:472-474](../src/control.ts#L472-L474) |
| `list` | no chats | `No chats.` | [control.ts:220](../src/control.ts#L220) |
| `list` | otherwise | one line per chat: `CHAT KIND NAME`, then `, approved for this chat by APPROVER` and `, not connected` when applicable | [control.ts:213-221](../src/control.ts#L213-L221) |
KIND is `contact`, `bot`, `agent`, `business`, `group` or `channel` ([tools.ts:928-935](../src/tools.ts#L928-L935)). WHAT is `connect to SUBJECT`, `send to SUBJECT`, `delete SUBJECT` or `leave SUBJECT` ([actions.ts:268-278](../src/actions.ts#L268-L278)). Names are shown by `safeName` ([control.ts:498-501](../src/control.ts#L498-L501)).
### 2.2 Cards
Card text: request lines, then one last line ([`cardText`](../src/control.ts#L417-L436), [`requestLines`](../src/control.ts#L438-L459)).
Request lines:
| Tool | First line | Further lines |
|---|---|---|
| `connect` | `I want to connect to SUBJECT, as myself.` or `, incognito.`<br>done and connected: `I connected to ...`<br>done and not connected: `I asked to connect to ...` | link description, one code span per line<br>welcome, one code span per line<br>with `message`: `With the message:` and its lines |
| `send` | `I want to send a message to SUBJECT:`, with `, where I am incognito` before the colon in incognito chats<br>done: `I sent a message to ...` | text, one code span per line<br>`replying to NAME: QUOTE` with `reply_to`<br>`mentioning @A, @'B C'` with `mention` |
| `leave` | `I want to delete KIND NAME (@N).` or `I want to leave KIND NAME (#N).`<br>done: `I deleted ...` or `I left ...` | |
SUBJECT is `KIND NAME`, or `an unnamed KIND`, followed by ` (SIMPLEX_NAME)` when known ([actions.ts:263-266](../src/actions.ts#L263-L266)). For `connect`, SIMPLEX_NAME is the target when it is a name; for `send`, the chat's verified SimpleX name.
Last line:
| State | Condition | Last line |
|---|---|---|
| `pending` | | command line |
| `pending` | interrupted | `Cancelled: the server stopped.` |
| `performing` | interrupted | `DECIDER; interrupted: the server stopped.` |
| `done` | standing | `Approved for this chat by NAME. /'revoke CHAT'` |
| `done` | | `DECIDER.` |
| `failed` | | ``DECIDER; failed: `ERROR`.`` |
| `rejected` | | `Rejected by NAME.` |
| `expired` | | `Expired.` |
DECIDER is `Performed without approval (mode host)` in mode `host`, `Approved for this chat by NAME` for a standing send, and `Approved by NAME` otherwise ([control.ts:419](../src/control.ts#L419)).
Command lines ([`commandsLine`](../src/control.ts#L461-L470)):
| Tool | Command line |
|---|---|
| `connect` | `/'approve N' or /'reject N'` |
| `send` | `/'approve CHAT N', /'approve CHAT all' or /'reject CHAT N'` |
| `leave` | `/'approve CHAT N' or /'reject CHAT N'` |
The app shows `/'approve N'` as the tappable command `/approve N` ([PLAN, What the owner sees](../PLAN.md#what-the-owner-sees-the-agent-control-chat)).
### 2.3 Membership messages
Posted in the chat and logged; in the chat, names are shown by `safeName` and the `--owners ...` part is a code span; in the log, names are folded to one line ([control.ts:322-325](../src/control.ts#L322-L325), [:64-65](../src/control.ts#L64-L65)).
| Situation | Text | Code |
|---|---|---|
| owner joined, `--owners` absent | `I'm your agent. I ask here before I connect or send.` (chat only) | [control.ts:19](../src/control.ts#L19), [:270](../src/control.ts#L270) |
| member joined, `--owners` without a list | `NAME joined (member ID). Restart the server with --owners ENTRIES` | [control.ts:275-278](../src/control.ts#L275-L278) |
| start with `--owners` without a list and humans present | `A joined (member ID). B joined (member ID). Restart the server with --owners ENTRIES` | [control.ts:242-245](../src/control.ts#L242-L245) |
| a human became owner outside the list | `NAME (member ID) is now an owner and can't approve actions yet. Restart the server with --owners ENTRIES` | [control.ts:283-287](../src/control.ts#L283-L287) |
| a listed owner stopped being a human owner | `NAME (member ID) is no longer an owner. Restart the server with --owners ENTRIES`<br>none remains: `NAME (member ID) is no longer an owner. No owners remain: restart the server with --reset-owners --owners` | [control.ts:288-293](../src/control.ts#L288-L293) |
ENTRIES: `ID:NAME` of each human, or of each human owner, joined by commas; an entry with characters other than letters, digits, `_` and `-` is quoted for the shell ([control.ts:581-591](../src/control.ts#L581-L591)).
### 2.4 Group profile
| Field | Value | Code |
|---|---|---|
| display name | the agent's display name, updated at start when it differs | [control.ts:91-93](../src/control.ts#L91-L93) |
| full name | empty | [control.ts:377](../src/control.ts#L377) |
| description | `Supervise your AI agent here: approve or reject its connections and messages with one tap.` | [control.ts:378](../src/control.ts#L378) |
| history | on | [control.ts:379](../src/control.ts#L379) |
| command menu | `/pending` "Pending actions"<br>`/list` "Chats and approvals"<br>updated at start when it differs | [control.ts:10-13](../src/control.ts#L10-L13), [:91-93](../src/control.ts#L91-L93) |
---
## 3. CLI
Binary: `simplex-mcp`, compiled `dist/cli.js` ([package.json:5-7](../package.json#L5-L7)). Help text: [cli.ts:10-35](../src/cli.ts#L10-L35).
```
simplex-mcp --help
simplex-mcp serve NAME [options]
simplex-mcp register NAME [options]
```
NAME: 1 to 32 characters of `[a-z0-9]` and single hyphens, starting with a letter or digit ([cli.ts:142-144](../src/cli.ts#L142-L144)).
### 3.1 `serve`
| Option | Value | Default | Rule | Code |
|---|---|---|---|---|
| `--data` | folder | `~/.simplex-mcp/NAME` | resolved to an absolute path; `dataGiven` is set | [cli.ts:65-68](../src/cli.ts#L65-L68) |
| `--display-name` | text | NAME when the user is created | must equal `validName(text)` | [cli.ts:69-71](../src/cli.ts#L69-L71), [:146-150](../src/cli.ts#L146-L150) |
| `--approve` | `requests` or `host` | `requests` | | [cli.ts:72-74](../src/cli.ts#L72-L74) |
| `--owners` | absent value, or `ID:NAME,...` | absent | the value is absent when the next argument is missing or starts with `--`; entries match `^([1-9]\d*):(.+)$` | [cli.ts:75-77](../src/cli.ts#L75-L77), [:167-171](../src/cli.ts#L167-L171) |
| `--expire` | hours | 23 | integer 1 to 23 | [cli.ts:78-80](../src/cli.ts#L78-L80) |
| `--reset-owners` | flag | off | refused with an `--owners` list | [cli.ts:81-83](../src/cli.ts#L81-L83), [:97-99](../src/cli.ts#L97-L99) |
| `--new-token` | flag | off | | [cli.ts:84-86](../src/cli.ts#L84-L86) |
| `--port` | number | 5226 | integer 1 to 65535 | [cli.ts:87-89](../src/cli.ts#L87-L89) |
| `--qr-code` | `small` or `large` | `small` | | [cli.ts:90-92](../src/cli.ts#L90-L92) |
### 3.2 `register`
| Option | Value | Default | Code |
|---|---|---|---|
| `--scope` | `user` or `local` | `user` | [cli.ts:107-109](../src/cli.ts#L107-L109) |
| `--data` | folder | `~/.simplex-mcp/NAME` | [cli.ts:110-112](../src/cli.ts#L110-L112) |
Entry ([`registryEntry`](../src/register.ts#L43-L50)):
| Field | Value |
|---|---|
| `type` | `http` |
| `url` | `http://127.0.0.1:PORT/mcp` |
| `headersHelper` | `cat DATA/headers.json`, Windows `type DATA\headers.json`, path shell-quoted |
| `timeout` | `(expireHours + 24) × 3600000 + 600000` ms |
Server name: `simplex-NAME` ([register.ts:33](../src/register.ts#L33)).
### 3.3 Exit codes and messages
| Code | Message | Code location |
|---|---|---|
| 0 | `--help` text on stdout; a stop after a signal | [cli.ts:194-196](../src/cli.ts#L194-L196), [serve.ts:53](../src/serve.ts#L53) |
| 2 | `Usage: simplex-mcp serve NAME [options] or simplex-mcp register NAME [options]. Run simplex-mcp --help.` | [cli.ts:45](../src/cli.ts#L45) |
| 2 | `NAME: 1 to 32 lowercase letters, digits and single hyphens, starting with a letter or digit.` | [cli.ts:46](../src/cli.ts#L46) |
| 2 | `OPTION: a value expected.` | [cli.ts:131](../src/cli.ts#L131) |
| 2 | `Unknown option OPTION. Run simplex-mcp --help.` | [cli.ts:184](../src/cli.ts#L184) |
| 2 | `--display-name: not a valid SimpleX name; try VALID` | [cli.ts:148](../src/cli.ts#L148) |
| 2 | `--approve: requests or host expected, got VALUE` | [cli.ts:153](../src/cli.ts#L153) |
| 2 | `--qr-code: small or large expected, got VALUE` | [cli.ts:158](../src/cli.ts#L158) |
| 2 | `--scope: user or local expected, got VALUE` | [cli.ts:163](../src/cli.ts#L163) |
| 2 | `--owners: ID:NAME expected, got ENTRY` | [cli.ts:169](../src/cli.ts#L169) |
| 2 | `--expire: hours from 1 to 23 expected, got VALUE` | [cli.ts:79](../src/cli.ts#L79), [:175](../src/cli.ts#L175) |
| 2 | `--port: a port from 1 to 65535 expected, got VALUE` | [cli.ts:88](../src/cli.ts#L88), [:175](../src/cli.ts#L175) |
| 2 | `--reset-owners takes --owners without a list: member IDs change in a new agent control chat.` | [cli.ts:98](../src/cli.ts#L98) |
| 2 | `--owners: member ENTRY is not in the agent control chat. Start with --reset-owners --owners, or use the IDs of current members.` | [control.ts:251](../src/control.ts#L251) |
| 1 | `Another server uses DATA (PID N).` | [files.ts:21](../src/files.ts#L21), [:23](../src/files.ts#L23) |
| 1 | `Port PORT is in use. Start with --port N.` | [serve.ts:205](../src/serve.ts#L205) |
| 1 | `Agent "NAME" was never started. Start it with: npx -y @simplex-chat/mcp serve NAME` | [register.ts:32](../src/register.ts#L32) |
| 1 | `Add this entry under LOCATION in ~/.claude.json: {"simplex-NAME": ENTRY}` | [register.ts:52-55](../src/register.ts#L52-L55) |
| 1 | `claude mcp add-json failed: STDERR` | [register.ts:39](../src/register.ts#L39) |
| 1 | `simplex-mcp: ERROR` for other errors, `stop: ERROR` for a failed stop | [cli.ts:210-211](../src/cli.ts#L210-L211), [serve.ts:55-56](../src/serve.ts#L55-L56) |
LOCATION is `"mcpServers"` for scope `user`, and `"projects" > "PROJECT" > "mcpServers"` for scope `local` ([register.ts:53](../src/register.ts#L53)).
### 3.4 Terminal lines
stdout ([log.ts:31-33](../src/log.ts#L31-L33)):
| Moment | Line | Code |
|---|---|---|
| start | `SimpleX MCP server VERSION, agent "NAME"`<br>`Data folder: DATA`<br>`Approval mode: MODE` | [serve.ts:72-74](../src/serve.ts#L72-L74) |
| control chat created | `Agent control chat "DISPLAY" created.` | [control.ts:383](../src/control.ts#L383) |
| link opened | `Join the agent control chat in the SimpleX app. The link works once:`<br>QR code<br>link<br>small QR code: `If the QR code does not scan, restart with --qr-code large, or paste the link into the app.`<br>role line | [control.ts:304-310](../src/control.ts#L304-L310) |
| listening | `Listening on http://127.0.0.1:PORT/mcp` | [serve.ts:120](../src/serve.ts#L120) |
| owner setup done | `Add the agent to Claude Code:`<br>` npx -y @simplex-chat/mcp register NAME [--data DATA]` | [serve.ts:164-166](../src/serve.ts#L164-L166) |
| owner joined | `NAME joined (member ID) and is now an owner. The link is deleted.` | [control.ts:271](../src/control.ts#L271) |
| listed owners set | `Owners: ENTRIES.` | [control.ts:262](../src/control.ts#L262) |
| approvers gone, `--owners` absent | `No owners in the agent control chat.` | [control.ts:20](../src/control.ts#L20), [:157](../src/control.ts#L157), [:238](../src/control.ts#L238) |
| control chat lost | `The agent lost its control chat. Restart the server with --reset-owners.` | [control.ts:298](../src/control.ts#L298) |
| action created | `Action N: WHAT` with `, as itself` or `, incognito` for `connect` and ` (CHAT)` for `send` and `leave` | [actions.ts:140-141](../src/actions.ts#L140-L141), [:268-278](../src/actions.ts#L268-L278) |
| perform ended, mode `requests` | `Action N approved by NAME: RESULT.` with RESULT `sent`, `deleted`, `left`, `connected`, `connection requested` or `failed: ERROR` | [actions.ts:238](../src/actions.ts#L238), [:284-291](../src/actions.ts#L284-L291) |
| perform ended, mode `host` | `Action N: performed without approval (mode host).`, or `...; failed: ERROR.` | [actions.ts:286](../src/actions.ts#L286) |
| rejected | `Action N rejected by NAME.` | [actions.ts:188](../src/actions.ts#L188) |
| expired | `Action N expired.` | [actions.ts:158](../src/actions.ts#L158) |
| stop | `Cancelled K action.` or `Cancelled K actions.` when K > 0<br>`Stopped.` | [serve.ts:155](../src/serve.ts#L155), [:159](../src/serve.ts#L159) |
| membership messages | see [2.3](#23-membership-messages) | |
stderr: `CONTEXT: ERROR` from `logError` ([log.ts:35-37](../src/log.ts#L35-L37)), with CONTEXT one of `action N`, `action N card`, `control chat command`, `EVENT in the agent control chat`, `EVENT event`, `MCP`, `HTTP request`, `stop`, `simplex-mcp`. The library's download lines are written to stderr, and its event processing errors to stdout ([libs.ts:114](../../simplex-chat-nodejs/src/libs.ts#L114), [api.ts:187](../../simplex-chat-nodejs/src/api.ts#L187)).
@@ -0,0 +1,579 @@
# SimpleX MCP server: architecture
> System structure, modules, state, process lifecycle, approval model and security boundaries of `@simplex-chat/mcp` 0.1.0.
>
> Related specs: [README](README.md) | [Flows](flows.md) | [API](api.md)
> Related docs: [README](../README.md) | [Reference](../REFERENCE.md) | [PLAN](../PLAN.md)
**Source:** [`cli.ts`](../src/cli.ts) | [`serve.ts`](../src/serve.ts) | [`http.ts`](../src/http.ts) | [`tools.ts`](../src/tools.ts) | [`actions.ts`](../src/actions.ts) | [`control.ts`](../src/control.ts) | [`messages.ts`](../src/messages.ts) | [`files.ts`](../src/files.ts) | [`register.ts`](../src/register.ts) | [`log.ts`](../src/log.ts)
---
## Table of contents
1. [System diagram](#1-system-diagram)
2. [Modules](#2-modules)
3. [State](#3-state)
4. [Process lifecycle](#4-process-lifecycle)
5. [Approval model](#5-approval-model)
6. [Security boundaries](#6-security-boundaries)
7. [Limits](#7-limits)
---
## 1. System diagram
```mermaid
flowchart LR
subgraph host["MCP host"]
cc["Claude Code"]
end
subgraph proc["Node.js process: simplex-mcp serve NAME"]
http["http.ts<br/>127.0.0.1:PORT/mcp<br/>Host and Origin guards<br/>bearer token<br/>path /mcp"]
sdk["MCP SDK<br/>createMcpHandler<br/>McpServer per request"]
tools["tools.ts<br/>AgentTools"]
actions["actions.ts<br/>Actions"]
control["control.ts<br/>Control"]
messages["messages.ts<br/>ChatItemsWatch"]
serve["serve.ts<br/>start, events, stop"]
files["files.ts"]
log["log.ts"]
lib["simplex-chat<br/>ChatApi"]
core["SimpleX core<br/>libsimplex core-7.1.0.12"]
end
cli["cli.ts"] --> serve
cli --> register["register.ts"]
register -->|"claude mcp add-json"| cc
cc -->|"headersHelper:<br/>cat headers.json"| data
cc -->|"POST /mcp<br/>Authorization: Bearer"| http
http --> sdk --> tools
tools --> actions
actions <--> control
tools --> messages
serve --> control
serve --> tools
serve --> messages
serve --> files
files --> data[("DATA:<br/>token<br/>headers.json<br/>server.json<br/>lock")]
tools --> lib
control --> lib
serve --> lib
lib --> core
core --> db[("DATA:<br/>simplex_chat.db<br/>simplex_agent.db")]
core <-->|"SMP<br/>private routing"| smp["SMP servers"]
smp <--> owner["Owner's SimpleX app<br/>agent control chat"]
smp <--> peers["Peers:<br/>contacts and bots<br/>groups and channels"]
log --> term["Terminal:<br/>stdout<br/>stderr"]
```
Data flow of a tool call:
1. Claude Code runs the `headersHelper` command of its entry ([`register.ts:47`](../src/register.ts#L47)) and sends the `Authorization` header it prints ([Reference](../REFERENCE.md#add-the-agent-to-claude-code)).
2. The server checks Host, Origin, token and path, then passes the request to the SDK handler ([`http.ts:18-26`](../src/http.ts#L18-L26)).
3. An `McpServer` is built from the factory for each request, and the arguments are validated against the input schema by the SDK ([`serve.ts:108-112`](../src/serve.ts#L108-L112), [`mcp-BumlNux0.cjs:1777-1791`](../node_modules/@modelcontextprotocol/server/dist/mcp-BumlNux0.cjs#L1777-L1791)).
4. The handler result is returned by `AgentTools.call` as `structuredContent` and as JSON text ([`tools.ts:367-374`](../src/tools.ts#L367-L374)).
5. The server sends chat commands through `ChatApi` to the core ([`messages.ts:169-173`](../src/messages.ts#L169-L173)); actions are created in `Actions` and shown to the owner by `Control` ([`actions.ts:119-146`](../src/actions.ts#L119-L146)).
6. The core emits events; the server dispatches them in one handler of `serve.ts` to the item watch, the connect actions and the agent control chat ([`serve.ts:123-144`](../src/serve.ts#L123-L144)).
---
## 2. Modules
### [2.1 `cli.ts`](../src/cli.ts)
Entry point of the `simplex-mcp` binary.
| Symbol | Line | Contents |
|---|---|---|
| `helpText` | [10](../src/cli.ts#L10) | `--help` output |
| `Command` | [37](../src/cli.ts#L37) | `help`, `serve` with `ServeOptions`, `register` with `RegisterOptions` |
| `parseCommand` | [42](../src/cli.ts#L42) | command, NAME and options to a `Command` |
| `serveOptions` | [51](../src/cli.ts#L51) | defaults and option switch of `serve` |
| `registerOptions` | [103](../src/cli.ts#L103) | defaults and option switch of `register` |
| `OptionReader` | [120](../src/cli.ts#L120) | `value`: the next argument, required<br>`optionalValue`: the next argument, absent when missing or starting with `--` |
| `validAgentName` | [142](../src/cli.ts#L142) | `^[a-z0-9](-?[a-z0-9])*-?$`, at most 32 characters |
| `usageError` | [187](../src/cli.ts#L187) | `ExitError` with code 2 |
| `main` | [191](../src/cli.ts#L191) | `help` to stdout, `serve`, `register` |
| top-level catch | [204-213](../src/cli.ts#L204-L213) | `ExitError`: message on stderr, its code; other errors: `logError`, code 1 |
### [2.2 `serve.ts`](../src/serve.ts)
Start sequence, event dispatch and stop sequence of one agent.
| Symbol | Line | Contents |
|---|---|---|
| `ServeOptions` | [26](../src/serve.ts#L26) | name, data folder and its `dataGiven` flag, display name, mode, owners, expiry, reset and token flags, port, QR code size |
| `OwnerEntry` | [21](../src/serve.ts#L21) | `{memberId, name}` of `--owners` |
| `RunningServer` | [40](../src/serve.ts#L40) | token, `ChatApi`, user, `Actions`, `stop` |
| `serve` | [48](../src/serve.ts#L48) | `startServer`, then SIGINT and SIGTERM handlers with one shared stop promise |
| `startServer` | [64](../src/serve.ts#L64) | data folder, lock, token, headers file, `ChatApi.init`, `startAgent`; cleanup on error |
| `startAgent` | [87](../src/serve.ts#L87) | user, network config, control group, wiring of `Actions`, `AgentTools`, `Control`, HTTP, event handler, `stop` |
| `registerHint` | [164](../src/serve.ts#L164) | the `register` command line printed after the owner setup |
| `deliveries` | [168](../src/serve.ts#L168) | wait for pending deliveries of the control chat at stop |
| `controlGroup` | [194](../src/serve.ts#L194) | found, created or reset agent control chat |
| `listen` | [201](../src/serve.ts#L201) | `startHttp`; `EADDRINUSE` to `ExitError` code 1 |
| `bufferEvents` | [210](../src/serve.ts#L210) | events received before the handler is installed are queued and replayed in order |
### [2.3 `http.ts`](../src/http.ts)
| Symbol | Line | Contents |
|---|---|---|
| `HttpOptions` | [7](../src/http.ts#L7) | port, token, `McpServerFactory` |
| `startHttp` | [13](../src/http.ts#L13) | `http.Server` on `127.0.0.1:port`: guards, SDK handler, timeouts |
| `authorized` | [32](../src/http.ts#L32) | length check and `timingSafeEqual` against `Bearer <token>` |
| `stopHttp` | [46](../src/http.ts#L46) | `server.close` and `closeAllConnections` |
### [2.4 `tools.ts`](../src/tools.ts)
Tool definitions, call handlers, connect plans and perform steps.
| Symbol | Line | Contents |
|---|---|---|
| `ConnectInput`, `SendInput`, `ReceiveInput`, `LeaveInput` | [31-57](../src/tools.ts#L31-L57) | tool inputs |
| `PlanSummary` | [63](../src/tools.ts#L63) | card kind, chat type, name, SimpleX name, peer type, description, welcome, commands, `takesMessage` |
| `ChatFields` | [75](../src/tools.ts#L75) | chat fields of `connect` and `list` results |
| `Agent` | [89](../src/tools.ts#L89) | `ChatApi`, user, `Actions`, `ChatItemsWatch`, control chat reference, standing approver lookup |
| `JoinMethod` | [98](../src/tools.ts#L98) | `prepared`, `prepareChannel`, `viaAddress`, `prepareContact`, `connect` |
| schemas | [119-299](../src/tools.ts#L119-L299) | input and output JSON schemas |
| descriptions, `instructions` | [301-332](../src/tools.ts#L301-L332) | tool descriptions and server instructions |
| `definitions` | [342](../src/tools.ts#L342) | name, description, schemas and action flag of the five tools |
| `AgentTools.register` | [355](../src/tools.ts#L355) | `registerTool` for each definition; `_meta` in mode `host` |
| `AgentTools.call` | [367](../src/tools.ts#L367) | handler result to `CallToolResult`; errors to `isError` |
| `AgentTools.perform` | [376](../src/tools.ts#L376) | perform step of an approved action |
| `onContactConnected`, `onUserJoinedGroup` | [387-395](../src/tools.ts#L387-L395) | completion of connect actions by events |
| `actionResult` | [431](../src/tools.ts#L431) | wait for the decision, result fields, removal |
| `agentChats`, `knownChat` | [698-721](../src/tools.ts#L698-L721) | the agent's chats; a checked chat reference |
| `classifyPlan` | [811](../src/tools.ts#L811) | `connectionPlan` response to a join plan or a known chat |
| `chatSubject`, `isConnected`, `isActive`, `memberCurrent` | [911-964](../src/tools.ts#L911-L964) | chat and member predicates shared with `control.ts` |
| `toolCall` | [1011](../src/tools.ts#L1011) | `ServerContext` to `ToolCall`: abort signal and progress notifications |
### [2.5 `actions.ts`](../src/actions.ts)
| Symbol | Line | Contents |
|---|---|---|
| `Mode` | [5](../src/actions.ts#L5) | `requests`, `host` |
| `State` | [6](../src/actions.ts#L6) | `pending`, `performing`, `done`, `failed`, `rejected`, `expired` |
| `ToolError` | [16](../src/actions.ts#L16) | errors the model can correct; text returned as is |
| `Subject` | [18](../src/actions.ts#L18) | kind, name, SimpleX name, incognito flag of a card |
| `ConnRef` | [25](../src/actions.ts#L25) | pending connection, contact or group of a connect action |
| `ActionRecord` | [31](../src/actions.ts#L31) | number, key, times, change signal, standing flag, state, card, decider, open calls, interrupted flag, error |
| `ConnectAction`, `SendAction`, `LeaveAction` | [47-79](../src/actions.ts#L47-L79) | per-tool fields |
| `ActionHooks` | [85](../src/actions.ts#L85) | `refusal`, `saveCounter`, `postCard`, `perform`, `outcome` |
| `Actions` | [93](../src/actions.ts#L93) | action map, expiry timer, posting and outcome promises |
| `create` | [119](../src/actions.ts#L119) | refusal checks, number, log line, counter write, card |
| `decided` | [166](../src/actions.ts#L166) | wait with progress until a decided state or the abort signal |
| `stop` | [200](../src/actions.ts#L200) | cancellation of pending and performing actions |
| `summary`, `performedText`, `progressText` | [268-304](../src/actions.ts#L268-L304) | log and progress texts |
Hooks are bound in [`serve.ts:97-103`](../src/serve.ts#L97-L103): `refusal`, `saveCounter`, `postCard` and `outcome` to `Control`; `perform` to `AgentTools`.
### [2.6 `control.ts`](../src/control.ts)
| Symbol | Line | Contents |
|---|---|---|
| `ControlData` | [31](../src/control.ts#L31) | `{control: true, nextAction}` |
| `ChatApproval` | [48](../src/control.ts#L48) | `{approvedBy, approvedById, approvedAt}` |
| `OwnerRequest` | [54](../src/control.ts#L54) | parsed owner command |
| `Control.load` | [88](../src/control.ts#L88) | lost check, group profile and menu, member list |
| `Control.start` | [97](../src/control.ts#L97) | owner setup per `--owners` |
| `Control.refusal` | [105](../src/control.ts#L105) | card size of a send, approver presence |
| `Control.standingApprover` | [113](../src/control.ts#L113) | approver name of a chat's standing approval |
| `Control.saveCounter` | [118](../src/control.ts#L118) | queued counter writes |
| `Control.postCard`, `Control.outcome` | [124-132](../src/control.ts#L124-L132) | card post and card edit |
| `Control.onMessage` | [134](../src/control.ts#L134) | owner commands, queued one at a time |
| `Control.onEvent` | [146](../src/control.ts#L146) | membership changes and loss of the control chat |
| `Control.ownerCommand` | [163](../src/control.ts#L163) | command dispatch and replies |
| `findControlGroup`, `createControlChat`, `resetControlChat` | [364-392](../src/control.ts#L364-L392) | control group lookup, creation, reset |
| `isControlEvent` | [394](../src/control.ts#L394) | membership events of the control group |
| `parseCommand` | [410](../src/control.ts#L410) | one-line `/keyword params` |
| `cardText` | [417](../src/control.ts#L417) | card text per state and mode |
| `largestCard`, `encodedLength` | [476-484](../src/control.ts#L476-L484) | card size check |
| `codeSpan`, `safeName`, `mentionText` | [494-506](../src/control.ts#L494-L506) | quoting of peer text |
| `ownerRequest` | [521](../src/control.ts#L521) | command words to an `OwnerRequest` |
| `qrCodeText` | [532](../src/control.ts#L532) | terminal QR code |
| `losesControl`, `isLost` | [548-563](../src/control.ts#L548-L563) | loss of the control chat |
| `entryText`, `restartText` | [581-591](../src/control.ts#L581-L591) | `--owners` entries in restart lines |
### [2.7 `messages.ts`](../src/messages.ts)
| Symbol | Line | Contents |
|---|---|---|
| `Message` | [11](../src/messages.ts#L11) | message item of tool results |
| `ToolCall` | [29](../src/messages.ts#L29) | abort signal and progress callback of one call |
| `Collected` | [34](../src/messages.ts#L34) | collected items, change signal, overflow count, time of the last change |
| `Changes` | [41](../src/messages.ts#L41) | set of wake callbacks |
| `ChatItemsWatch` | [58](../src/messages.ts#L58) | listeners per chat reference; received message items gathered by `collect` |
| `chatRef`, `isChatRef` | [104-117](../src/messages.ts#L104-L117) | `@N` and `#N` references |
| `toMessage` | [135](../src/messages.ts#L135) | chat item to `Message` |
| `chatCommand`, `getChat`, `markRead` | [169-181](../src/messages.ts#L169-L181) | raw chat commands with response type checks |
| `unreadItems` | [183](../src/messages.ts#L183) | unread items of a chat, paged |
| `reportProgress`, `nextChange`, `awaitResponses` | [200-230](../src/messages.ts#L200-L230) | waits |
| `validName`, `viewName` | [236-262](../src/messages.ts#L236-L262) | display name rules and `@name` form |
| `fullDomainName` | [264-267](../src/messages.ts#L264-L267) | `SimplexDomain` to `name.simplex` or `name.testing` |
### [2.8 `files.ts`](../src/files.ts)
| Symbol | Line | Contents |
|---|---|---|
| `createDataFolder` | [12](../src/files.ts#L12) | `mkdir -p` mode 0700; `chmod 0700` except on Windows |
| `acquireLock`, `releaseLock` | [17-28](../src/files.ts#L17-L28) | `DATA/lock` with the PID |
| `serverToken` | [30](../src/files.ts#L30) | `DATA/token`: read, or 32 random bytes in base64url |
| `writeHeadersFile` | [42](../src/files.ts#L42) | `DATA/headers.json` |
| `writeServerFile`, `readServerFile` | [46-53](../src/files.ts#L46-L53) | `DATA/server.json` |
### [2.9 `register.ts`](../src/register.ts)
| Symbol | Line | Contents |
|---|---|---|
| `register` | [30](../src/register.ts#L30) | `claude mcp remove`, then `claude mcp add-json` |
| `registryEntry` | [43](../src/register.ts#L43) | `{type, url, headersHelper, timeout}` |
| `manualEntryText` | [52](../src/register.ts#L52) | entry text when `claude` is missing |
| `projectRoot` | [57](../src/register.ts#L57) | project key of `--scope local` |
| `runCommand` | [83](../src/register.ts#L83) | `spawnSync` with ENOENT detection |
### [2.10 `log.ts`](../src/log.ts)
| Symbol | Line | Contents |
|---|---|---|
| `ExitError` | [12](../src/log.ts#L12) | message and exit code |
| `shellWord`, `shellPath` | [18-25](../src/log.ts#L18-L25) | POSIX single quotes or Windows double quotes |
| `oneLine` | [27](../src/log.ts#L27) | runs of `\p{Cc}`, `\p{Cf}`, `\p{Zl}`, `\p{Zp}` to one space |
| `log`, `logError` | [31-37](../src/log.ts#L31-L37) | stdout lines; stderr lines |
| `errorText`, `chatErrorText` | [39-47](../src/log.ts#L39-L47) | error text, folded to one line: the apps' wording for known error shapes, otherwise the type chain |
| `appErrorText` | [49-58](../src/log.ts#L49-L58) | wording of the apps' alerts for chat, agent and store errors |
| `chainText`, `errorChain` | [193-205](../src/log.ts#L193-L205) | `SimpleX error: <type chain>: <scalar fields>`, details at most 500 characters |
---
## 3. State
### 3.1 Memory
| Item | Holder | Written by | Read by |
|---|---|---|---|
| Actions | `Actions.actions` map ([actions.ts:96](../src/actions.ts#L96)) | `create`, `remove`, `expireDue` | tools, `Control` |
| Open calls of an action | `ActionRecord.open` ([:41](../src/actions.ts#L41)) | `actionResult` ([tools.ts:432](../src/tools.ts#L432), [:440](../src/tools.ts#L440)) | `list`, `expireDue` |
| Collected replies | `ConnectAction.messages`, `SendAction.responses` ([:52](../src/actions.ts#L52), [:69](../src/actions.ts#L69)) | `ChatItemsWatch.collect` | `connect`, `send` results |
| Item listeners | `ChatItemsWatch.listeners` ([messages.ts:59](../src/messages.ts#L59)) | `collect` | event dispatch |
| Members of the control chat | `Control.members` ([control.ts:68](../src/control.ts#L68)) | `refreshMembers` ([:332](../src/control.ts#L332)) | approver checks |
| Mode, expiry, owners list | `ServeOptions` of the run | CLI | all modules |
### 3.2 Chat database custom data
| Item | Entity | Value | Written by | Read by |
|---|---|---|---|---|
| Control marker and counter | agent control chat | `{simplexMcp: {control: true, nextAction: N}}` | [`createControlChat`](../src/control.ts#L374-L385), [`saveCounter`](../src/control.ts#L118) | [`findControlGroup`](../src/control.ts#L364), [`controlDataOf`](../src/control.ts#L369) |
| Standing approval | contact or group of the agent | `{...other keys, simplexMcp: {approvedBy, approvedById, approvedAt}}` | [`approveChat`](../src/control.ts#L192), [`revoke`](../src/control.ts#L205) | [`standingApprover`](../src/control.ts#L113) |
The profile, chats, items and members are stored by the core in `DATA/simplex_chat.db`; connections and queues in `DATA/simplex_agent.db` (file prefix `DATA/simplex`, [serve.ts:71](../src/serve.ts#L71)).
### 3.3 Files
| Path | Contents | Mode | Written | Read |
|---|---|---|---|---|
| `DATA/` | data folder | 0700, set at every start except on Windows | [files.ts:12](../src/files.ts#L12) | |
| `DATA/token` | base64url of 32 random bytes | 0600 at creation | first start, `--new-token` ([:30](../src/files.ts#L30)) | every start |
| `DATA/headers.json` | `{"Authorization":"Bearer <token>"}` | 0600 at creation | every start ([:42](../src/files.ts#L42)) | Claude Code `headersHelper` |
| `DATA/server.json` | `{name, port, expireHours}` | 0600 at creation | every start, after the user exists ([serve.ts:90](../src/serve.ts#L90)) | `register` |
| `DATA/lock` | PID and a newline | 0600 | start ([files.ts:57](../src/files.ts#L57)) | next start |
The core library is downloaded once into `CACHE/core-7.1.0.12/sqlite`, and the Node.js add-on into `CACHE/<add-on release>/`, each with a `Downloading ...` line on stderr. CACHE is `$XDG_CACHE_HOME/simplex-chat` or `~/.cache/simplex-chat`; on macOS `~/Library/Caches/simplex-chat`; on Windows `%LOCALAPPDATA%\simplex-chat` ([libs.ts:39-46](../../simplex-chat-nodejs/src/libs.ts#L39-L46), [:87](../../simplex-chat-nodejs/src/libs.ts#L87), [:107](../../simplex-chat-nodejs/src/libs.ts#L107), [:114](../../simplex-chat-nodejs/src/libs.ts#L114), [:127](../../simplex-chat-nodejs/src/libs.ts#L127)).
---
## 4. Process lifecycle
### 4.1 Start
1. Data folder, lock, token and headers file are prepared ([serve.ts:66-70](../src/serve.ts#L66-L70)).
2. libsimplex for SQLite is loaded by `ChatApi.init`, and the databases are migrated with confirmation `yesUp` ([serve.ts:71](../src/serve.ts#L71), [api.ts:114-123](../../simplex-chat-nodejs/src/api.ts#L114-L123)).
3. Three log lines: version and NAME, data folder, mode ([serve.ts:72-74](../src/serve.ts#L72-L74)).
4. The active user is read, or created with display name `--display-name` or NAME, empty full name and peer type `agent` ([serve.ts:89](../src/serve.ts#L89)).
5. `server.json` is written ([:90](../src/serve.ts#L90)).
6. The network config is set: `/network socks=off smp-proxy=always smp-proxy-fallback=protected` ([:91](../src/serve.ts#L91)).
7. Events are buffered, and the chat is started ([:92-93](../src/serve.ts#L92-L93)).
8. A `--display-name` different from the profile, or a peer type other than `agent`, is applied with one `apiUpdateProfile` ([:94](../src/serve.ts#L94), [:187-192](../src/serve.ts#L187-L192)).
9. The agent control chat is found by its marker, created with counter 1, or reset with `--reset-owners` ([:96](../src/serve.ts#L96), [:194-199](../src/serve.ts#L194-L199)).
10. `Actions`, `ChatItemsWatch`, `AgentTools` and `Control` are created; the counter is taken from the marker ([:97-106](../src/serve.ts#L97-L106)).
11. `Control.load`: for a lost control chat, `Actions.lost` is set; otherwise the group name and command menu are updated when they differ, and members are read ([control.ts:88-95](../src/control.ts#L88-L95)).
12. The HTTP server listens; with the port in use, the process exits with code 1 ([serve.ts:113](../src/serve.ts#L113), [:201-208](../src/serve.ts#L201-L208)).
13. The server runs the owner setup, `Control.start`; after an error, HTTP is closed and the error is rethrown ([serve.ts:114-119](../src/serve.ts#L114-L119)).
14. `Listening on http://127.0.0.1:PORT/mcp` ([:120](../src/serve.ts#L120)).
15. Buffered events are replayed, then live events are dispatched ([:123](../src/serve.ts#L123), [:210-224](../src/serve.ts#L210-L224)).
After an error inside `startAgent` (steps 4-15) the chat is closed; after any error once the lock is taken, the lock is released ([serve.ts:68-84](../src/serve.ts#L68-L84)).
### 4.2 Event dispatch
Each `onAny` receiver is awaited by the library before the next event is read ([api.ts:192-198](../../simplex-chat-nodejs/src/api.ts#L192-L198)). The dispatcher in [`serve.ts:123-144`](../src/serve.ts#L123-L144):
| Event | Condition | Handler |
|---|---|---|
| `newChatItems` | item of the control group, main scope | [`Control.onMessage`](../src/control.ts#L134) |
| `newChatItems` | every item | [`ChatItemsWatch.deliver`](../src/messages.ts#L85) |
| `chatItemUpdated` | | [`ChatItemsWatch.update`](../src/messages.ts#L89) |
| `chatItemsDeleted` | | [`ChatItemsWatch.remove`](../src/messages.ts#L93) of each `deletedChatItem` |
| `contactConnected` | | [`AgentTools.onContactConnected`](../src/tools.ts#L387) |
| `userJoinedGroup` | | [`AgentTools.onUserJoinedGroup`](../src/tools.ts#L392) |
| `joinedGroupMember`<br>`joinedGroupMemberConnecting`<br>`connectedToGroupMember`<br>`memberRole`<br>`leftMember`<br>`deletedMember`<br>`deletedMemberUser`<br>`groupDeleted` | event of the control group | [`Control.onEvent`](../src/control.ts#L146), queued one at a time |
| any event | after `stop()` started | ignored |
Owner commands are queued in a second chain, independent of the membership chain ([control.ts:140-143](../src/control.ts#L140-L143)).
### 4.3 Stop
On `SIGINT` or `SIGTERM` the server runs one stop, shared by all later signals ([serve.ts:50-61](../src/serve.ts#L50-L61)). `RunningServer.stop` ([serve.ts:150-160](../src/serve.ts#L150-L160)):
1. Events are ignored from now on.
2. `Actions.stop` starts: new actions are refused with `The server is stopping.`; the expiry timer is cleared; cards being posted are awaited; pending and performing actions are marked interrupted, and their cards are edited, or posted when they have no card; outcome edits already in progress are awaited ([actions.ts:200-209](../src/actions.ts#L200-L209)).
3. In parallel with step 2, the HTTP server is closed with all connections, and open calls are aborted ([http.ts:46-50](../src/http.ts#L46-L50)).
4. `Cancelled K action(s).` when K > 0 ([serve.ts:155](../src/serve.ts#L155)).
5. Pending deliveries of the control chat's member connections are polled with `/sql chat` and `/sql agent` every 100 ms, for at most 10 s ([serve.ts:168-176](../src/serve.ts#L168-L176)).
6. `chat.close()`: `/_stop`, then the store is closed ([api.ts:162-167](../../simplex-chat-nodejs/src/api.ts#L162-L167)).
7. The lock is removed, `Stopped.` is logged, and the process exits with code 0, or 1 after an error ([serve.ts:158-159](../src/serve.ts#L158-L159), [:52-58](../src/serve.ts#L52-L58)).
Performs in progress are not awaited; when one ends, its card stays as edited at the stop, because the action is interrupted ([actions.ts:239](../src/actions.ts#L239)).
### 4.4 The lock
```
acquireLock(DATA): files.ts:17
create DATA/lock exclusively (flag wx), PID :57
exists:
PID = number in the file
PID valid, other than ours, running: :21, :70-77
exit 1 "Another server uses DATA (PID N)."
remove the file; create it exclusively again :22-23
exists again: exit 1 with the same message
releaseLock(DATA): remove DATA/lock :26
```
`isRunning`: `true` when `kill(pid, 0)` succeeds or fails with `EPERM` ([files.ts:70-77](../src/files.ts#L70-L77)).
---
## 5. Approval model
### 5.1 Modes
The mode is set by `--approve` for one run ([cli.ts:72-74](../src/cli.ts#L72-L74)).
| Mode | State of a new action | Card | `_meta` of `connect`, `send`, `leave` |
|---|---|---|---|
| `requests` | `pending`<br>`performing` for a send under a standing approval | card with commands<br>outcome card after a standing send | none |
| `host` | `performing` | outcome card after the perform | `{"anthropic/requiresUserInteraction": true}` |
Sources: [actions.ts:130](../src/actions.ts#L130), [tools.ts:361](../src/tools.ts#L361). In both modes a new action is refused unless the control chat has an approver ([control.ts:107-110](../src/control.ts#L107-L110)).
### 5.2 Owners and approvers
| Term | Rule | Code |
|---|---|---|
| human | member other than the agent's membership, status `connected` or `complete` | [control.ts:353-355](../src/control.ts#L353-L355), [tools.ts:949-951](../src/tools.ts#L949-L951) |
| human owner | human with role `owner` | [control.ts:340-342](../src/control.ts#L340-L342) |
| approver, `--owners` absent | member with role `owner` | [control.ts:357-361](../src/control.ts#L357-L361) |
| approver, `--owners` without a list | none | [control.ts:360](../src/control.ts#L360) |
| approver, `--owners ID:NAME,...` | member with role `owner` whose `groupMemberId` and `displayName` match an entry | [control.ts:360](../src/control.ts#L360), [:565-567](../src/control.ts#L565-L567) |
The command sender's member record from the chat item is checked ([control.ts:139](../src/control.ts#L139), [:167](../src/control.ts#L167)). The approver count for refusals and standing approvals is taken from `Control.members`, refreshed at load and at each membership event ([control.ts:344-346](../src/control.ts#L344-L346)).
Owner setup at start ([control.ts:97-103](../src/control.ts#L97-L103)):
| `--owners` | Start | First join by the link |
|---|---|---|
| absent | a human owner exists: register hint<br>otherwise: `No owners in the agent control chat.` when humans exist, then a link with role `member` | link deleted, role `owner`, greeting, log line, register hint |
| without a list | no humans: link with role `observer`<br>humans exist: their IDs and the restart command, in the log and the chat | link deleted, the joined member and the restart command announced |
| `ID:NAME,...` | each entry matched to a human, or exit 2<br>role `owner` set where missing<br>other humans removed<br>link deleted<br>`Owners: ...` and register hint | |
Sources: [`startOwners`](../src/control.ts#L236), [`startObserver`](../src/control.ts#L242), [`startListed`](../src/control.ts#L247), [`ownerJoined`](../src/control.ts#L266), [`observerJoined`](../src/control.ts#L275).
Membership changes at run time ([control.ts:146-161](../src/control.ts#L146-L161)):
| Change | `--owners` | Server reaction |
|---|---|---|
| `deletedMemberUser`, `groupDeleted`, or the agent's role below `owner` | any | `Actions.loseControl`: pending actions set to `failed`; later cards left unedited; log line |
| approvers drop to zero after a human owner existed | absent | `No owners in the agent control chat.`, new link with role `member` |
| a human becomes owner outside the list | list | `<name> (member N) is now an owner and can't approve actions yet. Restart the server with --owners ...` |
| a listed owner stops being a human owner | list | `<name> (member N) is no longer an owner.` with the restart command, or `No owners remain: ...` |
### 5.3 Action state machine
Transitions, one per row:
| Event | From | To | Code |
|---|---|---|---|
| call creates the action, mode `requests` | | `pending` | [actions.ts:130](../src/actions.ts#L130) |
| call creates the action, mode `host` | | `performing` | [actions.ts:130](../src/actions.ts#L130) |
| `send` call creates the action under a standing approval | | `performing` | [actions.ts:130](../src/actions.ts#L130) |
| counter write or card post fails | `pending`<br>`performing` | `failed` | [actions.ts:211-221](../src/actions.ts#L211-L221) |
| stop arrives while the counter of a `performing` action is written | `performing` | `pending`, interrupted | [actions.ts:226](../src/actions.ts#L226) |
| control chat lost | `pending` | `failed` | [actions.ts:192-198](../src/actions.ts#L192-L198) |
| `/reject` | `pending` | `rejected` | [actions.ts:185-190](../src/actions.ts#L185-L190) |
| expiry time passes | `pending` | `expired` | [actions.ts:156-159](../src/actions.ts#L156-L159) |
| `/approve N`, `/approve CHAT N`, `/approve CHAT all` | `pending` | `performing` | [actions.ts:179-183](../src/actions.ts#L179-L183) |
| perform throws | `performing` | `failed` | [actions.ts:234-237](../src/actions.ts#L234-L237) |
| perform ends | `performing` | `done` | [actions.ts:232-233](../src/actions.ts#L232-L233) |
`create` ([actions.ts:119-146](../src/actions.ts#L119-L146)):
```
create(key, fields, standingApprover?):
state = mode host or standing ? performing : pending
refusal, first match:
stopping "The server is stopping."
control chat lost lostMessage
20 actions pending pendingMessage
send card over 15000 bytes cardTooLong
no approver no-owner message
refusal: throw ToolError counter unchanged
N = next; next += 1; store; log "Action N: ..."
post:
saveCounter(next)
state pending: card = postCard()
after post:
posted and performing:
stopping: state = pending
otherwise: perform
```
An action is removed from memory when its result is returned to a call ([tools.ts:437](../src/tools.ts#L437)), or at an expiry tick when it is decided, its open call count is 0 and its expiry time has passed ([actions.ts:160-161](../src/actions.ts#L160-L161)).
### 5.4 Cards
The server posts a card when a `pending` action is created, and edits it with `/_update item` at the outcome ([control.ts:124-132](../src/control.ts#L124-L132)). For an action without a card (mode `host`, standing sends) the server posts the outcome as a new message. The text per state is built by [`cardText`](../src/control.ts#L417-L436); forms are listed in [API: cards](api.md#22-cards).
Outcomes are posted after `done` and `failed` ([actions.ts:239](../src/actions.ts#L239)), after `rejected` and `expired` ([:189](../src/actions.ts#L189), [:159](../src/actions.ts#L159)), and at stop ([:206](../src/actions.ts#L206)). They are skipped after the control chat is lost ([:206](../src/actions.ts#L206), [:239](../src/actions.ts#L239), [:248](../src/actions.ts#L248)).
### 5.5 Standing approvals
| Aspect | Rule | Code |
|---|---|---|
| grant | `/approve CHAT all` from an approver: custom data of CHAT gets `simplexMcp: {approvedBy, approvedById, approvedAt}`, other keys kept; pending sends of CHAT with a card are approved | [control.ts:192-203](../src/control.ts#L192-L203) |
| removal | `/revoke CHAT`: the `simplexMcp` key is removed, other keys kept | [control.ts:205-211](../src/control.ts#L205-L211) |
| use | mode `requests`, and `approvedById` equal to the `groupMemberId` of a current approver | [control.ts:113-116](../src/control.ts#L113-L116) |
| scope | `send` only | [tools.ts:586](../src/tools.ts#L586) |
| decider shown | `safeName(approvedBy)` | [control.ts:115](../src/control.ts#L115) |
| storage | chat database; removed with the chat by `leave` | [tools.ts:646-651](../src/tools.ts#L646-L651) |
### 5.6 Expiry
| Rule | Code |
|---|---|
| `expires = created + expireHours × 3600000`, `expireHours` 1 to 23, default 23 | [actions.ts:127](../src/actions.ts#L127), [cli.ts:79](../src/cli.ts#L79) |
| check every 60 s on an unreferenced timer, and before each owner command | [actions.ts:97](../src/actions.ts#L97), [control.ts:164](../src/control.ts#L164) |
| pending and due: `expired`, log `Action N expired.`, card edited to `Expired.` | [actions.ts:156-159](../src/actions.ts#L156-L159) |
| decided, no open call, due: removed from memory | [actions.ts:160-161](../src/actions.ts#L160-L161) |
| connect wait for the other side: at most until `created + 24 h - 10 min` | [tools.ts:112](../src/tools.ts#L112), [:483](../src/tools.ts#L483) |
`Date.now()` is compared with `expires`, so due actions are expired at the first tick after a sleep of the computer.
### 5.7 Action counter
| Rule | Code |
|---|---|
| initial value: `nextAction` of the control marker, or 1 | [serve.ts:97](../src/serve.ts#L97) |
| a new action takes `next`, then `next` grows by 1 | [actions.ts:120](../src/actions.ts#L120), [:138](../src/actions.ts#L138) |
| the new `next` is written before the card is posted | [actions.ts:213-214](../src/actions.ts#L213-L214) |
| one write at a time, also after a failed write | [control.ts:118-122](../src/control.ts#L118-L122) |
| `--reset-owners` copies the value into the new control chat | [serve.ts:197-198](../src/serve.ts#L197-L198), [control.ts:387-392](../src/control.ts#L387-L392) |
| a refused call leaves the counter unchanged | [actions.ts:137](../src/actions.ts#L137) |
---
## 6. Security boundaries
### 6.1 HTTP
Checks in order of the request listener ([http.ts:18-26](../src/http.ts#L18-L26)):
| Check | Rule | Failure | Code |
|---|---|---|---|
| bind address | `127.0.0.1` only | | [http.ts:39](../src/http.ts#L39) |
| Host | hostname `localhost`, `127.0.0.1` or `[::1]`, any port | 403, JSON-RPC error | [node index.cjs:26-48](../node_modules/@modelcontextprotocol/node/dist/index.cjs#L26-L48), [server index.cjs:1520-1558](../node_modules/@modelcontextprotocol/server/dist/index.cjs#L1520-L1558) |
| Origin | absent or empty passes; otherwise the same hostnames, any port | 403, JSON-RPC error | [node index.cjs:74-96](../node_modules/@modelcontextprotocol/node/dist/index.cjs#L74-L96), [server index.cjs:1599-1644](../node_modules/@modelcontextprotocol/server/dist/index.cjs#L1599-L1644) |
| token | `Authorization` equal to `Bearer <token>`, same length, `timingSafeEqual` | 401, empty body | [http.ts:32-35](../src/http.ts#L32-L35) |
| path | pathname `/mcp` | 404, empty body | [http.ts:21](../src/http.ts#L21) |
| body size | 4 MiB | 413 | [node index.cjs:395-401](../node_modules/@modelcontextprotocol/node/dist/index.cjs#L395-L401) |
| content type of POST | `application/json` | 415 | [server index.cjs:1464-1467](../node_modules/@modelcontextprotocol/server/dist/index.cjs#L1464-L1467) |
| tool arguments | input schema, Ajv | `isError: true` result | [mcp-BumlNux0.cjs:1812-1817](../node_modules/@modelcontextprotocol/server/dist/mcp-BumlNux0.cjs#L1812-L1817) |
| tool results | output schema, Ajv | `isError: true` result | [mcp-BumlNux0.cjs:1822-1829](../node_modules/@modelcontextprotocol/server/dist/mcp-BumlNux0.cjs#L1822-L1829) |
Node limits: `requestTimeout` 0, `timeout` 0, `headersTimeout` 60 s, `keepAliveTimeout` 5 s ([http.ts:27-30](../src/http.ts#L27-L30)). When the client closes the response stream, the call is aborted: the request signal is aborted in `toNodeHandler` on `close`, the per-request transport is closed, and `ctx.mcpReq.signal` is aborted by the protocol layer ([node index.cjs:299-303](../node_modules/@modelcontextprotocol/node/dist/index.cjs#L299-L303), [server index.cjs:86-90](../node_modules/@modelcontextprotocol/server/dist/index.cjs#L86-L90), [src-BhnGYybN.cjs:6410-6427](../node_modules/@modelcontextprotocol/server/dist/src-BhnGYybN.cjs#L6410-L6427)). A result produced after the abort is discarded ([src-BhnGYybN.cjs:6536](../node_modules/@modelcontextprotocol/server/dist/src-BhnGYybN.cjs#L6536)).
### 6.2 Token and `headersHelper`
- The token is created once and kept in `DATA/token`; `--new-token` replaces it ([files.ts:30-36](../src/files.ts#L30-L36)).
- `DATA/headers.json` is rewritten at every start ([files.ts:42-44](../src/files.ts#L42-L44)).
- The Claude Code entry gets the command `cat <DATA>/headers.json`, or `type "<DATA>\headers.json"` on Windows, with the path quoted for the shell ([register.ts:47](../src/register.ts#L47), [log.ts:18-25](../src/log.ts#L18-L25)). Only this command is written to the entry and to the `claude mcp add-json` arguments ([register.test.ts:56](../test/unit/register.test.ts#L56)).
- File modes 0600 apply when a file is created; the data folder is set to 0700 at every start, except on Windows ([files.ts:12-15](../src/files.ts#L12-L15)).
### 6.3 Peer-written text
| Destination | Rule | Code |
|---|---|---|
| card and reply names | folded to one line; a valid display name is plain, others a code span | [`safeName`](../src/control.ts#L498-L501) |
| card text lines | one code span per line; backticks become `'`; a leading or trailing space becomes U+00A0; blank lines empty | [`codeLines`, `codeSpan`](../src/control.ts#L490-L496) |
| reply quote on a send card | whitespace runs to one space, first 100 UTF-16 units, one code span | [control.ts:450](../src/control.ts#L450) |
| mention names | valid name: `@name` or `@'name'`; others a code span | [`mentionText`](../src/control.ts#L503-L506) |
| errors on cards | folded to one line, one code span | [control.ts:430](../src/control.ts#L430) |
| `--owners` part of restart lines in the chat | one code span | [control.ts:65](../src/control.ts#L65), [:589-591](../src/control.ts#L589-L591) |
| terminal names and errors of actions | folded to one line | [actions.ts:141](../src/actions.ts#L141), [:285-287](../src/actions.ts#L285-L287), [control.ts:64](../src/control.ts#L64) |
| tool results | returned as data; schema descriptions and instructions mark `text`, `from`, `name`, `description`, `welcome` and `commands` with their labels and params as data, not instructions; the instructions take a chat's messages as prompts only when the user said so in the session, and limit what the agent shares about its user | [tools.ts:125-129](../src/tools.ts#L125-L129), [:146-147](../src/tools.ts#L146-L147), [:157-161](../src/tools.ts#L157-L161), [:330](../src/tools.ts#L330) |
The server writes the tappable form `/'...'` only in the card's command line, `/'approve ...'` and `/'reject ...'` ([control.ts:461-470](../src/control.ts#L461-L470)), and in `/'revoke CHAT'` of standing cards and replies ([:428](../src/control.ts#L428), [:202](../src/control.ts#L202)).
### 6.4 Chat references
| Rule | Code |
|---|---|
| `^[@#][1-9]\d{0,14}$`: 1 to 15 digits; the core reads `#(2^64 + N)` as `#N` | [messages.ts:115-117](../src/messages.ts#L115-L117), [scenarios.test.ts:320-332](../test/e2e/scenarios.test.ts#L320-L332) |
| the control chat is refused by `send`, `receive`, `leave` and `/approve CHAT all`, and left out of `list` and `/list` | [tools.ts:655](../src/tools.ts#L655), [:708-711](../src/tools.ts#L708-L711), [:715](../src/tools.ts#L715), [control.ts:223-230](../src/control.ts#L223-L230) |
| a store error of the chat read is reported as `No chat CHAT.` | [tools.ts:660](../src/tools.ts#L660), [:719](../src/tools.ts#L719) |
| the agent's own links are refused by `connect` (`ownLink` plans) | [tools.ts:830](../src/tools.ts#L830), [:848](../src/tools.ts#L848), [:870](../src/tools.ts#L870) |
### 6.5 Owner commands
| Rule | Code |
|---|---|
| source: `groupRcv` text items in the main scope of the control group | [serve.ts:128](../src/serve.ts#L128), [control.ts:136](../src/control.ts#L136) |
| form: one line `/keyword params`, leading spaces allowed | [control.ts:410-415](../src/control.ts#L410-L415) |
| the help reply is sent to any sender; other commands are accepted from approvers only | [control.ts:165-167](../src/control.ts#L165-L167) |
| commands run one at a time in arrival order | [control.ts:140-143](../src/control.ts#L140-L143) |
| `/approve` and `/reject` need a pending action with a card, and the action's chat when CHAT is given | [control.ts:181-183](../src/control.ts#L181-L183) |
### 6.6 Link and network
- The join link is a group link of the control chat with role `member`, or `observer` with `--owners`; it is printed with a QR code on stdout and deleted at the first join event ([control.ts:301-311](../src/control.ts#L301-L311), [:267](../src/control.ts#L267), [:276](../src/control.ts#L276)).
- At every start the server sets private message routing: `smp-proxy=always`, `smp-proxy-fallback=protected`, `socks=off` ([serve.ts:91](../src/serve.ts#L91)).
- With `incognito: true` the server sends `incognito=on` with the connect command ([tools.ts:516](../src/tools.ts#L516)); the core then uses a random profile for that chat ([PLAN, Participants](../PLAN.md#participants)).
---
## 7. Limits
| Limit | Value | Code |
|---|---|---|
| pending actions | 20 | [actions.ts:10](../src/actions.ts#L10) |
| expiry | 1 to 23 h, default 23 | [cli.ts:79](../src/cli.ts#L79) |
| connect wait for the other side | `created + 23 h 50 min` | [tools.ts:112](../src/tools.ts#L112) |
| send card | 15000 bytes JSON-escaped, in its largest form | [control.ts:17](../src/control.ts#L17), [:476-484](../src/control.ts#L476-L484) |
| error assumed in the largest card | 600 × `€` | [control.ts:18](../src/control.ts#L18) |
| `send.text` | 1 to 12000 characters | [tools.ts:184](../src/tools.ts#L184) |
| `connect.message` | 1 to 2000 characters | [tools.ts:172](../src/tools.ts#L172) |
| `send.mention` | 1 to 3 unique member IDs | [tools.ts:186](../src/tools.ts#L186) |
| `wait` | 0 to 86400 s | [tools.ts:173](../src/tools.ts#L173), [:187](../src/tools.ts#L187), [:199](../src/tools.ts#L199) |
| `idle` | 1 to 300 s, default 30 | [tools.ts:174](../src/tools.ts#L174), [:188](../src/tools.ts#L188), [:115](../src/tools.ts#L115) |
| `limit` | 1 to 100, default 50 | [tools.ts:189](../src/tools.ts#L189), [:200](../src/tools.ts#L200), [:116](../src/tools.ts#L116) |
| `connect` snapshot | last 50 items | [tools.ts:117](../src/tools.ts#L117) |
| collected items per action or call | 100, then counted as overflow | [messages.ts:7](../src/messages.ts#L7), [:78-79](../src/messages.ts#L78-L79) |
| unread page | 100 items | [messages.ts:6](../src/messages.ts#L6) |
| chat list page | 100 chats | [tools.ts:111](../src/tools.ts#L111) |
| error details | 500 characters | [log.ts:5](../src/log.ts#L5) |
| progress interval | 60 s | [messages.ts:4](../src/messages.ts#L4) |
| expiry tick | 60 s | [actions.ts:9](../src/actions.ts#L9) |
| delivery wait at stop | 10 s, polled every 100 ms | [serve.ts:16-17](../src/serve.ts#L16-L17) |
| HTTP body | 4 MiB | [server index.cjs:254](../node_modules/@modelcontextprotocol/server/dist/index.cjs#L254) |
| SSE keep-alive | 15 s | [mcp-BumlNux0.cjs:64](../node_modules/@modelcontextprotocol/server/dist/mcp-BumlNux0.cjs#L64) |
| agent NAME | 1 to 32 characters | [cli.ts:143](../src/cli.ts#L143) |
| display name | 50 characters, `mkValidName` rules | [messages.ts:8-9](../src/messages.ts#L8-L9), [:236-258](../src/messages.ts#L236-L258) |
| port | 1 to 65535, default 5226 | [cli.ts:88](../src/cli.ts#L88), [:59](../src/cli.ts#L59) |
+543
View File
@@ -0,0 +1,543 @@
# SimpleX MCP server: flows
> Sequence diagrams of the main flows, each with notes and source locations.
>
> Related specs: [README](README.md) | [Architecture](architecture.md) | [API](api.md)
Participants:
| Letter | Participant |
|---|---|
| P | the person at the terminal |
| A | the agent in Claude Code |
| H | the HTTP listener with the MCP SDK |
| T | `AgentTools` |
| X | `Actions` |
| K | `Control` |
| C | the SimpleX core behind `ChatApi` |
| O | the owner in the SimpleX app |
| D | a peer |
`#G` is the agent control chat, `USER` the agent's user ID, `LINK` the link as `connFullLink [connShortLink]`. Chat commands are shown as the server sends them. Card and progress texts with a semicolon are shown with a comma in the diagrams; the exact texts are in the notes.
---
## Table of contents
1. [First start and the owner joins](#1-first-start-and-the-owner-joins)
2. [`register`](#2-register)
3. [`connect` with approval](#3-connect-with-approval)
4. [`send` with responses](#4-send-with-responses)
5. [`receive` with `wait`](#5-receive-with-wait)
6. [`send` under a standing approval, and `/revoke`](#6-send-under-a-standing-approval-and-revoke)
7. [`leave`](#7-leave)
8. [Mode `host`](#8-mode-host)
9. [Expiry](#9-expiry)
10. [A lost call and its re-attachment](#10-a-lost-call-and-its-re-attachment)
11. [Shutdown with pending actions](#11-shutdown-with-pending-actions)
12. [`--owners` setup](#12---owners-setup)
---
## 1. First start and the owner joins
```mermaid
sequenceDiagram
participant P as Person
participant S as serve.ts
participant C as Core
participant K as Control
participant O as Owner
P->>S: simplex-mcp serve my-agent --display-name "My Agent"
S->>S: DATA mode 0700, lock, token, headers.json
S->>C: ChatApi.init DATA/simplex
S->>P: version line, data folder, approval mode
S->>C: /user (no active user)
S->>C: /_create user {"profile":{"displayName":"My Agent","fullName":"","peerType":"bot"},...}
S->>S: server.json
S->>C: /network socks=off smp-proxy=always smp-proxy-fallback=protected
S->>C: startChat, events buffered
S->>C: /_groups USER (no control marker)
S->>C: /_group USER {My Agent, history on, menu pending and list}
S->>C: /_set custom #G {"simplexMcp":{"control":true,"nextAction":1}}
S->>P: Agent control chat "My Agent" created.
S->>K: load
K->>C: /_members #G
S->>S: HTTP listens on 127.0.0.1:5226
K->>C: /_delete link #G (groupLinkNotFound ignored)
K->>C: /_create link #G member
K->>P: join line, QR code, link, QR hint, owner line
S->>P: Listening on http://127.0.0.1:5226/mcp
O->>C: joins by the link
C->>S: joinedGroupMember
S->>K: onEvent, queued
K->>C: /_members #G
K->>C: /_delete link #G
K->>C: /_member role #G 3 owner
K->>C: /_members #G
K->>O: I'm your agent. I ask here before I connect or send.
K->>P: Alice joined (member 3) and is now an owner. The link is deleted.
K->>P: Add the agent to Claude Code: npx -y @simplex-chat/mcp register my-agent
```
Notes:
- At first use, libsimplex and the Node.js add-on are downloaded, with one `Downloading ...` line each on stderr ([libs.ts:112-133](../../simplex-chat-nodejs/src/libs.ts#L112-L133)).
- With `--data`, `--data <folder>` is appended to the register hint, shell-quoted ([serve.ts:164-166](../src/serve.ts#L164-L166)).
- With `--qr-code small`, the line `If the QR code does not scan, ...` is printed after the link ([control.ts:307](../src/control.ts#L307)).
- The server runs `ownerJoined` for every `joinedGroupMember` event of a human, and the link is deleted in that handler ([control.ts:152-153](../src/control.ts#L152-L153), [:266-273](../src/control.ts#L266-L273)).
Source: [cli.ts:191-200](../src/cli.ts#L191-L200), [serve.ts:64-123](../src/serve.ts#L64-L123), [files.ts:12-44](../src/files.ts#L12-L44), [control.ts:374-385](../src/control.ts#L374-L385), [:88-103](../src/control.ts#L88-L103), [:236-240](../src/control.ts#L236-L240), [:301-311](../src/control.ts#L301-L311), [:146-155](../src/control.ts#L146-L155).
---
## 2. `register`
```mermaid
sequenceDiagram
participant P as Person
participant R as register.ts
participant L as claude CLI
participant CC as Claude Code
participant H as HTTP
P->>R: simplex-mcp register my-agent [--scope local] [--data DIR]
R->>R: read DATA/server.json
alt server.json missing
R->>P: Agent "my-agent" was never started. Start it with: ... (exit 1)
end
R->>L: claude mcp remove --scope user simplex-my-agent
R->>L: claude mcp add-json --scope user simplex-my-agent {"type":"http","url":"http://127.0.0.1:5226/mcp","headersHelper":"cat DATA/headers.json","timeout":169800000}
alt claude not found
R->>P: Add this entry under "mcpServers" in ~/.claude.json: {...} (exit 1)
else add-json exit status other than 0
R->>P: claude mcp add-json failed: STDERR (exit 1)
end
R->>P: Added to Claude Code: scope user, server simplex-my-agent.
CC->>CC: headersHelper: cat DATA/headers.json
CC->>H: POST /mcp with Authorization: Bearer TOKEN
```
Notes:
- `simplex-mcp` ignores the exit status of `claude mcp remove` and stops at that step only when the `claude` binary is missing ([register.ts:35-36](../src/register.ts#L35-L36)).
- `timeout` is `(expireHours + 24) × 3600000 + 600000` ms from `server.json` ([register.ts:48](../src/register.ts#L48)).
- With `--scope local`, the project folder in the manual entry is the git repository root, the main repository of a worktree, or the current folder outside a repository ([register.ts:53](../src/register.ts#L53), [:57-81](../src/register.ts#L57-L81)).
Source: [cli.ts:103-118](../src/cli.ts#L103-L118), [:200](../src/cli.ts#L200), [register.ts:30-87](../src/register.ts#L30-L87), [files.ts:50-53](../src/files.ts#L50-L53).
---
## 3. `connect` with approval
```mermaid
sequenceDiagram
participant A as Agent
participant H as HTTP and SDK
participant T as AgentTools
participant X as Actions
participant K as Control
participant C as Core
participant O as Owner
participant D as Peer
A->>H: tools/call connect {target: "@directory.simplex", wait, idle}
H->>T: validated input, abort signal, progress token
T->>X: find(key): none
T->>C: /_connect plan USER @directory.simplex
C-->>T: connectionPlan: contactAddress ok, planSimplexName
T->>T: classifyPlan: prepareContact with domain
T->>X: create(key, connect fields)
X->>K: refusal: none
X->>K: saveCounter: /_set custom #G
X->>K: postCard
K->>O: I want to connect to bot X (@directory.simplex), as myself. /'approve N' or /'reject N'
T->>X: decided(action, call)
X-->>A: progress: Action N waits for an approver until TIME.
O->>C: /approve N
C->>K: newChatItems
K->>X: approve(action, "Alice")
X->>T: performConnect
T->>C: /_connect plan USER @directory.simplex
T->>C: /_prepare contact USER LINK domain=directory.simplex {link data}
T->>C: /_connect contact @C incognito=off
C-->>T: startedConnectionToContact
D->>C: accepts the request
C->>T: contactConnected: markConnected, collect @C
T-->>X: perform ends
X->>K: outcome
K->>O: card edited: I connected to bot X (@directory.simplex), as myself. Approved by Alice.
T->>C: /_get chat @C count=50
T->>T: wait until idle or startedAt + wait
T->>C: /_read chat items @C, new item IDs
T->>X: remove(action)
T-->>A: {action: N, status: done, chat: @C, connected: true, messages}
```
Notes:
- The plan is read again in the perform step ([tools.ts:484](../src/tools.ts#L484)).
- Join commands per plan ([tools.ts:514-542](../src/tools.ts#L514-L542)):
| Plan | Commands |
|---|---|
| known prepared contact or group | `/_connect contact @C incognito=X [json M]`<br>`/_connect group #G incognito=X [json M]` |
| channel link (`direct` false, with link data) | `/_prepare group USER LINK direct=off [domain=D] JSON`<br>`/_connect group #G incognito=X` |
| contact address with link data, and a SimpleX name or a message | `/_prepare contact USER LINK [domain=D] JSON`<br>`/_connect contact @C incognito=X [json M]`, or `/_connect group #G ...` for a business |
| `contactViaAddress` | `/_connect contact USER incognito=X CONTACT_ID` |
| other links | `/_connect USER incognito=X LINK` |
- After a failed prepared connect, the server deletes the prepared chat with `notify=off` ([tools.ts:544-551](../src/tools.ts#L544-L551)).
- The connection is matched by `contactConnected` or `userJoinedGroup`: by contact or group ID, or by the pending connection ID ([tools.ts:387-395](../src/tools.ts#L387-L395), [:557-562](../src/tools.ts#L557-L562)).
- In the perform step the server waits for the other side until `startedAt + wait`, at most until `created + 23 h 50 min`, and then reports `connected: false` ([tools.ts:483](../src/tools.ts#L483), [:507-511](../src/tools.ts#L507-L511)).
- Without `message`, the idle time is counted from the connection; with `message`, from the first reply ([tools.ts:568](../src/tools.ts#L568), [messages.ts:225](../src/messages.ts#L225)).
- For a target the agent is connected to, the result is `status: known` with its chat fields, and no action is created ([tools.ts:417](../src/tools.ts#L417)).
- Exact progress texts: `Action N waits for an approver until TIME.`, then `Action N waits for the connection until TIME.`, then `K messages in CHAT; waiting until TIME.` ([actions.ts:296-299](../src/actions.ts#L296-L299), [tools.ts:452](../src/tools.ts#L452)).
Source: [tools.ts:412-464](../src/tools.ts#L412-L464), [:466-571](../src/tools.ts#L466-L571), [:811-874](../src/tools.ts#L811-L874), [actions.ts:119-183](../src/actions.ts#L119-L183), [control.ts:134-190](../src/control.ts#L134-L190).
---
## 4. `send` with responses
```mermaid
sequenceDiagram
participant A as Agent
participant T as AgentTools
participant X as Actions
participant K as Control
participant C as Core
participant O as Owner
participant D as Peer
A->>T: send {chat: #15, text, reply_to, mention: [44], wait, idle, limit}
T->>C: /_get chat #15 count=0
T->>C: /_get chat #15 around=REPLY_TO count=0
T->>C: /_members #15
T->>K: standingApprover(chatInfo): none
T->>X: create(key, send fields)
X->>K: card size check and approvers
K->>O: I want to send a message to group X: `text` replying to ... mentioning @Switchback /'approve #15 N', /'approve #15 all' or /'reject #15 N'
O->>C: /approve #15 N
K->>X: approve(action, "Alice")
X->>T: performSend
T->>T: collect #15 items with ID above the sent item
T->>C: /_send #15 json [{msgContent, mentions, quotedItemId}]
C-->>T: sent item S
D->>C: replies S+1, S+2
C->>T: newChatItems: collected
X->>K: outcome
K->>O: card edited: I sent a message to group X: `text` ... Approved by Alice.
T->>T: wait until idle seconds after the last reply, or sentAt + wait
T->>C: /_read chat items #15 S+1,S+2
T->>X: remove(action)
T-->>A: {action: N, status: done, chat: #15, item: S, responses, more: 0}
```
Notes:
- Checks before the action: chat reference, role `observer` or `relay` refused, `reply_to` item present, mentioned members present and the chat a group ([tools.ts:573-581](../src/tools.ts#L573-L581)).
- The server starts collecting before the send and, after it, drops items with IDs up to the sent item ([tools.ts:612](../src/tools.ts#L612), [:622-624](../src/tools.ts#L622-L624)).
- Mentioned names missing from the text are prefixed as `@name` ([tools.ts:614-615](../src/tools.ts#L614-L615)).
- While a collected message is live, the server waits for its final update; live items are left out of `responses` and counted in `more` ([messages.ts:224-225](../src/messages.ts#L224-L225), [tools.ts:599](../src/tools.ts#L599), [:605](../src/tools.ts#L605)).
- At most 100 replies are kept per action; later ones are counted as overflow ([messages.ts:78-79](../src/messages.ts#L78-L79)).
Source: [tools.ts:573-629](../src/tools.ts#L573-L629), [messages.ts:71-83](../src/messages.ts#L71-L83), [:222-230](../src/messages.ts#L222-L230), [control.ts:448-453](../src/control.ts#L448-L453).
---
## 5. `receive` with `wait`
```mermaid
sequenceDiagram
participant A as Agent
participant T as AgentTools
participant W as ChatItemsWatch
participant C as Core
participant D as Peer
A->>T: receive {chat: #12, wait: 3600, limit: 50}
T->>W: collect #12 into arrived
T->>C: /_get chat #12 initial=1
C-->>T: chatStats.unreadCount 0
T-->>A: progress: No new messages in #12, waiting until TIME.
D->>C: post, item 40
C->>W: newChatItems: arrived
T->>C: /_read chat items #12 40
T->>W: stop collecting
T-->>A: {chat: #12, messages: [40], more: 0}
```
Notes:
- With unread items the server reads `around=<minUnreadItemId> count=0`, then pages of `after=<last> count=100` until it holds `limit + 1` unread, non-live messages or a page is short ([messages.ts:183-194](../src/messages.ts#L183-L194)).
- Unread items other than messages in the read pages are marked read with the returned messages ([tools.ts:663](../src/tools.ts#L663), [:680](../src/tools.ts#L680)).
- `more`: the arrived messages not returned, the overflow and the unread items not read ([tools.ts:682](../src/tools.ts#L682)).
- Items received while the pages are read are returned once ([tools.ts:662-665](../src/tools.ts#L662-L665)).
- Exact progress text: `No new messages in CHAT; waiting until TIME.` ([tools.ts:669](../src/tools.ts#L669)).
Source: [tools.ts:653-686](../src/tools.ts#L653-L686), [messages.ts:183-220](../src/messages.ts#L183-L220).
---
## 6. `send` under a standing approval, and `/revoke`
```mermaid
sequenceDiagram
participant A as Agent
participant T as AgentTools
participant X as Actions
participant K as Control
participant C as Core
participant O as Owner
O->>C: /approve #9 all (on a pending send card)
C->>K: newChatItems
K->>C: /_get chat #9 count=0
K->>C: /_set custom #9 {"simplexMcp":{"approvedBy":"Alice","approvedById":3,"approvedAt":"..."}}
K->>X: approve each pending send to #9 with a card
K->>O: Approved for business X (#9) by Alice. /'revoke #9'
A->>T: send {chat: #9, text}
T->>C: /_get chat #9 count=0
T->>K: standingApprover: Alice
T->>X: create(key, fields, "Alice"): performing, standing
X->>K: saveCounter (no card)
X->>T: performSend
T->>C: /_send #9 json [...]
X->>K: outcome
K->>O: I sent a message to business X: `text` Approved for this chat by Alice. /'revoke #9'
T-->>A: {action, status: done, chat: #9, item, responses, more}
O->>C: /revoke #9
K->>C: /_get chat #9 count=0
K->>C: /_set custom #9 {other keys}
K->>O: Messages to business X (#9) need approval again.
```
Notes:
- The approval is used in mode `requests` while member `approvedById` is an approver ([control.ts:113-116](../src/control.ts#L113-L116)).
- For sends that were pending at `/approve CHAT all`, the card ends with the plain `Approved by Alice.` ([control.ts:199-201](../src/control.ts#L199-L201), [:419](../src/control.ts#L419)).
- `connect` and `leave` actions are created without the standing approver, so they stay pending ([tools.ts:423](../src/tools.ts#L423), [:638](../src/tools.ts#L638), [commands.test.ts:360-369](../test/unit/commands.test.ts#L360-L369)).
Source: [control.ts:192-211](../src/control.ts#L192-L211), [tools.ts:586](../src/tools.ts#L586), [actions.ts:119-146](../src/actions.ts#L119-L146), [:223-240](../src/actions.ts#L223-L240).
---
## 7. `leave`
```mermaid
sequenceDiagram
participant A as Agent
participant T as AgentTools
participant X as Actions
participant K as Control
participant C as Core
participant O as Owner
A->>T: leave {chat: #9}
T->>X: find(key): none
T->>C: /_get chat #9 count=0
T->>X: create(key, leave fields)
K->>O: I want to leave business X (#9). /'approve #9 N' or /'reject #9 N'
O->>C: /approve #9 N
K->>X: approve(action, "Alice")
X->>T: performLeave
T->>C: /_get chat #9 count=0
T->>C: /_leave #9 (membership current or pending)
T->>C: /_delete #9 full
X->>K: outcome
K->>O: card edited: I left business X (#9). Approved by Alice.
T-->>A: {action: N, status: done, chat: #9}
```
Notes:
- For a contact, the card text is `I want to delete KIND NAME (@N).`, and the perform step sends `/_delete @N full` ([actions.ts:280-282](../src/actions.ts#L280-L282), [tools.ts:647](../src/tools.ts#L647)).
- The delete mode is `full` without `notify=off`, so the core notifies the contact or the members ([types.js:76-81](../node_modules/@simplex-chat/types/dist/types.js#L76-L81)).
- The server looks for an attached action before it reads the chat, so a repeated call after the deletion is attached to the done action ([tools.ts:633-636](../src/tools.ts#L633-L636), [messages.test.ts:745-759](../test/unit/messages.test.ts#L745-L759)).
Source: [tools.ts:631-651](../src/tools.ts#L631-L651), [control.ts:454-457](../src/control.ts#L454-L457).
---
## 8. Mode `host`
```mermaid
sequenceDiagram
participant P as Person
participant A as Agent
participant H as HTTP and SDK
participant T as AgentTools
participant X as Actions
participant K as Control
participant O as Owner
Note over P,H: the person started serve my-agent --approve host
A->>H: tools/list
H-->>A: connect, send, leave with _meta {"anthropic/requiresUserInteraction": true}
A->>A: Claude Code asks its user
A->>T: send {chat: @4, text}
T->>X: create: performing
X->>K: saveCounter (no card)
X->>T: performSend
X->>P: Action N: performed without approval (mode host).
X->>K: outcome
K->>O: I sent a message to bot X: `text` Performed without approval (mode host).
T-->>A: {action: N, status: done, ...}
```
Notes:
- New actions are still refused unless the control chat has an approver ([control.ts:107-110](../src/control.ts#L107-L110)).
- In mode `host`, `undefined` is returned by `standingApprover`, so standing approvals are ignored by `send`, `list` and `/list` ([control.ts:115](../src/control.ts#L115)).
- `_meta` is set on `connect`, `send` and `leave` only ([tools.ts:361](../src/tools.ts#L361)).
Source: [tools.ts:355-365](../src/tools.ts#L355-L365), [actions.ts:130](../src/actions.ts#L130), [:238](../src/actions.ts#L238), [control.ts:419](../src/control.ts#L419).
---
## 9. Expiry
```mermaid
sequenceDiagram
participant A as Agent
participant X as Actions
participant K as Control
participant O as Owner
A->>X: send call creates pending action N at T
K->>O: card N
Note over X: expires = T + expireHours
loop every 60 s, and before each owner command
X->>X: expireDue
end
X->>X: pending and due: expired
X->>K: outcome
K->>O: card edited: ... Expired.
X-->>A: {action: N, status: expired}
X->>X: removed with the result
O->>K: /approve N
K->>O: Action N is not pending.
```
Notes:
- A decided action with an open count of 0 is removed at the first tick after its expiry time ([actions.ts:160-161](../src/actions.ts#L160-L161)).
- Expiry times are listed in the `/pending` reply ([control.ts:472-474](../src/control.ts#L472-L474)).
- The server ends the connect wait at `created + 23 h 50 min`, within the 24 hours in which a message can be edited ([tools.ts:112](../src/tools.ts#L112), [:483](../src/tools.ts#L483)).
Source: [actions.ts:97](../src/actions.ts#L97), [:153-164](../src/actions.ts#L153-L164), [control.ts:164](../src/control.ts#L164), [:433-434](../src/control.ts#L433-L434).
---
## 10. A lost call and its re-attachment
```mermaid
sequenceDiagram
participant A as Agent
participant H as HTTP and SDK
participant T as AgentTools
participant X as Actions
participant O as Owner
A->>H: send {chat: @4, text: "t"}
H->>T: call
T->>X: create: pending, open 1
A->>H: closes the response stream
H->>T: ctx.mcpReq.signal aborted
T->>X: decided: false, open 0
Note over H: result discarded by the SDK
A->>T: list
T-->>A: actions: [{action: N, tool: send, input, state: pending, open: false}]
O->>X: /approve N
X->>X: perform, done, replies collected
A->>T: send {chat: @4, text: "t"}
T->>X: find(key): action N
T->>T: doneAtCall: wait counted from now
T->>X: remove(action)
T-->>A: {action: N, status: done, item, responses, more}
```
Notes:
- The key is the tool name with the action fields: `target`, `incognito`, `message` for `connect`; `chat`, `text`, `reply_to`, `mention` for `send`; `chat` for `leave` ([tools.ts:413](../src/tools.ts#L413), [:582](../src/tools.ts#L582), [:633](../src/tools.ts#L633), [:1003-1005](../src/tools.ts#L1003-L1005)).
- The `wait`, `idle` and `limit` of the attaching call are used ([tools.ts:593](../src/tools.ts#L593)).
- For a call aborted before `create`, the server returns before it creates an action ([tools.ts:422](../src/tools.ts#L422), [:585](../src/tools.ts#L585), [:637](../src/tools.ts#L637)).
- Abort path in the SDK: on `res` close the web request is aborted ([node index.cjs:299-303](../node_modules/@modelcontextprotocol/node/dist/index.cjs#L299-L303)); the per-request transport is closed ([server index.cjs:86-90](../node_modules/@modelcontextprotocol/server/dist/index.cjs#L86-L90)); the handler signal is aborted ([src-BhnGYybN.cjs:6425](../node_modules/@modelcontextprotocol/server/dist/src-BhnGYybN.cjs#L6425)).
Source: [tools.ts:431-442](../src/tools.ts#L431-L442), [:590-608](../src/tools.ts#L590-L608), [:694](../src/tools.ts#L694), [actions.ts:166-177](../src/actions.ts#L166-L177), [messages.ts:206-220](../src/messages.ts#L206-L220).
---
## 11. Shutdown with pending actions
```mermaid
sequenceDiagram
participant P as Person
participant S as serve.ts
participant X as Actions
participant K as Control
participant H as HTTP
participant C as Core
participant O as Owner
P->>S: Ctrl-C (SIGINT)
S->>S: stopped: events ignored
par actions
S->>X: stop()
X->>X: stopping, timer cleared, cards being posted awaited
X->>X: pending and performing: interrupted
X->>K: outcome of each
K->>O: pending: ... Cancelled: the server stopped.
K->>O: performing: ... Approved by Alice, interrupted: the server stopped.
and HTTP
S->>H: close, closeAllConnections (open calls aborted)
end
S->>P: Cancelled 2 actions.
S->>C: /sql chat: agent connection IDs of #G members
S->>C: /sql agent: count of pending deliveries, every 100 ms, at most 10 s
S->>C: /_stop, close the store
S->>S: remove DATA/lock
S->>P: Stopped.
Note over P,O: next start: counter continues, /approve of an old number: Action N is not pending.
```
Notes:
- The interrupted card line is `DECIDER; interrupted: the server stopped.` ([control.ts:426](../src/control.ts#L426)).
- A host-mode action whose counter is being written at the stop is set to `pending` and cancelled without a perform ([actions.ts:226](../src/actions.ts#L226), [actions.test.ts:380-391](../test/unit/actions.test.ts#L380-L391)).
- After a crash, the cards of that run are left with their commands; `/approve` of their numbers gets the reply `Action N is not pending.` ([control.ts:182](../src/control.ts#L182)).
Source: [serve.ts:48-62](../src/serve.ts#L48-L62), [:150-176](../src/serve.ts#L150-L176), [actions.ts:200-209](../src/actions.ts#L200-L209), [http.ts:46-50](../src/http.ts#L46-L50), [control.ts:421-426](../src/control.ts#L421-L426).
---
## 12. `--owners` setup
```mermaid
sequenceDiagram
participant P as Person
participant K as Control
participant C as Core
participant O as Owner Alice
participant B as Member Bob
P->>K: serve my-agent --owners
K->>C: /_create link #G observer
K->>P: The link adds an observer. Its ID and name are printed when it joins.
O->>C: joins by the link
C->>K: joinedGroupMember
K->>C: /_delete link #G
K->>P: Alice joined (member 3). Restart the server with --owners 3:Alice
K->>O: Alice joined (member 3). Restart the server with `--owners 3:Alice`
P->>K: Ctrl-C, then serve my-agent --owners 3:Alice
K->>C: /_member role #G 3 owner
K->>C: /_remove #G ID, for each other human
K->>C: /_delete link #G
K->>P: Owners: 3:Alice.
K->>P: Add the agent to Claude Code: ...
O->>C: adds Bob, makes Bob an owner
C->>K: memberRole
K->>P: Bob (member 7) is now an owner and can't approve actions yet. Restart ... --owners 3:Alice,7:Bob
K->>O: same line, the --owners part as code
B->>K: /approve N
K->>B: Only the owners listed in --owners approve actions.
```
Notes:
- Until the restart with a list, new actions are refused: `The agent has no approving owner. ...` ([control.ts:108-110](../src/control.ts#L108-L110)).
- An entry must match a human by member ID and display name, or the server exits with code 2 ([control.ts:249-253](../src/control.ts#L249-L253)).
- With `--reset-owners`, the server deletes the control chat for all members and creates a new one with the same counter; the CLI refuses the option with an `--owners` list ([control.ts:387-392](../src/control.ts#L387-L392), [cli.ts:97-99](../src/cli.ts#L97-L99)).
- When a listed owner leaves and none remains: `... is no longer an owner. No owners remain: restart the server with --reset-owners --owners` ([control.ts:288-293](../src/control.ts#L288-L293)).
Source: [control.ts:242-264](../src/control.ts#L242-L264), [:275-294](../src/control.ts#L275-L294), [:357-361](../src/control.ts#L357-L361), [:565-591](../src/control.ts#L565-L591).
+304
View File
@@ -0,0 +1,304 @@
import {errorText, log, logError, oneLine} from "./log"
import {Changes, Collected, ToolCall, nextChange, reportProgress, utcMinutes} from "./messages"
import type {ConnectInput, LeaveInput, PlanSummary, SendInput} from "./tools"
export type Mode = "requests" | "host"
export type State = "pending" | "performing" | "done" | "failed" | "rejected" | "expired"
const progressTick = 60_000
const expiryTick = 60_000
const maxPending = 20
export const lostMessage = "The agent lost its control chat. The person who runs it has to restart it with --reset-owners."
export const stoppingMessage = "The server is stopping."
const pendingMessage = `${maxPending} actions are pending; wait for the owner's decisions.`
export class ToolError extends Error {}
export interface Subject {
kind: string
name?: string
simplexName?: string
incognito: boolean
}
export interface ConnRef {
pcc?: number
contact?: number
group?: number
}
interface ActionRecord {
readonly n: number
readonly key: string
readonly created: number
readonly expires: number
readonly changes: Changes
readonly standing: boolean
state: State
card?: number
decidedBy?: string
open: number
interrupted: boolean
error?: string
stopCollecting?: () => void
}
export interface ConnectAction extends ActionRecord {
readonly tool: "connect"
readonly input: ConnectInput
readonly subject: Subject
readonly plan: PlanSummary
readonly messages: Collected
conn?: ConnRef
chat?: string
knownAs?: string
connected?: boolean
connectedAt?: number
startedAt?: number
connectUntil?: number
}
export interface SendAction extends ActionRecord {
readonly tool: "send"
readonly input: SendInput
readonly subject: Subject
readonly chat: string
readonly replyTo?: {from: string; text: string}
readonly mentions: {name: string; member: number}[]
readonly responses: Collected
item?: number
sentAt?: number
}
export interface LeaveAction extends ActionRecord {
readonly tool: "leave"
readonly input: LeaveInput
readonly subject: Subject
readonly chat: string
}
export type Action = ConnectAction | SendAction | LeaveAction
export type NewAction = Omit<ConnectAction, keyof ActionRecord> | Omit<SendAction, keyof ActionRecord> | Omit<LeaveAction, keyof ActionRecord>
export interface ActionHooks {
refusal(action: Action): string | undefined
saveCounter(next: number): Promise<void>
postCard(action: Action): Promise<number>
perform(action: Action): Promise<void>
outcome(action: Action): Promise<void>
}
export class Actions {
lost = false
private stopping = false
private readonly actions = new Map<number, Action>()
private readonly expiry = setInterval(() => this.expireDue(), expiryTick).unref()
private readonly posting = new Map<number, Promise<boolean>>()
private readonly outcomes = new Set<Promise<void>>()
constructor(readonly mode: Mode, readonly expireHours: number, public next: number, private readonly hooks: ActionHooks) {}
get(n: number): Action | undefined {
return this.actions.get(n)
}
all(): Action[] {
return [...this.actions.values()]
}
pending(): Action[] {
return this.all().filter(({state}) => state === "pending")
}
find(key: string): Action | undefined {
return this.all().find(action => action.key === key)
}
create(key: string, fields: NewAction, approvedBy?: string): Action {
const n = this.next
const created = Date.now()
const standing = approvedBy !== undefined
const record: ActionRecord = {
n,
key,
created,
expires: created + this.expireHours * 3_600_000,
changes: new Changes(),
standing,
state: this.mode === "host" || standing ? "performing" : "pending",
decidedBy: approvedBy,
open: 0,
interrupted: false,
}
const action = {...fields, ...record} as Action
const refusal = this.stopping ? stoppingMessage : this.lost ? lostMessage : this.pending().length >= maxPending ? pendingMessage : this.hooks.refusal(action)
if (refusal !== undefined) throw new ToolError(refusal)
this.next++
this.actions.set(n, action)
const {what, detail} = summary(action, oneLine)
log(`Action ${n}: ${what}${detail}`)
const posting = this.post(action)
this.posting.set(n, posting)
posting.then(posted => this.afterPosting(action, posted)).catch(error => logError(`action ${n}`, error))
return action
}
remove(action: Action): void {
this.actions.delete(action.n)
action.stopCollecting?.()
}
expireDue(): void {
const now = Date.now()
for (const action of this.all().filter(({expires}) => expires <= now)) {
if (action.state === "pending") {
this.decide(action, "expired")
log(`Action ${action.n} expired.`)
this.updateCard(action)
} else if (isDecided(action.state) && action.open === 0) {
this.remove(action)
}
}
}
async decided(action: Action, call: ToolCall): Promise<boolean> {
if (isDecided(action.state)) return true
const stopProgress = reportProgress(call, () => progressText(action))
try {
while (!isDecided(action.state)) {
if (!(await nextChange(action.changes, call.signal, progressTick))) return false
}
return true
} finally {
stopProgress()
}
}
approve(action: Action, approver: string): void {
action.decidedBy = approver
this.decide(action, "performing")
this.runPerform(action).catch(error => logError(`action ${action.n}`, error))
}
reject(action: Action, approver: string): void {
action.decidedBy = approver
this.decide(action, "rejected")
log(`Action ${action.n} rejected by ${approver}.`)
this.updateCard(action)
}
loseControl(): void {
this.lost = true
for (const action of this.pending()) {
action.error = lostMessage
this.decide(action, "failed")
}
}
async stop(): Promise<number> {
this.stopping = true
clearInterval(this.expiry)
await Promise.all(this.posting.values())
const stopped = this.all().filter(({state}) => state === "pending" || state === "performing")
for (const action of stopped) action.interrupted = true
if (!this.lost) await Promise.all(stopped.map(action => this.hooks.outcome(action).catch(error => logError(`action ${action.n} card`, error))))
await Promise.allSettled(this.outcomes)
return stopped.length
}
private async post(action: Action): Promise<boolean> {
try {
await this.hooks.saveCounter(this.next)
if (action.state === "pending") action.card = await this.hooks.postCard(action)
return true
} catch (error) {
action.error = errorText(error)
this.decide(action, "failed")
return false
}
}
private async afterPosting(action: Action, posted: boolean): Promise<void> {
this.posting.delete(action.n)
if (!posted || action.state !== "performing") return
if (this.stopping) action.state = "pending"
else await this.runPerform(action)
}
private async runPerform(action: Action): Promise<void> {
try {
await this.hooks.perform(action)
this.decide(action, "done")
} catch (error) {
action.error = errorText(error)
this.decide(action, "failed")
}
log(`Action ${action.n}${this.mode === "host" ? ":" : ` approved by ${action.decidedBy}:`} ${performedText(action, this.mode)}`)
if (!action.interrupted && !this.lost) await this.postOutcome(action)
}
private decide(action: Action, state: State): void {
action.state = state
action.changes.notify()
}
private updateCard(action: Action): void {
if (this.lost) return
this.postOutcome(action).catch(error => logError(`action ${action.n} card`, error))
}
private postOutcome(action: Action): Promise<void> {
const outcome = this.hooks.outcome(action)
this.outcomes.add(outcome)
return outcome.finally(() => this.outcomes.delete(outcome))
}
}
export function isDecided(state: State): boolean {
return state === "done" || state === "failed" || state === "rejected" || state === "expired"
}
export function subjectText({kind, name, simplexName}: Subject, showName: (name: string) => string): string {
const named = name === undefined ? `an unnamed ${kind}` : `${kind} ${showName(name)}`
return simplexName === undefined ? named : `${named} (${simplexName})`
}
export function summary(action: Action, showName: (name: string) => string): {what: string; detail: string} {
const target = subjectText({...action.subject, simplexName: undefined}, showName)
switch (action.tool) {
case "connect":
return {what: `connect to ${target}`, detail: `, ${action.subject.incognito ? "incognito" : "as itself"}`}
case "send":
return {what: `send to ${target}`, detail: ` (${action.chat})`}
case "leave":
return {what: `${leaveVerbs(action).present} ${target}`, detail: ` (${action.chat})`}
}
}
export function leaveVerbs({chat}: LeaveAction): {present: string; past: string} {
return chat.startsWith("@") ? {present: "delete", past: "deleted"} : {present: "leave", past: "left"}
}
function performedText(action: Action, mode: Mode): string {
const failure = action.state === "failed" ? `; failed: ${oneLine(action.error ?? "")}` : ""
if (mode === "host") return `performed without approval (mode host)${failure}.`
if (failure !== "") return `failed: ${oneLine(action.error ?? "")}.`
if (action.tool === "send") return "sent."
if (action.tool === "leave") return `${leaveVerbs(action).past}.`
return action.connected ? "connected." : "connection requested."
}
function progressText(action: Action): string {
switch (action.state) {
case "pending":
return `Action ${action.n} waits for an approver until ${utcMinutes(action.expires)}.`
case "performing":
return action.tool === "connect" && action.connectUntil !== undefined
? `Action ${action.n} waits for the connection until ${utcMinutes(action.connectUntil)}.`
: `Action ${action.n} is being performed.`
default:
return `Action ${action.n} is ${action.state}.`
}
}
+213
View File
@@ -0,0 +1,213 @@
#!/usr/bin/env node
import {homedir} from "os"
import {join, resolve} from "path"
import {Mode} from "./actions"
import {ExitError, log, logError} from "./log"
import {validName} from "./messages"
import {RegisterOptions, Scope, register} from "./register"
import {OwnerEntry, QrCodeSize, ServeOptions, serve} from "./serve"
export const helpText = `Usage:
simplex-mcp serve NAME [options] run agent NAME
simplex-mcp register NAME [options] add agent NAME to Claude Code
serve options:
--data DIR data folder (default: ~/.simplex-mcp/NAME)
--display-name TEXT the agent's name in SimpleX (default at the first start: NAME)
--approve MODE what the owner approves in the agent control chat:
requests the agent's connections and messages (default)
host nothing; the MCP host asks
--owners [ID:NAME,...] only these owners approve; without a list, the link
adds an observer and its ID is printed at the join.
Without --owners, whoever joins becomes an owner and
every owner approves.
--expire HOURS pending actions expire after HOURS, 1 to 23 (default: 23)
--reset-owners replace the owners: a new agent control chat and link
--new-token replace the access token
--port N HTTP port on 127.0.0.1 (default: 5226)
--qr-code SIZE QR code of the link: small (default) or large
register options:
--scope SCOPE user (default) or local
--data DIR data folder (default: ~/.simplex-mcp/NAME)
--reset-owners takes --owners without a list.
`
export type Command =
| {command: "help"}
| {command: "serve"; options: ServeOptions}
| {command: "register"; options: RegisterOptions}
export function parseCommand(args: string[], home: string = homedir()): Command {
const [command, name, ...rest] = args
if (command === "--help") return {command: "help"}
if (command !== "serve" && command !== "register") throw usageError("Usage: simplex-mcp serve NAME [options] or simplex-mcp register NAME [options]. Run simplex-mcp --help.")
if (name === undefined || !validAgentName(name)) throw usageError("NAME: 1 to 32 lowercase letters, digits and single hyphens, starting with a letter or digit.")
const options = new OptionReader(rest)
return command === "serve" ? {command, options: serveOptions(name, options, home)} : {command, options: registerOptions(name, options, home)}
}
function serveOptions(name: string, options: OptionReader, home: string): ServeOptions {
const serve: ServeOptions = {
name,
data: dataFolder(name, home),
approve: "requests",
expireHours: 23,
resetOwners: false,
newToken: false,
port: 5226,
qrCode: "small",
dataGiven: false,
}
for (let option = options.next(); option !== undefined; option = options.next()) {
switch (option) {
case "--data":
serve.data = resolve(options.value(option))
serve.dataGiven = true
break
case "--display-name":
serve.displayName = displayName(options.value(option))
break
case "--approve":
serve.approve = approveMode(options.value(option))
break
case "--owners":
serve.owners = options.optionalValue() === undefined ? [] : options.value(option).split(",").map(ownerEntry)
break
case "--expire":
serve.expireHours = integer(option, options.value(option), 1, 23, "hours from 1 to 23")
break
case "--reset-owners":
serve.resetOwners = true
break
case "--new-token":
serve.newToken = true
break
case "--port":
serve.port = integer(option, options.value(option), 1, 65535, "a port from 1 to 65535")
break
case "--qr-code":
serve.qrCode = qrCodeSize(options.value(option))
break
default:
throw unknownOption(option)
}
}
if (serve.resetOwners && serve.owners !== undefined && serve.owners.length > 0) {
throw usageError("--reset-owners takes --owners without a list: member IDs change in a new agent control chat.")
}
return serve
}
function registerOptions(name: string, options: OptionReader, home: string): RegisterOptions {
const register: RegisterOptions = {name, data: dataFolder(name, home), scope: "user"}
for (let option = options.next(); option !== undefined; option = options.next()) {
switch (option) {
case "--scope":
register.scope = scope(options.value(option))
break
case "--data":
register.data = resolve(options.value(option))
break
default:
throw unknownOption(option)
}
}
return register
}
class OptionReader {
private index = 0
constructor(private readonly args: string[]) {}
next(): string | undefined {
return this.args[this.index++]
}
value(option: string): string {
const value = this.optionalValue()
if (value === undefined) throw usageError(`${option}: a value expected.`)
this.index++
return value
}
optionalValue(): string | undefined {
const value = this.args[this.index]
return value === undefined || value.startsWith("--") ? undefined : value
}
}
export function validAgentName(name: string): boolean {
return name.length <= 32 && /^[a-z0-9](-?[a-z0-9])*-?$/.test(name)
}
function displayName(value: string): string {
const valid = validName(value)
if (valid !== value) throw usageError(`--display-name: not a valid SimpleX name; try ${valid}`)
return value
}
function approveMode(value: string): Mode {
if (value !== "requests" && value !== "host") throw usageError(`--approve: requests or host expected, got ${value}`)
return value
}
function qrCodeSize(value: string): QrCodeSize {
if (value !== "small" && value !== "large") throw usageError(`--qr-code: small or large expected, got ${value}`)
return value
}
function scope(value: string): Scope {
if (value !== "user" && value !== "local") throw usageError(`--scope: user or local expected, got ${value}`)
return value
}
function ownerEntry(entry: string): OwnerEntry {
const match = /^([1-9]\d*):(.+)$/.exec(entry)
if (!match) throw usageError(`--owners: ID:NAME expected, got ${entry}`)
return {memberId: Number(match[1]), name: match[2]}
}
function integer(option: string, value: string, min: number, max: number, expected: string): number {
const n = Number(value)
if (!/^\d+$/.test(value) || n < min || n > max) throw usageError(`${option}: ${expected} expected, got ${value}`)
return n
}
function dataFolder(name: string, home: string): string {
return join(home, ".simplex-mcp", name)
}
function unknownOption(option: string): ExitError {
return usageError(`Unknown option ${option}. Run simplex-mcp --help.`)
}
function usageError(message: string): ExitError {
return new ExitError(message, 2)
}
async function main(args: string[]): Promise<void> {
const command = parseCommand(args)
switch (command.command) {
case "help":
process.stdout.write(helpText)
return
case "serve":
return serve(command.options)
case "register":
return log(register(command.options))
}
}
if (require.main === module) {
main(process.argv.slice(2)).catch(error => {
if (error instanceof ExitError) {
process.stderr.write(`${error.message}\n`)
process.exit(error.code)
}
logError("simplex-mcp", error)
process.exit(1)
})
}
+609
View File
@@ -0,0 +1,609 @@
import {ChatEvent, T} from "@simplex-chat/types"
import * as qrcode from "qrcode"
import {api} from "simplex-chat"
import {Action, ActionHooks, Actions, Subject, ToolError, leaveVerbs, subjectText, summary} from "./actions"
import {ExitError, chatErrorOf, log, logError, oneLine, shellWord} from "./log"
import {chatNumber, chatRef, isChatRef, utcMinutes, validName, viewName} from "./messages"
import type {OwnerEntry, QrCodeSize} from "./serve"
import {AgentTools, chatSubject, isActive, isConnected, memberCurrent} from "./tools"
const controlCommands: T.ChatBotCommand[] = [
{type: "command", keyword: "pending", label: "Pending actions"},
{type: "command", keyword: "list", label: "Chats and approvals"},
]
const commandsReply = "Commands: /approve, /reject, /revoke, /pending, /list. Each card shows its commands."
const cardTooLong = "text: too long for its approval card; shorten it."
const maxCardLength = 15_000
const listedChats = 100
const maxLineLength = 100
const truncationMark = "…"
const longestError = "€".repeat(600)
const greeting = "I'm your agent. I ask here before I connect or send."
const noOwners = "No owners in the agent control chat."
const qrCodeHint = "If the QR code does not scan, restart with --qr-code large, or paste the link into the app."
const qrMargin = 2
const darkCell = "\x1b[40m "
const lightCell = "\x1b[47m "
const darkHalfCell = "\x1b[40m "
const lightHalfCell = "\x1b[47m "
const darkTopCell = "\x1b[37;40m▄"
const darkBottomCell = "\x1b[30;47m▄"
const resetColors = "\x1b[0m"
export interface ControlData {
control: true
nextAction: number
}
export interface ControlOptions {
displayName: string
owners?: OwnerEntry[]
registerHint: string
qrCode: QrCodeSize
}
export interface Command {
keyword: string
params: string
}
interface ChatApproval {
approvedBy: string
approvedById: number
approvedAt: string
}
type OwnerRequest =
| {type: "approve" | "reject"; action: number; chat?: string}
| {type: "approveChat" | "revoke"; chat: string}
| {type: "pending" | "list"}
interface Shown {
name: (name: string) => string
command: (command: string) => string
}
const inLog: Shown = {name: oneLine, command: command => command}
const inChat: Shown = {name: safeName, command: codeSpan}
export class Control implements Pick<ActionHooks, "refusal" | "saveCounter" | "postCard" | "outcome"> {
private members: T.GroupMember[] = []
private writes: Promise<unknown> = Promise.resolve()
private ownerCommands: Promise<unknown> = Promise.resolve()
constructor(
private readonly chat: api.ChatApi,
private group: T.GroupInfo,
private readonly options: ControlOptions,
private readonly actions: Actions,
private readonly tools: Pick<AgentTools, "knownChat" | "agentChats">,
) {}
get groupId(): number {
return this.group.groupId
}
get chatRef(): string {
return `#${this.group.groupId}`
}
async load(): Promise<void> {
if (isLost(this.group)) return this.lose()
const {groupProfile} = this.group
if (groupProfile.displayName !== this.options.displayName || JSON.stringify(groupProfile.groupPreferences?.commands) !== JSON.stringify(controlCommands)) {
this.group = await this.chat.apiUpdateGroupProfile(this.groupId, {...groupProfile, displayName: this.options.displayName, groupPreferences: {...groupProfile.groupPreferences, commands: controlCommands}})
}
await this.refreshMembers()
}
async start(): Promise<void> {
if (this.actions.lost) return
const {owners} = this.options
if (owners === undefined) return this.startOwners()
if (owners.length === 0) return this.startObserver()
return this.startListed(owners)
}
refusal(action: Action): string | undefined {
if (action.tool === "send" && largestCard(action, this.actions.mode, this.longestApproverName()) > maxCardLength) return cardTooLong
if (this.approvers().length > 0) return undefined
return this.options.owners === undefined
? "The agent has no owner. The person who runs it has to join its control chat with the link the server printed."
: "The agent has no approving owner. The person who runs it has to restart it with the --owners options it printed."
}
standingApprover(chatInfo: T.ChatInfo): string | undefined {
const approval = approvalOf(customDataOf(chatInfo))
return this.actions.mode === "requests" && approval !== undefined && this.approvers().some(({groupMemberId}) => groupMemberId === approval.approvedById) ? safeName(approval.approvedBy) : undefined
}
saveCounter(nextAction: number): Promise<void> {
const write = this.writes.then(() => this.chat.apiSetGroupCustomData(this.groupId, controlData(nextAction)))
this.writes = write.catch(() => undefined)
return write
}
async postCard(action: Action): Promise<number> {
return this.post(cardText(action, this.actions.mode))
}
async outcome(action: Action): Promise<void> {
const text = cardText(action, this.actions.mode)
if (action.card === undefined) await this.post(text)
else await this.chat.apiUpdateChatItem(T.ChatType.Group, this.groupId, action.card, {type: "text", text}, false)
}
onMessage(ci: T.AChatItem): void {
const {chatItem} = ci
if (this.actions.lost || chatItem.chatDir.type !== "groupRcv" || chatItem.content.type !== "rcvMsgContent") return
const command = parseCommand(chatItem.content.msgContent.text)
if (command === undefined) return
const sender = chatItem.chatDir.groupMember
this.ownerCommands = this.ownerCommands
.then(() => this.ownerCommand(sender, command))
.then(reply => (reply === undefined ? undefined : this.chat.apiSendTextReply(ci, reply)))
.catch(error => logError("control chat command", error))
}
async onEvent(event: ChatEvent): Promise<void> {
if (this.actions.lost) return
if (losesControl(event)) return this.lose()
const before = this.humanOwners()
await this.refreshMembers()
const {owners} = this.options
if (event.type === "joinedGroupMember" && this.isHuman(event.member)) {
if (owners === undefined) return this.ownerJoined(event.member)
if (owners.length === 0) return this.observerJoined(event.member)
}
if (owners === undefined && before.length > 0 && this.approvers().length === 0) {
log(noOwners)
return this.openLink(T.GroupMemberRole.Member)
}
if (owners !== undefined && owners.length > 0) await this.reportOwnerChanges(owners, before)
}
async ownerCommand(sender: T.GroupMember, command: Command): Promise<string | undefined> {
this.actions.expireDue()
const request = ownerRequest(command)
if (request === undefined) return commandsReply
if (!this.isApprover(sender)) return this.options.owners === undefined ? "Only owners approve actions." : "Only the owners listed in --owners approve actions."
switch (request.type) {
case "pending": {
const lines = this.actions.pending().filter(({card}) => card !== undefined).map(pendingLine)
return lines.length > 0 ? lines.join("\n") : "No pending actions."
}
case "list":
return this.chatsText()
case "approveChat":
return this.approveChat(sender, request.chat)
case "revoke":
return this.revoke(request.chat)
case "approve":
case "reject": {
const action = this.actions.get(request.action)
if (action === undefined || action.state !== "pending" || action.card === undefined) return `Action ${request.action} is not pending.`
if (request.chat !== undefined && (action.tool === "connect" || action.chat !== request.chat)) return `Action ${request.action} is not for ${request.chat}.`
const approver = safeName(sender.memberProfile.displayName)
if (request.type === "approve") this.actions.approve(action, approver)
else this.actions.reject(action, approver)
return undefined
}
}
}
private async approveChat(sender: T.GroupMember, chat: string): Promise<string> {
const chatInfo = await this.agentChat(chat)
if (chatInfo === undefined) return `No chat ${chat}. /list shows the agent's chats.`
const approvedBy = sender.memberProfile.displayName
const approval: ChatApproval = {approvedBy, approvedById: sender.groupMemberId, approvedAt: new Date().toISOString()}
await this.setCustomData(chat, {...customDataOf(chatInfo), simplexMcp: approval})
const approver = safeName(approvedBy)
for (const action of this.actions.pending()) {
if (action.tool === "send" && action.chat === chat && action.card !== undefined) this.actions.approve(action, approver)
}
return `Approved for ${chatName(chatSubject(chatInfo))} (${chat}) by ${approver}. /'revoke ${chat}'`
}
private async revoke(chat: string): Promise<string> {
const chatInfo = await this.agentChat(chat)
if (chatInfo === undefined || approvalOf(customDataOf(chatInfo)) === undefined) return `Messages to ${chat} have no standing approval.`
const {simplexMcp, ...customData} = customDataOf(chatInfo) as {simplexMcp: unknown}
await this.setCustomData(chat, customData)
return `Messages to ${chatName(chatSubject(chatInfo))} (${chat}) need approval again.`
}
private async chatsText(): Promise<string> {
const chats = await this.tools.agentChats()
const lines = chats.slice(0, listedChats).map(({chatInfo}) => {
const approvedBy = this.standingApprover(chatInfo)
const approval = approvedBy === undefined ? "" : `, approved for this chat by ${approvedBy}`
const connection = isConnected(chatInfo) ? "" : ", not connected"
return truncatedLine(`${chatRef(chatInfo)} ${chatName(chatSubject(chatInfo))}${approval}${connection}`)
})
const rest = chats.length - lines.length
if (rest > 0) lines.push(`${rest} older chat${rest === 1 ? " is" : "s are"} not shown.`)
return lines.length > 0 ? lines.join("\n") : "No chats."
}
private async agentChat(chat: string): Promise<T.ChatInfo | undefined> {
try {
return (await this.tools.knownChat(chat)).chatInfo
} catch (error) {
if (error instanceof ToolError) return undefined
throw error
}
}
private setCustomData(chat: string, customData: object): Promise<void> {
return chat.startsWith("@") ? this.chat.apiSetContactCustomData(chatNumber(chat), customData) : this.chat.apiSetGroupCustomData(chatNumber(chat), customData)
}
private async startOwners(): Promise<void> {
if (this.humanOwners().length > 0) return log(this.options.registerHint)
if (this.humans().length > 0) log(noOwners)
await this.openLink(T.GroupMemberRole.Member)
}
private async startObserver(): Promise<void> {
if (this.humans().length === 0) return this.openLink(T.GroupMemberRole.Observer)
await this.announce(shown => `${this.humans().map(member => joinedText(member, shown.name)).join(". ")}. ${restartText(this.humans(), shown)}`)
}
private async startListed(owners: OwnerEntry[]): Promise<void> {
const humans = this.humans()
const listed = owners.map(owner => {
const member = humans.find(({groupMemberId, memberProfile}) => groupMemberId === owner.memberId && memberProfile.displayName === owner.name)
if (!member) throw new ExitError(`--owners: member ${entryText(owner)} is not in the agent control chat. Start with --reset-owners --owners, or use the IDs of current members.`, 2)
return member
})
for (const member of listed) {
if (member.memberRole !== T.GroupMemberRole.Owner) await this.chat.apiSetMembersRole(this.groupId, [member.groupMemberId], T.GroupMemberRole.Owner)
}
for (const member of humans) {
if (!listed.includes(member)) await this.chat.apiRemoveMembers(this.groupId, [member.groupMemberId])
}
await this.deleteLink()
await this.refreshMembers()
log(`Owners: ${owners.map(entryText).join(",")}.`)
log(this.options.registerHint)
}
private async ownerJoined(member: T.GroupMember): Promise<void> {
await this.deleteLink()
await this.chat.apiSetMembersRole(this.groupId, [member.groupMemberId], T.GroupMemberRole.Owner)
await this.refreshMembers()
await this.post(greeting)
log(`${joinedText(member, oneLine)} and is now an owner. The link is deleted.`)
log(this.options.registerHint)
}
private async observerJoined(member: T.GroupMember): Promise<void> {
await this.deleteLink()
await this.announce(shown => `${joinedText(member, shown.name)}. ${restartText(this.humans(), shown)}`)
}
private async reportOwnerChanges(owners: OwnerEntry[], before: T.GroupMember[]): Promise<void> {
const after = this.humanOwners()
const isListed = (member: T.GroupMember): boolean => owners.some(owner => listsMember(owner, member))
for (const member of after) {
if (!isListed(member) && !before.some(sameMember(member))) {
await this.announce(shown => `${memberText(member, shown.name)} is now an owner and can't approve actions yet. ${restartText(after, shown)}`)
}
}
for (const member of before) {
if (isListed(member) && !after.some(sameMember(member))) {
const remaining = (shown: Shown): string => (after.length > 0 ? restartText(after, shown) : "No owners remain: restart the server with --reset-owners --owners")
await this.announce(shown => `${memberText(member, shown.name)} is no longer an owner. ${remaining(shown)}`)
}
}
}
private lose(): void {
this.actions.loseControl()
log(`The agent lost its control chat. Restart the server with --reset-owners.`)
}
private async openLink(role: T.GroupMemberRole): Promise<void> {
await this.deleteLink()
const link = await this.chat.apiCreateGroupLink(this.groupId, role)
log("Join the agent control chat in the SimpleX app. The link works once:")
log(qrCodeText(link, this.options.qrCode))
log(link)
if (this.options.qrCode === "small") log(qrCodeHint)
log(role === T.GroupMemberRole.Observer
? "The link adds an observer. Its ID and name are printed when it joins."
: `Whoever joins by this link becomes an owner of "${this.options.displayName}" and approves its actions. To approve owners yourself, start with --owners.`)
}
private async deleteLink(): Promise<void> {
try {
await this.chat.apiDeleteGroupLink(this.groupId)
} catch (error) {
const chatError = chatErrorOf(error)
if (chatError?.type !== "errorStore" || chatError.storeError.type !== "groupLinkNotFound") throw error
}
}
private async announce(text: (shown: Shown) => string): Promise<void> {
log(text(inLog))
await this.post(text(inChat))
}
private async post(text: string): Promise<number> {
const [item] = await this.chat.apiSendMessages([T.ChatType.Group, this.groupId], [{msgContent: {type: "text", text}, mentions: {}}])
return item.chatItem.meta.itemId
}
private async refreshMembers(): Promise<void> {
this.members = await this.chat.apiListMembers(this.groupId)
}
private humans(): T.GroupMember[] {
return this.members.filter(member => this.isHuman(member))
}
private humanOwners(): T.GroupMember[] {
return this.humans().filter(({memberRole}) => memberRole === T.GroupMemberRole.Owner)
}
private approvers(): T.GroupMember[] {
return this.humans().filter(member => this.isApprover(member))
}
private longestApproverName(): string {
const names = this.approvers().map(({memberProfile}) => safeName(memberProfile.displayName))
return names.reduce((longest, name) => (encodedLength(name) > encodedLength(longest) ? name : longest), "")
}
private isHuman(member: T.GroupMember): boolean {
return member.groupMemberId !== this.group.membership.groupMemberId && isActive(member)
}
private isApprover(member: T.GroupMember): boolean {
const {owners} = this.options
const owner = member.memberRole === T.GroupMemberRole.Owner
return owners === undefined ? owner : owner && owners.some(entry => listsMember(entry, member))
}
}
export async function findControlGroup(chat: api.ChatApi, userId: number): Promise<T.GroupInfo | undefined> {
const groups = (await chat.apiListGroups(userId)).filter(({customData}) => controlDataOf(customData) !== undefined)
return groups.reduce<T.GroupInfo | undefined>((latest, group) => (latest === undefined || group.groupId > latest.groupId ? group : latest), undefined)
}
export function controlDataOf(customData: object | undefined): ControlData | undefined {
const data = (customData as {simplexMcp?: Partial<ControlData>} | undefined)?.simplexMcp
return data?.control === true && typeof data.nextAction === "number" ? {control: true, nextAction: data.nextAction} : undefined
}
export async function createControlChat(chat: api.ChatApi, userId: number, displayName: string, nextAction: number): Promise<T.GroupInfo> {
const group = await chat.apiNewGroup(userId, {
displayName,
fullName: "",
description: "Supervise your AI agent here: approve or reject its connections and messages with one tap.",
groupPreferences: {history: {enable: T.GroupFeatureEnabled.On}, commands: controlCommands},
})
const customData = controlData(nextAction)
await chat.apiSetGroupCustomData(group.groupId, customData)
log(`Agent control chat "${displayName}" created.`)
return {...group, customData}
}
export async function resetControlChat(chat: api.ChatApi, userId: number, group: T.GroupInfo, displayName: string, nextAction: number): Promise<T.GroupInfo> {
const {membership} = group
if (memberCurrent(membership) && membership.memberRole !== T.GroupMemberRole.Owner) await chat.apiLeaveGroup(group.groupId)
await chat.apiDeleteChat(T.ChatType.Group, group.groupId, {type: "full", notify: true})
return createControlChat(chat, userId, displayName, nextAction)
}
export function isControlEvent(event: ChatEvent, groupId: number): boolean {
switch (event.type) {
case "joinedGroupMember":
case "joinedGroupMemberConnecting":
case "connectedToGroupMember":
case "memberRole":
case "leftMember":
case "deletedMember":
case "deletedMemberUser":
case "groupDeleted":
return event.groupInfo.groupId === groupId
default:
return false
}
}
export function parseCommand(text: string): Command | undefined {
const lines = text.split("\n")
if (lines.at(-1) === "") lines.pop()
const match = lines.length === 1 ? /^ *\/(\S+)([\s\S]*)$/.exec(lines[0]) : null
return match ? {keyword: match[1], params: match[2].trim()} : undefined
}
export function cardText(action: Action, mode: Actions["mode"]): string {
const request = requestLines(action)
const approval = mode === "host" ? "Performed without approval (mode host)" : action.standing ? `Approved for this chat by ${action.decidedBy}` : `Approved by ${action.decidedBy}`
switch (action.state) {
case "pending":
return action.interrupted
? [...request, "Cancelled: the server stopped."].join("\n")
: [...request, commandsLine(action)].join("\n")
case "performing":
return [...request, `${approval}; interrupted: the server stopped.`].join("\n")
case "done":
return [...request, action.standing ? `${approval}. /'revoke ${action.chat}'` : `${approval}.`].join("\n")
case "failed":
return [...request, `${approval}; failed: ${codeSpan(oneLine(action.error ?? ""))}.`].join("\n")
case "rejected":
return [...request, `Rejected by ${action.decidedBy}.`].join("\n")
case "expired":
return [...request, "Expired."].join("\n")
}
}
function requestLines(action: Action): string[] {
const done = action.state === "done"
const subject = subjectText(action.subject, safeName)
switch (action.tool) {
case "connect": {
const {plan, input} = action
const verb = !done ? "I want to connect" : action.connected ? "I connected" : "I asked to connect"
const message = input.message === undefined ? [] : ["With the message:", ...codeLines(input.message)]
return [`${verb} to ${subject}, ${action.subject.incognito ? "incognito" : "as myself"}.`, ...codeLines(plan.description), ...codeLines(plan.welcome), ...message]
}
case "send": {
const {replyTo, mentions} = action
const reply = replyTo === undefined ? [] : [`replying to ${safeName(replyTo.from)}: ${codeSpan(replyTo.text.replace(/\s+/g, " ").slice(0, 100))}`]
const mentioning = mentions.length === 0 ? [] : [`mentioning ${mentions.map(({name}) => mentionText(name)).join(", ")}`]
return [`${done ? "I sent" : "I want to send"} a message to ${subject}${action.subject.incognito ? ", where I am incognito" : ""}:`, ...codeLines(action.input.text), ...reply, ...mentioning]
}
case "leave": {
const {present, past} = leaveVerbs(action)
return [`${done ? `I ${past}` : `I want to ${present}`} ${chatName(action.subject)} (${action.chat}).`]
}
}
}
function commandsLine(action: Action): string {
switch (action.tool) {
case "connect":
return `/'approve ${action.n}' or /'reject ${action.n}'`
case "send":
return `/'approve ${action.chat} ${action.n}', /'approve ${action.chat} all' or /'reject ${action.chat} ${action.n}'`
case "leave":
return `/'approve ${action.chat} ${action.n}' or /'reject ${action.chat} ${action.n}'`
}
}
function pendingLine(action: Action): string {
return `${action.n} ${summary(action, safeName).what} expires ${utcMinutes(action.expires)}`
}
function largestCard(action: Action, mode: Actions["mode"], approver: string): number {
const decidedBy = action.decidedBy ?? approver
const cards: Action[] = [action, {...action, state: "done", decidedBy}, {...action, state: "failed", decidedBy, error: longestError}]
return Math.max(...cards.map(card => encodedLength(cardText(card, mode))))
}
function encodedLength(text: string): number {
return [...text].reduce((length, c) => length + (/["\\\n\r\t]/.test(c) ? 2 : c < " " ? 6 : Buffer.byteLength(c)), 0)
}
function truncatedLine(line: string): string {
if (encodedLength(line) <= maxLineLength) return line
let length = encodedLength(truncationMark)
let kept = ""
for (const c of line) {
length += encodedLength(c)
if (length > maxLineLength) break
kept += c
}
return `${kept}${truncationMark}`
}
function chatName(subject: Subject): string {
return subjectText({...subject, simplexName: undefined}, safeName)
}
function codeLines(text: string | undefined): string[] {
return text === undefined ? [] : text.split("\n").map(line => (line.trim() === "" ? "" : codeSpan(line)))
}
function codeSpan(text: string): string {
return `\`${text.replace(/`/g, "'").replace(/^ | $/g, "\u00a0")}\``
}
function safeName(name: string): string {
const line = oneLine(name)
return validName(line) === line ? line : codeSpan(line)
}
function mentionText(name: string): string {
const line = oneLine(name)
return validName(line) === line ? `@${viewName(line)}` : codeSpan(`@${line}`)
}
function controlData(nextAction: number): {simplexMcp: ControlData} {
return {simplexMcp: {control: true, nextAction}}
}
function customDataOf(chatInfo: T.ChatInfo): object | undefined {
return chatInfo.type === "direct" ? chatInfo.contact.customData : (chatInfo as T.ChatInfo.Group).groupInfo.customData
}
function approvalOf(customData: object | undefined): {approvedBy: string; approvedById: number} | undefined {
const data = (customData as {simplexMcp?: Partial<ChatApproval>} | undefined)?.simplexMcp
return typeof data?.approvedBy === "string" && typeof data.approvedById === "number" ? {approvedBy: data.approvedBy, approvedById: data.approvedById} : undefined
}
function ownerRequest({keyword, params}: Command): OwnerRequest | undefined {
const words = params === "" ? [] : params.split(/\s+/)
const chat = words.length === 2 && isChatRef(words[0]) ? words[0] : undefined
const value = words.length === 1 || chat !== undefined ? words.at(-1) : undefined
if ((keyword === "approve" || keyword === "reject") && value !== undefined && /^\d+$/.test(value)) return {type: keyword, action: Number(value), chat}
if (keyword === "approve" && chat !== undefined && value === "all") return {type: "approveChat", chat}
if (keyword === "revoke" && chat === undefined && value !== undefined && isChatRef(value)) return {type: "revoke", chat: value}
if ((keyword === "pending" || keyword === "list") && words.length === 0) return {type: keyword}
return undefined
}
export function qrCodeText(link: string, size: QrCodeSize): string {
const {modules} = qrcode.create(link, {errorCorrectionLevel: "L"})
const positions = Array.from({length: modules.size + 2 * qrMargin}, (_, i) => i - qrMargin)
const inside = (i: number): boolean => i >= 0 && i < modules.size
const dark = (row: number, col: number): boolean => inside(row) && inside(col) && modules.get(row, col) !== 0
const cell = (row: number, col: number): string => (dark(row, col) ? darkCell : lightCell)
const halfCell = (top: boolean, bottom: boolean): string => {
if (top === bottom) return top ? darkHalfCell : lightHalfCell
return top ? darkTopCell : darkBottomCell
}
const lines = size === "large"
? positions.map(row => positions.map(col => cell(row, col)).join(""))
: positions.filter((_, i) => i % 2 === 0).map(row => positions.map(col => halfCell(dark(row, col), dark(row + 1, col))).join(""))
return lines.map(line => line + resetColors).join("\n")
}
function losesControl(event: ChatEvent): boolean {
switch (event.type) {
case "deletedMemberUser":
case "groupDeleted":
return true
case "memberRole":
return event.member.groupMemberId === event.groupInfo.membership.groupMemberId && event.toRole !== T.GroupMemberRole.Owner
default:
return false
}
}
function isLost({membership}: T.GroupInfo): boolean {
const statuses: T.GroupMemberStatus[] = [T.GroupMemberStatus.Creator, T.GroupMemberStatus.Connected, T.GroupMemberStatus.Complete]
return membership.memberRole !== T.GroupMemberRole.Owner || !statuses.includes(membership.memberStatus)
}
function listsMember({memberId, name}: OwnerEntry, member: T.GroupMember): boolean {
return member.groupMemberId === memberId && member.memberProfile.displayName === name
}
function sameMember(member: T.GroupMember): (other: T.GroupMember) => boolean {
return other => other.groupMemberId === member.groupMemberId
}
function memberText({groupMemberId, memberProfile}: T.GroupMember, showName: (name: string) => string): string {
return `${showName(memberProfile.displayName)} (member ${groupMemberId})`
}
function joinedText(member: T.GroupMember, showName: (name: string) => string): string {
return `${showName(member.memberProfile.displayName)} joined (member ${member.groupMemberId})`
}
export function entryText({memberId, name}: OwnerEntry): string {
return shellWord(`${memberId}:${name}`, /^\d+:[\p{L}\p{N}_-]+$/u)
}
function ownersText(members: T.GroupMember[]): string {
return members.map(({groupMemberId, memberProfile}) => entryText({memberId: groupMemberId, name: oneLine(memberProfile.displayName)})).join(",")
}
function restartText(members: T.GroupMember[], shown: Shown): string {
return `Restart the server with ${shown.command(`--owners ${ownersText(members)}`)}`
}
+77
View File
@@ -0,0 +1,77 @@
import {randomBytes} from "crypto"
import {chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync} from "fs"
import {join} from "path"
import {ExitError} from "./log"
export interface ServerFile {
name: string
port: number
expireHours: number
}
export function createDataFolder(dir: string): void {
mkdirSync(dir, {recursive: true, mode: 0o700})
if (process.platform !== "win32") chmodSync(dir, 0o700)
}
export function acquireLock(dir: string): void {
const file = join(dir, "lock")
if (createLock(file)) return
const pid = lockPid(file)
if (pid !== undefined && pid !== process.pid && isRunning(pid)) throw new ExitError(`Another server uses ${dir} (PID ${pid}).`, 1)
unlinkSync(file)
if (!createLock(file)) throw new ExitError(`Another server uses ${dir} (PID ${lockPid(file)}).`, 1)
}
export function releaseLock(dir: string): void {
unlinkSync(join(dir, "lock"))
}
export function serverToken(dir: string, renew: boolean): string {
const file = join(dir, "token")
if (!renew && existsSync(file)) return readFileSync(file, "utf8").trim()
const token = randomBytes(32).toString("base64url")
writeFileSync(file, `${token}\n`, {mode: 0o600})
return token
}
export function headersFile(dir: string): string {
return join(dir, "headers.json")
}
export function writeHeadersFile(dir: string, token: string): void {
writeFileSync(headersFile(dir), `${JSON.stringify({Authorization: `Bearer ${token}`})}\n`, {mode: 0o600})
}
export function writeServerFile(dir: string, server: ServerFile): void {
writeFileSync(join(dir, "server.json"), `${JSON.stringify(server)}\n`, {mode: 0o600})
}
export function readServerFile(dir: string): ServerFile | undefined {
const file = join(dir, "server.json")
return existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : undefined
}
function createLock(file: string): boolean {
try {
writeFileSync(file, `${process.pid}\n`, {flag: "wx", mode: 0o600})
return true
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "EEXIST") return false
throw error
}
}
function lockPid(file: string): number | undefined {
const pid = Number(readFileSync(file, "utf8").trim())
return Number.isInteger(pid) && pid > 0 ? pid : undefined
}
function isRunning(pid: number): boolean {
try {
process.kill(pid, 0)
return true
} catch (error) {
return (error as NodeJS.ErrnoException).code === "EPERM"
}
}
Loaded 100 of 234 files, more files were not shown because too many files have changed in this diff. Show more