Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ curl -fsSL https://bun.sh/install | bash
**Ubuntu / Debian / WSL2:**
```bash
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs tmux
sudo apt-get install -y nodejs tmux build-essential
curl -fsSL https://bun.sh/install | bash

# Docker Engine
Expand Down Expand Up @@ -243,7 +243,7 @@ src/
├── config/ Zod schema, config I/O, paths, Gemini CLI settings
├── memory/ SQLite usage tracking, QMD integration
├── mcp/ MCP servers (status, cron, ask-user, gog, admin)
├── channels/ Chat SDK adapters + reply delivery (Discord/Slack)
├── channels/ Chat SDK adapters + reply delivery (Discord/Slack/Telegram)
├── inngest/ Durable functions (agent-run, heartbeat, cron, daily-summary)
├── cli/commands/ CLI command implementations
├── vault/ Secret management (keyring/encrypted-file/command)
Expand Down
27 changes: 12 additions & 15 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 11 additions & 9 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ GeminiClaw treats **Gemini CLI as the core agent engine** and wraps it with thin
| LLM reasoning & tool use | **Gemini CLI** (ACP) | Full-featured agent runtime with sandbox, MCP, multi-turn sessions |
| Durable execution & scheduling | **Inngest** | Retry, step persistence, concurrency control without custom queue |
| Memory search & retrieval | **QMD** | Hybrid search (BM25 + Vector + LLM reranking) without custom embedder |
| Multi-channel messaging | **Vercel Chat SDK** | Discord/Slack adapters without managing gateway connections |
| Multi-channel messaging | **Vercel Chat SDK** | Discord/Slack/Telegram adapters without managing gateway connections |

GeminiClaw itself is the glue: session lifecycle, process pool management, context injection, and security gates. This thin-wrapper approach keeps the codebase extensible without reimplementing capabilities that already exist upstream.

```
┌─────────────────────┐
│ Trigger Sources │
│ Discord · Slack │
│ Discord · Slack · │
│ Telegram │
│ CLI · Cron · HTTP │
└─────────┬───────────┘
│ webhook / event
Expand All @@ -37,8 +38,8 @@ GeminiClaw itself is the glue: session lifecycle, process pool management, conte
│ │ │ │ │
│ ┌───────▼─────┐ ┌────────▼────────┐ ┌──────▼──────┐ │
│ │ QMD │ │ Chat SDK │ │ MCP │ │
│ │ hybrid │ │ Discord/Slack │ │ Servers │ │
│ │ search │ │ reply routing │ │ gog · cron │ │
│ │ hybrid │ │ Discord/Slack/ │ │ Servers │ │
│ │ search │ │ Telegram │ │ gog · cron │ │
│ │ over │ │ │ │ ask-user │ │
│ │ memory │ │ │ │ status │ │
│ └─────────────┘ └─────────────────┘ │ admin │ │
Expand Down Expand Up @@ -111,11 +112,12 @@ Highest-scoring process wins. Within the same score, recency (`lastUsedAt`) brea
Inngest serves as both the **durable execution engine** and the **event bus** that unifies all trigger sources. Every external input — webhooks, CLI commands, cron schedules — is normalized into an Inngest event (`geminiclaw/run`), making the agent execution path uniform regardless of origin.

```
Discord webhook ─┐
Slack webhook ─┤
CLI command ─┼─→ inngest.send('geminiclaw/run') ─→ agentRun() ─→ turn lifecycle
Heartbeat cron ─┤
Custom cron ─┘
Discord webhook ─┐
Slack webhook ─┤
Telegram polling ─┤
CLI command ─┼─→ inngest.send('geminiclaw/run') ─→ agentRun() ─→ turn lifecycle
Heartbeat cron ─┤
Custom cron ─┘
```

This architecture means adding a new trigger source (e.g. a GitHub webhook, an HTTP API, or a scheduled task) only requires emitting an Inngest event — no changes to the agent execution pipeline.
Expand Down
2 changes: 1 addition & 1 deletion docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ Gemini CLI's ACP does not support native `ask_user` prompts, and the MCP specifi

