Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
554e0f0
fix: use transient content blocks for image attachments
f-liva Mar 15, 2026
71412cc
feat(whatsapp-gateway): add TOML config fallback, agent name resoluti…
f-liva Mar 16, 2026
e0dfa5c
fix(whatsapp-gateway): add connection resilience and PM2 process mana…
f-liva Mar 16, 2026
2793d56
feat: custom Docker image with Lazycat NAS deployment support
Mar 15, 2026
1585172
fix(ci): use default GITHUB_TOKEN for push triggers, PAT_TOKEN only f…
Mar 15, 2026
aee09bf
chore: update workflow to build from custom branch
f-liva Mar 15, 2026
e4728a5
feat(claude-code): support image attachments from WhatsApp
f-liva Mar 16, 2026
5ab19b0
feat(whatsapp-gateway): add startup port cleanup, PID file, and read …
f-liva Mar 16, 2026
0f867e1
feat(whatsapp-gateway): convert Markdown to WhatsApp formatting befor…
f-liva Mar 16, 2026
0360219
fix(whatsapp-gateway): harden connection resilience and message relia…
f-liva Mar 16, 2026
1036b18
fix(entrypoint): resurrect PM2 processes on container boot
f-liva Mar 16, 2026
e669b6e
fix(whatsapp-gateway): prevent conflict reconnect loop with stable ba…
f-liva Mar 16, 2026
db1d9e8
feat(whatsapp-gateway): recover missed messages after reconnection
f-liva Mar 16, 2026
7d3e80a
feat(whatsapp-gateway): bulletproof connection resilience system
f-liva Mar 16, 2026
6195b2a
refactor(whatsapp-gateway): complete rewrite for stability
f-liva Mar 16, 2026
49cbd88
fix: resolve ambiguous 'main' checkout when upstream remote exists
f-liva Mar 16, 2026
7cf9294
feat(whatsapp-gateway): handle vCard and multi-contact messages
f-liva Mar 16, 2026
aa6ca0c
fix(runtime): retry retryable LLM errors and add action validator
f-liva Mar 17, 2026
35594b0
fix(whatsapp-gateway): add message deduplication and filter group mes…
f-liva Mar 17, 2026
bf91ea6
fix(runtime): deterministic sender identity verification for WhatsApp
f-liva Mar 17, 2026
9e13469
fix(runtime): deterministic sender identity verification for WhatsApp
f-liva Mar 17, 2026
0dca2df
fix(routing): propagate channel_type through entire message chain to …
f-liva Mar 17, 2026
78e012d
merge: resolve conflicts for channel_type propagation fix
f-liva Mar 17, 2026
f3b3b2d
Fix build: add missing channel_type arg in openai_compat + suppress w…
f-liva Mar 17, 2026
72a6a30
Fix build: add missing channel_type arg in CLI event handler
f-liva Mar 17, 2026
206ed3f
ci: retrigger build after channel_type fix
f-liva Mar 17, 2026
1ba7fbd
feat(debounce): typing-aware message debounce (#728)
f-liva Mar 18, 2026
45c6b3e
fix(debounce): use 5s default when channel overrides are absent
f-liva Mar 18, 2026
18d3fa6
fix(whatsapp-gateway): add media download, caching, and message debounce
f-liva Mar 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .current-upstream-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v0.4.0
112 changes: 112 additions & 0 deletions .github/workflows/sync-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
name: Sync upstream & build custom image

on:
schedule:
- cron: '0 */6 * * *' # ogni 6 ore
workflow_dispatch: # trigger manuale
push:
branches: [custom] # rebuild ad ogni push su custom

jobs:
sync-and-build:
runs-on: ubuntu-latest
steps:
- name: Checkout fork (custom branch)
uses: actions/checkout@v4
with:
ref: custom
fetch-depth: 0
token: ${{ secrets.PAT_TOKEN }}

- name: Notify Telegram (start)
run: |
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
-d chat_id="${{ secrets.TELEGRAM_CHAT_ID }}" \
-d parse_mode="Markdown" \
-d text="🚀 *OpenFang Build Started*%0A%0ATrigger: \`${{ github.event_name }}\`%0ABranch: \`custom\`%0ACommit: \`${GITHUB_SHA::7}\`%0A[View run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})"

- name: Configure git
run: |
git config user.name "github-actions"
git config user.email "actions@github.com"

- name: Sync main with upstream
if: github.event_name != 'push'
run: |
git remote add upstream https://github.com/RightNow-AI/openfang.git || true
git fetch upstream --tags
git fetch origin main

# Fast-forward main to latest upstream tag
LATEST_TAG=$(git tag -l 'v*' --sort=-v:refname | head -1)
CURRENT=$(cat .current-upstream-version 2>/dev/null || echo "none")
echo "LATEST_TAG=$LATEST_TAG" >> "$GITHUB_ENV"
echo "CURRENT=$CURRENT" >> "$GITHUB_ENV"

if [ "$LATEST_TAG" != "$CURRENT" ]; then
echo "NEW_RELEASE=true" >> "$GITHUB_ENV"
# Reset main to latest tag, cherry-pick our generic fixes
git checkout -B main "$LATEST_TAG"
# Cherry-pick any commits on main that are ours (not upstream)
for commit in $(git log origin/main --oneline --format="%H" "$LATEST_TAG"..origin/main 2>/dev/null); do
git cherry-pick "$commit" --no-commit && git commit -C "$commit" || git cherry-pick --abort || true
done
git push origin main --force
# Now rebase custom on updated main
git checkout custom
git rebase main || git rebase --abort
echo "$LATEST_TAG" > .current-upstream-version
git add .current-upstream-version
git commit -m "chore: sync to upstream $LATEST_TAG" || true
git push origin custom --force
else
echo "NEW_RELEASE=false" >> "$GITHUB_ENV"
fi

- name: Set up Docker Buildx
if: github.event_name == 'push' || env.NEW_RELEASE == 'true'
uses: docker/setup-buildx-action@v3

- name: Login to Docker Hub
if: github.event_name == 'push' || env.NEW_RELEASE == 'true'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Build and push
if: github.event_name == 'push' || env.NEW_RELEASE == 'true'
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
fliva/openfang:latest
fliva/openfang:${{ env.LATEST_TAG || 'custom' }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Update Docker Hub description
if: github.event_name == 'push' || env.NEW_RELEASE == 'true'
uses: peter-evans/dockerhub-description@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: fliva/openfang
readme-filepath: ./DOCKER_README.md

- name: Notify Telegram (success)
if: success() && (github.event_name == 'push' || env.NEW_RELEASE == 'true')
run: |
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
-d chat_id="${{ secrets.TELEGRAM_CHAT_ID }}" \
-d parse_mode="Markdown" \
-d text="✅ *OpenFang Build OK*%0A%0ATag: \`${{ env.LATEST_TAG || 'custom' }}\`%0ABranch: \`custom\`%0ACommit: \`${GITHUB_SHA::7}\`%0A[View run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})"

- name: Notify Telegram (failure)
if: failure()
run: |
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
-d chat_id="${{ secrets.TELEGRAM_CHAT_ID }}" \
-d parse_mode="Markdown" \
-d text="❌ *OpenFang Build FAILED*%0A%0ABranch: \`custom\`%0ACommit: \`${GITHUB_SHA::7}\`%0A[View run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})"
42 changes: 42 additions & 0 deletions DOCKER_README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# OpenFang for Lazycat NAS

Custom [OpenFang](https://github.com/RightNow-AI/openfang) Docker image optimized for deployment on Lazycat LCMD Microserver.

**Automatically rebuilt on every new upstream release via GitHub Actions.**

## What's included

- **OpenFang Agent OS** — Rust-based autonomous AI agent daemon
- **Claude Code CLI** — Anthropic's CLI for Claude, as LLM provider
- **Node.js 22** — JavaScript runtime
- **Python 3** — Python runtime
- **Go** — via Homebrew
- **Homebrew** — package manager for additional tools
- **uv** — fast Python package manager
- **gh** — GitHub CLI
- **gog** — [Google Workspace CLI](https://gogcli.sh/) (Gmail, Calendar, Drive, Sheets, etc.)
- **ffmpeg** — multimedia processing
- **jq** — JSON processor
- **git, curl, wget** — standard utilities

## Non-root execution

The image uses `gosu` to drop root privileges to the `openfang` user at runtime. This is required because Claude Code's `--dangerously-skip-permissions` flag refuses to run as root.

The `openfang` user has passwordless `sudo` access, so it can still install system packages when needed.

## Usage

```bash
docker run -d \
-p 4200:4200 \
-v openfang-data:/data \
-v openfang-home:/home/openfang \
-e OPENFANG_HOME=/data \
fliva/openfang:latest
```

## Source

- **This fork**: [github.com/f-liva/openfang](https://github.com/f-liva/openfang)
- **Upstream**: [github.com/RightNow-AI/openfang](https://github.com/RightNow-AI/openfang)
39 changes: 28 additions & 11 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,37 @@ ENV CARGO_PROFILE_RELEASE_LTO=${LTO} \
CARGO_PROFILE_RELEASE_CODEGEN_UNITS=${CODEGEN_UNITS}
RUN cargo build --release --bin openfang

FROM rust:1-slim-bookworm
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
python3 \
python3-pip \
python3-venv \
nodejs \
npm \
&& rm -rf /var/lib/apt/lists/*

FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates curl git ffmpeg python3 python3-pip chromium gosu sudo procps build-essential jq && rm -rf /var/lib/apt/lists/*
RUN ln -s /usr/bin/python3 /usr/bin/python
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
RUN (type -p wget >/dev/null || (apt-get update && apt-get install -y wget)) && \
mkdir -p -m 755 /etc/apt/keyrings && \
out=$(mktemp) && wget -qO "$out" https://cli.github.com/packages/githubcli-archive-keyring.gpg && \
cat "$out" | tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null && \
chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg && \
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null && \
apt-get update && apt-get install -y gh && rm -rf /var/lib/apt/lists/*
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
apt-get install -y nodejs && \
npm install -g @anthropic-ai/claude-code @qwen-code/qwen-code && \
rm -rf /var/lib/apt/lists/*
RUN useradd -m -s /bin/bash openfang && echo "openfang ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/openfang
USER openfang
RUN NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
RUN eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" && brew install steipete/tap/gogcli
USER root
RUN echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' >> /home/openfang/.bashrc && \
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' >> /root/.bashrc && \
echo 'export PATH="/data/npm-global/bin:$PATH"' >> /home/openfang/.bashrc && \
echo 'export PATH="/data/npm-global/bin:$PATH"' >> /root/.bashrc
COPY --from=builder /build/target/release/openfang /usr/local/bin/
COPY --from=builder /build/agents /opt/openfang/agents
RUN mkdir -p /data && chown openfang:openfang /data
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
EXPOSE 4200
VOLUME /data
ENV OPENFANG_HOME=/data
ENTRYPOINT ["openfang"]
ENTRYPOINT ["entrypoint.sh"]
CMD ["start"]
64 changes: 63 additions & 1 deletion crates/openfang-api/src/channel_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,30 @@ impl ChannelBridgeHandle for KernelBridgeAdapter {
Ok(result.response)
}

async fn send_message_with_context(
&self,
agent_id: AgentId,
message: &str,
ctx: openfang_channels::bridge::ChannelContext,
) -> Result<String, String> {
let result = self
.kernel
.send_message_with_handle(
agent_id,
message,
None,
ctx.sender_id,
ctx.sender_name,
ctx.channel_type,
)
.await
.map_err(|e| format!("{e}"))?;
if result.silent {
return Ok(String::new());
}
Ok(result.response)
}

async fn send_message_with_blocks(
&self,
agent_id: AgentId,
Expand All @@ -102,9 +126,47 @@ impl ChannelBridgeHandle for KernelBridgeAdapter {
};
let result = self
.kernel
.send_message_with_blocks(agent_id, &text, blocks)
.send_message_with_blocks(agent_id, &text, blocks, None)
.await
.map_err(|e| format!("{e}"))?;
Ok(result.response)
}

async fn send_message_with_blocks_and_context(
&self,
agent_id: AgentId,
blocks: Vec<openfang_types::message::ContentBlock>,
ctx: openfang_channels::bridge::ChannelContext,
) -> Result<String, String> {
let text: String = blocks
.iter()
.filter_map(|b| match b {
openfang_types::message::ContentBlock::Text { text, .. } => Some(text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n");
let text = if text.is_empty() {
"[Image]".to_string()
} else {
text
};
let result = self
.kernel
.send_message_with_handle_and_blocks(
agent_id,
&text,
None,
Some(blocks),
ctx.sender_id,
ctx.sender_name,
ctx.channel_type,
)
.await
.map_err(|e| format!("{e}"))?;
if result.silent {
return Ok(String::new());
}
Ok(result.response)
}

Expand Down
4 changes: 2 additions & 2 deletions crates/openfang-api/src/openai_compat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ pub async fn chat_completions(
let kernel_handle: Arc<dyn KernelHandle> = state.kernel.clone() as Arc<dyn KernelHandle>;
match state
.kernel
.send_message_with_handle(agent_id, &last_user_msg, Some(kernel_handle), None, None)
.send_message_with_handle(agent_id, &last_user_msg, Some(kernel_handle), None, None, None)
.await
{
Ok(result) => {
Expand Down Expand Up @@ -379,7 +379,7 @@ async fn stream_response(

let (mut rx, _handle) = state
.kernel
.send_message_streaming(agent_id, message, Some(kernel_handle), None, None)
.send_message_streaming(agent_id, message, Some(kernel_handle), None, None, None, None)
.map_err(|e| format!("Streaming setup failed: {e}"))?;

let (tx, stream_rx) = tokio::sync::mpsc::channel::<Result<SseEvent, Infallible>>(64);
Expand Down
Loading