```
1. MCP tool → writes ask-user-pending-{askId}.json
2. Runner → detects file → sends button message to chat (Discord/Slack)
2. Runner → detects file → sends button message to chat (Discord/Slack/Telegram)
3. User → clicks button
4. Chat handler → writes ask-user-answer-{askId}.json
5. MCP tool → detects file (500ms polling) → continue or abort
Expand Down
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,17 @@
"prepare": "git config core.hooksPath .githooks"
},
"dependencies": {
"@chat-adapter/discord": "^4.18.0",
"@chat-adapter/slack": "^4.14.0",
"@chat-adapter/state-memory": "^4.14.0",
"@chat-adapter/discord": "^4.20.0",
"@chat-adapter/slack": "^4.20.0",
"@chat-adapter/state-memory": "^4.20.0",
"@chat-adapter/telegram": "^4.20.0",
"@clack/prompts": "^1.0.1",
"@google/gemini-cli": "0.34.0-nightly.20260307.6c3a90645",
"@mariozechner/pi-tui": "^0.54.2",
"@modelcontextprotocol/sdk": "^1",
"@tobilu/qmd": "^1.1.6",
"chalk": "^5",
"chat": "^4.14.0",
"chat": "^4.20.0",
"commander": "^12",
"croner": "^10.0.1",
"express": "^5",
Expand Down
15 changes: 15 additions & 0 deletions patches/@tobilu%2Fqmd@1.1.6.patch
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,18 @@ index d1b09687ae86c1f22c245091ed7391879057f7c1..29946f0f109449d7d36e071b46cfdfbf
const server = new McpServer({ name: "qmd", version: "0.9.9" }, { instructions: buildInstructions(store) });
// ---------------------------------------------------------------------------
// Resource: qmd://{path} - read-only access to documents by path
diff --git a/package.json b/package.json
index a47a93bc0a8cceeb46432cb2e2f797bd2be7ae6b..a7d3041db4c51be80c17aa970db022a448926e63 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,9 @@
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
- }
+ },
+ "./dist/store.js": "./dist/store.js",
+ "./dist/mcp.js": "./dist/mcp.js"
},
"bin": {
"qmd": "dist/qmd.js"
2 changes: 1 addition & 1 deletion src/agent/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function shouldBootstrap(params: {
if (!existsSync(join(workspacePath, BOOTSTRAP_FILENAME))) return false;

// For channel triggers, only activate in home channel or DMs
if (trigger === 'discord' || trigger === 'slack') {
if (trigger === 'discord' || trigger === 'slack' || trigger === 'telegram') {
if (!isHomeChannel && !isDM) return false;
}

Expand Down
7 changes: 2 additions & 5 deletions src/agent/context-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,7 @@ export class ContextBuilder {
/** Resolve home channel as `platform:channelId` string. */
private resolveHomeChannel(): string | undefined {
const config = loadConfig();
const dc = config.channels.discord;
if (dc.enabled && dc.homeChannel) return `discord:${dc.homeChannel}`;
const sc = config.channels.slack;
if (sc.enabled && sc.homeChannel) return `slack:${sc.homeChannel}`;
if (config.home) return `${config.home.channel}:${config.home.channelId}`;
return undefined;
}

Expand Down Expand Up @@ -324,7 +321,7 @@ export class ContextBuilder {
parts.push('Do NOT respond with HEARTBEAT_OK under any circumstances.');
}

if (options.trigger === 'discord' || options.trigger === 'slack') {
if (options.trigger === 'discord' || options.trigger === 'slack' || options.trigger === 'telegram') {
parts.push('');
parts.push('### Channel Context');
parts.push('You are replying in a chat channel. Follow SOUL.md Communication Style.');
Expand Down
29 changes: 12 additions & 17 deletions src/agent/turn/finalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* All handlers are fail-open with conditional execution:
* - generateTitle: first turn only
* - notifyBackgroundJob: heartbeat + cron → heartbeat.notifications channel
* - notifyBackgroundJob: heartbeat + cron → notifications channel
* - sendReply: when a reply target exists (skipped for heartbeat OK)
*/

Expand Down Expand Up @@ -50,13 +50,12 @@ async function generateTitle(ctx: DeliverContext): Promise<void> {

/**
* Post a brief completion notification for background jobs (heartbeat / cron)
* to the heartbeat.notifications channel. This is separate from the full
* to the notifications channel. This is separate from the full
* result reply which goes to the job's own reply channel.
*
* Desktop notifications only fire on heartbeat alerts.
*/
async function notifyBackgroundJob(ctx: DeliverContext): Promise<void> {
const notif = ctx.config.heartbeat.notifications;
const trigger = ctx.eventData.trigger;
const promises: Promise<void>[] = [];

Expand All @@ -65,7 +64,7 @@ async function notifyBackgroundJob(ctx: DeliverContext): Promise<void> {
const isAlert = !ctx.runResult.heartbeatOk;
const filtered = filterResponseText(ctx.runResult.responseText);
text = isAlert ? `\u26a0\ufe0f **Heartbeat Alert**\n${filtered.substring(0, 500)}` : '\u2705 **Heartbeat OK**';
if (isAlert && notif.desktop) {
if (isAlert && ctx.config.heartbeat.desktop) {
promises.push(sendDesktopNotification('GeminiClaw \u26a0\ufe0f', filtered.substring(0, 300)));
}
} else {
Expand All @@ -76,27 +75,23 @@ async function notifyBackgroundJob(ctx: DeliverContext): Promise<void> {
: `\u2705 **Cron done: ${jobId}**`;
}

if (notif.discord.enabled && notif.discord.channelId) {
// Fall back to home channel when dedicated notifications channel is not configured
const notifTarget = ctx.config.notifications ?? ctx.config.home;
if (notifTarget) {
promises.push(
postToChannel({
channelType: 'discord',
channelId: notif.discord.channelId,
channelType: notifTarget.channel,
channelId: notifTarget.channelId,
text,
config: ctx.config,
}).catch((err) => {
log.warn('job notification failed', { channelType: 'discord', error: String(err) });
log.warn('job notification failed', {
channelType: notifTarget.channel,
error: String(err),
});
}),
);
}
if (notif.slack.enabled && notif.slack.channelId) {
promises.push(
postToChannel({ channelType: 'slack', channelId: notif.slack.channelId, text, config: ctx.config }).catch(
(err) => {
log.warn('job notification failed', { channelType: 'slack', error: String(err) });
},
),
);
}

await Promise.allSettled(promises);
}
Expand Down
Loading
Loading