refactor: remove legacy container and non-discord remnants
This commit is contained in:
@@ -1,135 +1,46 @@
|
||||
---
|
||||
name: add-compact
|
||||
description: Add /compact command for manual context compaction. Solves context rot in long sessions by forwarding the SDK's built-in /compact slash command. Main-group or trusted sender only.
|
||||
description: Verify or port the existing /compact session command in current NanoClaw.
|
||||
---
|
||||
|
||||
# Add /compact Command
|
||||
|
||||
Adds a `/compact` session command that compacts conversation history to fight context rot in long-running sessions. Uses the Claude Agent SDK's built-in `/compact` slash command — no synthetic system prompts.
|
||||
현재 NanoClaw에는 `/compact`가 이미 들어 있습니다. 이 스킬은 새로 브랜치를 머지하는 용도보다, 오래된 포크에 같은 기능을 옮기거나 리팩터링 뒤 동작을 검증하는 체크리스트로 씁니다.
|
||||
|
||||
**Session contract:** `/compact` keeps the same logical session alive. The SDK returns a new session ID after compaction (via the `init` system message), which the agent-runner forwards to the orchestrator as `newSessionId`. No destructive reset occurs — the agent retains summarized context.
|
||||
## 관련 파일
|
||||
|
||||
## Phase 1: Pre-flight
|
||||
- `src/session-commands.ts` - 명령 파싱, 권한 검사, pre-compact 처리
|
||||
- `src/index.ts` - 메시지 루프에서 세션 명령을 가로채는 호출부
|
||||
- `runners/agent-runner/src/index.ts` - Claude Code 쪽 compaction 처리
|
||||
- `runners/codex-runner/src/index.ts` - Codex 쪽 compaction 처리
|
||||
|
||||
Check if `src/session-commands.ts` exists:
|
||||
|
||||
```bash
|
||||
test -f src/session-commands.ts && echo "Already applied" || echo "Not applied"
|
||||
```
|
||||
|
||||
If already applied, skip to Phase 3 (Verify).
|
||||
|
||||
## Phase 2: Apply Code Changes
|
||||
|
||||
Merge the skill branch:
|
||||
|
||||
```bash
|
||||
git fetch upstream skill/compact
|
||||
git merge upstream/skill/compact
|
||||
```
|
||||
|
||||
> **Note:** `upstream` is the remote pointing to `qwibitai/nanoclaw`. If using a different remote name, substitute accordingly.
|
||||
|
||||
This adds:
|
||||
- `src/session-commands.ts` (extract and authorize session commands)
|
||||
- `src/session-commands.test.ts` (unit tests for command parsing and auth)
|
||||
- Session command interception in `src/index.ts` (both `processGroupMessages` and `startMessageLoop`)
|
||||
- Slash command handling in `container/agent-runner/src/index.ts`
|
||||
|
||||
### Validate
|
||||
## 확인할 동작
|
||||
|
||||
1. `/compact`가 정확히 명령으로 인식되는지
|
||||
2. 메인 그룹 또는 admin/trusted sender만 허용되는지
|
||||
3. `/compact` 전에 들어온 메시지를 먼저 세션에 반영하는지
|
||||
4. compaction 후 세션이 이어지고 `newSessionId`가 갱신되는지
|
||||
5. 대화 아카이브가 `groups/<folder>/conversations/`에 남는지
|
||||
|
||||
## 포팅할 때 원칙
|
||||
|
||||
- 예전 skill branch를 다시 머지하지 말고 현재 트리의 관련 파일을 기준으로 옮깁니다
|
||||
- Claude 러너와 Codex 러너 양쪽을 같이 봅니다
|
||||
- `/clear`와 `/compact`의 의미를 섞지 않습니다
|
||||
|
||||
## 검증
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
npm test
|
||||
npm run build:runners
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Rebuild container
|
||||
그 다음 디스코드에서 확인합니다.
|
||||
|
||||
```bash
|
||||
./container/build.sh
|
||||
```
|
||||
- 메인 채널에서 `/compact`
|
||||
- 비메인 채널에서 일반 사용자 `/compact` 또는 멘션 포함 `/compact`
|
||||
- 비메인 채널에서 admin/trusted sender의 `/compact`
|
||||
|
||||
### Restart service
|
||||
|
||||
```bash
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# Linux: systemctl --user restart nanoclaw
|
||||
```
|
||||
|
||||
## Phase 3: Verify
|
||||
|
||||
### Integration Test
|
||||
|
||||
1. Start NanoClaw in dev mode: `npm run dev`
|
||||
2. From the **main group** (self-chat), send exactly: `/compact`
|
||||
3. Verify:
|
||||
- The agent acknowledges compaction (e.g., "Conversation compacted.")
|
||||
- The session continues — send a follow-up message and verify the agent responds coherently
|
||||
- A conversation archive is written to `groups/{folder}/conversations/` (by the PreCompact hook)
|
||||
- Container logs show `Compact boundary observed` (confirms SDK actually compacted)
|
||||
- If `compact_boundary` was NOT observed, the response says "compact_boundary was not observed"
|
||||
4. From a **non-main group** as a non-admin user, send: `@<assistant> /compact`
|
||||
5. Verify:
|
||||
- The bot responds with "Session commands require admin access."
|
||||
- No compaction occurs, no container is spawned for the command
|
||||
6. From a **non-main group** as the admin (device owner / `is_from_me`), send: `@<assistant> /compact`
|
||||
7. Verify:
|
||||
- Compaction proceeds normally (same behavior as main group)
|
||||
8. While an **active container** is running for the main group, send `/compact`
|
||||
9. Verify:
|
||||
- The active container is signaled to close (authorized senders only — untrusted senders cannot kill in-flight work)
|
||||
- Compaction proceeds via a new container once the active one exits
|
||||
- The command is not dropped (no cursor race)
|
||||
10. Send a normal message, then `/compact`, then another normal message in quick succession (same polling batch):
|
||||
11. Verify:
|
||||
- Pre-compact messages are sent to the agent first (check container logs for two `runAgent` calls)
|
||||
- Compaction proceeds after pre-compact messages are processed
|
||||
- Messages **after** `/compact` in the batch are preserved (cursor advances to `/compact`'s timestamp only) and processed on the next poll cycle
|
||||
12. From a **non-main group** as a non-admin user, send `@<assistant> /compact`:
|
||||
13. Verify:
|
||||
- Denial message is sent ("Session commands require admin access.")
|
||||
- The `/compact` is consumed (cursor advanced) — it does NOT replay on future polls
|
||||
- Other messages in the same batch are also consumed (cursor is a high-water mark — this is an accepted tradeoff for the narrow edge case of denied `/compact` + other messages in the same polling interval)
|
||||
- No container is killed or interrupted
|
||||
14. From a **non-main group** (with `requiresTrigger` enabled) as a non-admin user, send bare `/compact` (no trigger prefix):
|
||||
15. Verify:
|
||||
- No denial message is sent (trigger policy prevents untrusted bot responses)
|
||||
- The `/compact` is consumed silently
|
||||
- Note: in groups where `requiresTrigger` is `false`, a denial message IS sent because the sender is considered reachable
|
||||
16. After compaction, verify **no auto-compaction** behavior — only manual `/compact` triggers it
|
||||
|
||||
### Validation on Fresh Clone
|
||||
|
||||
```bash
|
||||
git clone <your-fork> /tmp/nanoclaw-test
|
||||
cd /tmp/nanoclaw-test
|
||||
claude # then run /add-compact
|
||||
npm run build
|
||||
npm test
|
||||
./container/build.sh
|
||||
# Manual: send /compact from main group, verify compaction + continuation
|
||||
# Manual: send @<assistant> /compact from non-main as non-admin, verify denial
|
||||
# Manual: send @<assistant> /compact from non-main as admin, verify allowed
|
||||
# Manual: verify no auto-compaction behavior
|
||||
```
|
||||
|
||||
## Security Constraints
|
||||
|
||||
- **Main-group or trusted/admin sender only.** The main group is the user's private self-chat and is trusted (see `docs/SECURITY.md`). Non-main groups are untrusted — a careless or malicious user could wipe the agent's short-term memory. However, the device owner (`is_from_me`) is always trusted and can compact from any group.
|
||||
- **No auto-compaction.** This skill implements manual compaction only. Automatic threshold-based compaction is a separate concern and should be a separate skill.
|
||||
- **No config file.** NanoClaw's philosophy is customization through code changes, not configuration sprawl.
|
||||
- **Transcript archived before compaction.** The existing `PreCompact` hook in the agent-runner archives the full transcript to `conversations/` before the SDK compacts it.
|
||||
- **Session continues after compaction.** This is not a destructive reset. The conversation continues with summarized context.
|
||||
|
||||
## What This Does NOT Do
|
||||
|
||||
- No automatic compaction threshold (add separately if desired)
|
||||
- No `/clear` command (separate skill, separate semantics — `/clear` is a destructive reset)
|
||||
- No cross-group compaction (each group's session is isolated)
|
||||
- No changes to the container image, Dockerfile, or build script
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **"Session commands require admin access"**: Only the device owner (`is_from_me`) or main-group senders can use `/compact`. Other users are denied.
|
||||
- **No compact_boundary in logs**: The SDK may not emit this event in all versions. Check the agent-runner logs for the warning message. Compaction may still have succeeded.
|
||||
- **Pre-compact failure**: If messages before `/compact` fail to process, the error message says "Failed to process messages before /compact." The cursor advances past sent output to prevent duplicates; `/compact` remains pending for the next attempt.
|
||||
정상이라면 비권한 사용자는 거부되고, 권한 있는 쪽은 세션이 압축된 뒤 계속 이어집니다.
|
||||
|
||||
@@ -1,212 +1,84 @@
|
||||
---
|
||||
name: add-discord
|
||||
description: Add Discord bot channel integration to NanoClaw.
|
||||
description: Finish Discord bot setup and registration for NanoClaw.
|
||||
---
|
||||
|
||||
# Add Discord Channel
|
||||
|
||||
This skill adds Discord support to NanoClaw, then walks through interactive setup.
|
||||
현재 저장소에는 디스코드 채널 구현이 이미 포함되어 있습니다. 이 스킬은 외부 브랜치를 머지하는 용도가 아니라, 토큰 설정과 채널 등록을 빠르게 끝내는 체크리스트로 사용합니다.
|
||||
|
||||
## Phase 1: Pre-flight
|
||||
## 1. 전제 확인
|
||||
|
||||
### Check if already applied
|
||||
확인할 것:
|
||||
|
||||
Check if `src/channels/discord.ts` exists. If it does, skip to Phase 3 (Setup). The code changes are already in place.
|
||||
- `src/channels/discord.ts`가 존재하는지
|
||||
- `package.json`에 `discord.js`가 있는지
|
||||
- `npm run typecheck`가 통과하는지
|
||||
|
||||
### Ask the user
|
||||
## 2. 봇 토큰 준비
|
||||
|
||||
Use `AskUserQuestion` to collect configuration:
|
||||
토큰이 없다면 디스코드 개발자 포털에서 새 애플리케이션과 봇을 만들고 아래 권한을 켭니다.
|
||||
|
||||
AskUserQuestion: Do you have a Discord bot token, or do you need to create one?
|
||||
- `Message Content Intent`
|
||||
- 필요하면 `Server Members Intent`
|
||||
|
||||
If they have one, collect it now. If not, we'll create one in Phase 3.
|
||||
봇 초대 링크는 `bot` 스코프와 최소 `Send Messages`, `Read Message History`, `View Channels` 권한으로 생성합니다.
|
||||
|
||||
## Phase 2: Apply Code Changes
|
||||
## 3. 환경 변수 설정
|
||||
|
||||
### Ensure channel remote
|
||||
`.env`:
|
||||
|
||||
```bash
|
||||
git remote -v
|
||||
DISCORD_BOT_TOKEN=<token>
|
||||
```
|
||||
|
||||
If `discord` is missing, add it:
|
||||
선택:
|
||||
|
||||
```bash
|
||||
git remote add discord https://github.com/qwibitai/nanoclaw-discord.git
|
||||
DISCORD_CODEX_BOT_TOKEN=<second-token>
|
||||
```
|
||||
|
||||
### Merge the skill branch
|
||||
두 번째 토큰이 있으면 Codex 전용 디스코드 봇도 따로 띄울 수 있습니다.
|
||||
|
||||
## 4. 채널 등록
|
||||
|
||||
디스코드에서 개발자 모드를 켜고 채널 ID를 복사합니다. 등록 JID는 `dc:<channel-id>` 형식입니다.
|
||||
|
||||
메인 채널:
|
||||
|
||||
```bash
|
||||
git fetch discord main
|
||||
git merge discord/main
|
||||
npm run setup -- --step register -- \
|
||||
--jid dc:<channel-id> \
|
||||
--name "<server-name> #<channel-name>" \
|
||||
--folder discord_main \
|
||||
--trigger @Andy \
|
||||
--is-main \
|
||||
--no-trigger-required
|
||||
```
|
||||
|
||||
This merges in:
|
||||
- `src/channels/discord.ts` (DiscordChannel class with self-registration via `registerChannel`)
|
||||
- `src/channels/discord.test.ts` (unit tests with discord.js mock)
|
||||
- `import './discord.js'` appended to the channel barrel file `src/channels/index.ts`
|
||||
- `discord.js` npm dependency in `package.json`
|
||||
- `DISCORD_BOT_TOKEN` in `.env.example`
|
||||
|
||||
If the merge reports conflicts, resolve them by reading the conflicted files and understanding the intent of both sides.
|
||||
|
||||
### Validate code changes
|
||||
보조 채널:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
npx vitest run src/channels/discord.test.ts
|
||||
npm run setup -- --step register -- \
|
||||
--jid dc:<channel-id> \
|
||||
--name "<server-name> #<channel-name>" \
|
||||
--folder discord_<channel-name> \
|
||||
--trigger @Andy
|
||||
```
|
||||
|
||||
All tests must pass (including the new Discord tests) and build must be clean before proceeding.
|
||||
|
||||
## Phase 3: Setup
|
||||
|
||||
### Create Discord Bot (if needed)
|
||||
|
||||
If the user doesn't have a bot token, tell them:
|
||||
|
||||
> I need you to create a Discord bot:
|
||||
>
|
||||
> 1. Go to the [Discord Developer Portal](https://discord.com/developers/applications)
|
||||
> 2. Click **New Application** and give it a name (e.g., "Andy Assistant")
|
||||
> 3. Go to the **Bot** tab on the left sidebar
|
||||
> 4. Click **Reset Token** to generate a new bot token — copy it immediately (you can only see it once)
|
||||
> 5. Under **Privileged Gateway Intents**, enable:
|
||||
> - **Message Content Intent** (required to read message text)
|
||||
> - **Server Members Intent** (optional, for member display names)
|
||||
> 6. Go to **OAuth2** > **URL Generator**:
|
||||
> - Scopes: select `bot`
|
||||
> - Bot Permissions: select `Send Messages`, `Read Message History`, `View Channels`
|
||||
> - Copy the generated URL and open it in your browser to invite the bot to your server
|
||||
|
||||
Wait for the user to provide the token.
|
||||
|
||||
### Configure environment
|
||||
|
||||
Add to `.env`:
|
||||
|
||||
```bash
|
||||
DISCORD_BOT_TOKEN=<their-token>
|
||||
```
|
||||
|
||||
Channels auto-enable when their credentials are present — no extra configuration needed.
|
||||
|
||||
Sync to container environment:
|
||||
|
||||
```bash
|
||||
mkdir -p data/env && cp .env data/env/env
|
||||
```
|
||||
|
||||
The container reads environment from `data/env/env`, not `.env` directly.
|
||||
|
||||
### Build and restart
|
||||
## 5. 빌드와 검증
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
|
||||
npm run build:runners
|
||||
npm run setup -- --step service
|
||||
npm run setup -- --step verify
|
||||
```
|
||||
|
||||
## Phase 4: Registration
|
||||
|
||||
### Get Channel ID
|
||||
|
||||
Tell the user:
|
||||
|
||||
> To get the channel ID for registration:
|
||||
>
|
||||
> 1. In Discord, go to **User Settings** > **Advanced** > Enable **Developer Mode**
|
||||
> 2. Right-click the text channel you want the bot to respond in
|
||||
> 3. Click **Copy Channel ID**
|
||||
>
|
||||
> The channel ID will be a long number like `1234567890123456`.
|
||||
|
||||
Wait for the user to provide the channel ID (format: `dc:1234567890123456`).
|
||||
|
||||
### Register the channel
|
||||
|
||||
Use the IPC register flow or register directly. The channel ID, name, and folder name are needed.
|
||||
|
||||
For a main channel (responds to all messages):
|
||||
|
||||
```typescript
|
||||
registerGroup("dc:<channel-id>", {
|
||||
name: "<server-name> #<channel-name>",
|
||||
folder: "discord_main",
|
||||
trigger: `@${ASSISTANT_NAME}`,
|
||||
added_at: new Date().toISOString(),
|
||||
requiresTrigger: false,
|
||||
isMain: true,
|
||||
});
|
||||
```
|
||||
|
||||
For additional channels (trigger-only):
|
||||
|
||||
```typescript
|
||||
registerGroup("dc:<channel-id>", {
|
||||
name: "<server-name> #<channel-name>",
|
||||
folder: "discord_<channel-name>",
|
||||
trigger: `@${ASSISTANT_NAME}`,
|
||||
added_at: new Date().toISOString(),
|
||||
requiresTrigger: true,
|
||||
});
|
||||
```
|
||||
|
||||
## Phase 5: Verify
|
||||
|
||||
### Test the connection
|
||||
|
||||
Tell the user:
|
||||
|
||||
> Send a message in your registered Discord channel:
|
||||
> - For main channel: Any message works
|
||||
> - For non-main: @mention the bot in Discord
|
||||
>
|
||||
> The bot should respond within a few seconds.
|
||||
|
||||
### Check logs if needed
|
||||
필요하면 로그 확인:
|
||||
|
||||
```bash
|
||||
tail -f logs/nanoclaw.log
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Bot not responding
|
||||
|
||||
1. Check `DISCORD_BOT_TOKEN` is set in `.env` AND synced to `data/env/env`
|
||||
2. Check channel is registered: `sqlite3 store/messages.db "SELECT * FROM registered_groups WHERE jid LIKE 'dc:%'"`
|
||||
3. For non-main channels: message must include trigger pattern (@mention the bot)
|
||||
4. Service is running: `launchctl list | grep nanoclaw`
|
||||
5. Verify the bot has been invited to the server (check OAuth2 URL was used)
|
||||
|
||||
### Bot only responds to @mentions
|
||||
|
||||
This is the default behavior for non-main channels (`requiresTrigger: true`). To change:
|
||||
- Update the registered group's `requiresTrigger` to `false`
|
||||
- Or register the channel as the main channel
|
||||
|
||||
### Message Content Intent not enabled
|
||||
|
||||
If the bot connects but can't read messages, ensure:
|
||||
1. Go to [Discord Developer Portal](https://discord.com/developers/applications)
|
||||
2. Select your application > **Bot** tab
|
||||
3. Under **Privileged Gateway Intents**, enable **Message Content Intent**
|
||||
4. Restart NanoClaw
|
||||
|
||||
### Getting Channel ID
|
||||
|
||||
If you can't copy the channel ID:
|
||||
- Ensure **Developer Mode** is enabled: User Settings > Advanced > Developer Mode
|
||||
- Right-click the channel name in the server sidebar > Copy Channel ID
|
||||
|
||||
## After Setup
|
||||
|
||||
The Discord bot supports:
|
||||
- Text messages in registered channels
|
||||
- Attachment descriptions (images, videos, files shown as placeholders)
|
||||
- Reply context (shows who the user is replying to)
|
||||
- @mention translation (Discord `<@botId>` → NanoClaw trigger format)
|
||||
- Message splitting for responses over 2000 characters
|
||||
- Typing indicators while the agent processes
|
||||
메인 채널이면 일반 메시지, 보조 채널이면 멘션 또는 트리거 메시지로 테스트합니다.
|
||||
|
||||
@@ -1,216 +0,0 @@
|
||||
---
|
||||
name: add-gmail
|
||||
description: Add Gmail integration to NanoClaw. Can be configured as a tool (agent reads/sends emails when triggered from WhatsApp) or as a full channel (emails can trigger the agent, schedule tasks, and receive replies). Guides through GCP OAuth setup and implements the integration.
|
||||
---
|
||||
|
||||
# Add Gmail Integration
|
||||
|
||||
This skill adds Gmail support to NanoClaw — either as a tool (read, send, search, draft) or as a full channel that polls the inbox.
|
||||
|
||||
## Phase 1: Pre-flight
|
||||
|
||||
### Check if already applied
|
||||
|
||||
Check if `src/channels/gmail.ts` exists. If it does, skip to Phase 3 (Setup). The code changes are already in place.
|
||||
|
||||
### Ask the user
|
||||
|
||||
Use `AskUserQuestion`:
|
||||
|
||||
AskUserQuestion: Should incoming emails be able to trigger the agent?
|
||||
|
||||
- **Yes** — Full channel mode: the agent listens on Gmail and responds to incoming emails automatically
|
||||
- **No** — Tool-only: the agent gets full Gmail tools (read, send, search, draft) but won't monitor the inbox. No channel code is added.
|
||||
|
||||
## Phase 2: Apply Code Changes
|
||||
|
||||
### Ensure channel remote
|
||||
|
||||
```bash
|
||||
git remote -v
|
||||
```
|
||||
|
||||
If `gmail` is missing, add it:
|
||||
|
||||
```bash
|
||||
git remote add gmail https://github.com/qwibitai/nanoclaw-gmail.git
|
||||
```
|
||||
|
||||
### Merge the skill branch
|
||||
|
||||
```bash
|
||||
git fetch gmail main
|
||||
git merge gmail/main
|
||||
```
|
||||
|
||||
This merges in:
|
||||
- `src/channels/gmail.ts` (GmailChannel class with self-registration via `registerChannel`)
|
||||
- `src/channels/gmail.test.ts` (unit tests)
|
||||
- `import './gmail.js'` appended to the channel barrel file `src/channels/index.ts`
|
||||
- Gmail credentials mount (`~/.gmail-mcp`) in `src/container-runner.ts`
|
||||
- Gmail MCP server (`@gongrzhe/server-gmail-autoauth-mcp`) and `mcp__gmail__*` allowed tool in `container/agent-runner/src/index.ts`
|
||||
- `googleapis` npm dependency in `package.json`
|
||||
|
||||
If the merge reports conflicts, resolve them by reading the conflicted files and understanding the intent of both sides.
|
||||
|
||||
### Add email handling instructions (Channel mode only)
|
||||
|
||||
If the user chose channel mode, append the following to `groups/main/CLAUDE.md` (before the formatting section):
|
||||
|
||||
```markdown
|
||||
## Email Notifications
|
||||
|
||||
When you receive an email notification (messages starting with `[Email from ...`), inform the user about it but do NOT reply to the email unless specifically asked. You have Gmail tools available — use them only when the user explicitly asks you to reply, forward, or take action on an email.
|
||||
```
|
||||
|
||||
### Validate code changes
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
npx vitest run src/channels/gmail.test.ts
|
||||
```
|
||||
|
||||
All tests must pass (including the new Gmail tests) and build must be clean before proceeding.
|
||||
|
||||
## Phase 3: Setup
|
||||
|
||||
### Check existing Gmail credentials
|
||||
|
||||
```bash
|
||||
ls -la ~/.gmail-mcp/ 2>/dev/null || echo "No Gmail config found"
|
||||
```
|
||||
|
||||
If `credentials.json` already exists, skip to "Build and restart" below.
|
||||
|
||||
### GCP Project Setup
|
||||
|
||||
Tell the user:
|
||||
|
||||
> I need you to set up Google Cloud OAuth credentials:
|
||||
>
|
||||
> 1. Open https://console.cloud.google.com — create a new project or select existing
|
||||
> 2. Go to **APIs & Services > Library**, search "Gmail API", click **Enable**
|
||||
> 3. Go to **APIs & Services > Credentials**, click **+ CREATE CREDENTIALS > OAuth client ID**
|
||||
> - If prompted for consent screen: choose "External", fill in app name and email, save
|
||||
> - Application type: **Desktop app**, name: anything (e.g., "NanoClaw Gmail")
|
||||
> 4. Click **DOWNLOAD JSON** and save as `gcp-oauth.keys.json`
|
||||
>
|
||||
> Where did you save the file? (Give me the full path, or paste the file contents here)
|
||||
|
||||
If user provides a path, copy it:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.gmail-mcp
|
||||
cp "/path/user/provided/gcp-oauth.keys.json" ~/.gmail-mcp/gcp-oauth.keys.json
|
||||
```
|
||||
|
||||
If user pastes JSON content, write it to `~/.gmail-mcp/gcp-oauth.keys.json`.
|
||||
|
||||
### OAuth Authorization
|
||||
|
||||
Tell the user:
|
||||
|
||||
> I'm going to run Gmail authorization. A browser window will open — sign in and grant access. If you see an "app isn't verified" warning, click "Advanced" then "Go to [app name] (unsafe)" — this is normal for personal OAuth apps.
|
||||
|
||||
Run the authorization:
|
||||
|
||||
```bash
|
||||
npx -y @gongrzhe/server-gmail-autoauth-mcp auth
|
||||
```
|
||||
|
||||
If that fails (some versions don't have an auth subcommand), try `timeout 60 npx -y @gongrzhe/server-gmail-autoauth-mcp || true`. Verify with `ls ~/.gmail-mcp/credentials.json`.
|
||||
|
||||
### Build and restart
|
||||
|
||||
Clear stale per-group agent-runner copies (they only get re-created if missing, so existing copies won't pick up the new Gmail server):
|
||||
|
||||
```bash
|
||||
rm -r data/sessions/*/agent-runner-src 2>/dev/null || true
|
||||
```
|
||||
|
||||
Rebuild the container (agent-runner changed):
|
||||
|
||||
```bash
|
||||
cd container && ./build.sh
|
||||
```
|
||||
|
||||
Then compile and restart:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# Linux: systemctl --user restart nanoclaw
|
||||
```
|
||||
|
||||
## Phase 4: Verify
|
||||
|
||||
### Test tool access (both modes)
|
||||
|
||||
Tell the user:
|
||||
|
||||
> Gmail is connected! Send this in your main channel:
|
||||
>
|
||||
> `@Andy check my recent emails` or `@Andy list my Gmail labels`
|
||||
|
||||
### Test channel mode (Channel mode only)
|
||||
|
||||
Tell the user to send themselves a test email. The agent should pick it up within a minute. Monitor: `tail -f logs/nanoclaw.log | grep -iE "(gmail|email)"`.
|
||||
|
||||
Once verified, offer filter customization via `AskUserQuestion` — by default, only emails in the Primary inbox trigger the agent (Promotions, Social, Updates, and Forums are excluded). The user can keep this default or narrow further by sender, label, or keywords. No code changes needed for filters.
|
||||
|
||||
### Check logs if needed
|
||||
|
||||
```bash
|
||||
tail -f logs/nanoclaw.log
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Gmail connection not responding
|
||||
|
||||
Test directly:
|
||||
|
||||
```bash
|
||||
npx -y @gongrzhe/server-gmail-autoauth-mcp
|
||||
```
|
||||
|
||||
### OAuth token expired
|
||||
|
||||
Re-authorize:
|
||||
|
||||
```bash
|
||||
rm ~/.gmail-mcp/credentials.json
|
||||
npx -y @gongrzhe/server-gmail-autoauth-mcp
|
||||
```
|
||||
|
||||
### Container can't access Gmail
|
||||
|
||||
- Verify `~/.gmail-mcp` is mounted: check `src/container-runner.ts` for the `.gmail-mcp` mount
|
||||
- Check container logs: `cat groups/main/logs/container-*.log | tail -50`
|
||||
|
||||
### Emails not being detected (Channel mode only)
|
||||
|
||||
- By default, the channel polls unread Primary inbox emails (`is:unread category:primary`)
|
||||
- Check logs for Gmail polling errors
|
||||
|
||||
## Removal
|
||||
|
||||
### Tool-only mode
|
||||
|
||||
1. Remove `~/.gmail-mcp` mount from `src/container-runner.ts`
|
||||
2. Remove `gmail` MCP server and `mcp__gmail__*` from `container/agent-runner/src/index.ts`
|
||||
3. Rebuild and restart
|
||||
4. Clear stale agent-runner copies: `rm -r data/sessions/*/agent-runner-src 2>/dev/null || true`
|
||||
5. Rebuild: `cd container && ./build.sh && cd .. && npm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `systemctl --user restart nanoclaw` (Linux)
|
||||
|
||||
### Channel mode
|
||||
|
||||
1. Delete `src/channels/gmail.ts` and `src/channels/gmail.test.ts`
|
||||
2. Remove `import './gmail.js'` from `src/channels/index.ts`
|
||||
3. Remove `~/.gmail-mcp` mount from `src/container-runner.ts`
|
||||
4. Remove `gmail` MCP server and `mcp__gmail__*` from `container/agent-runner/src/index.ts`
|
||||
5. Uninstall: `npm uninstall googleapis`
|
||||
6. Rebuild and restart
|
||||
7. Clear stale agent-runner copies: `rm -r data/sessions/*/agent-runner-src 2>/dev/null || true`
|
||||
8. Rebuild: `cd container && ./build.sh && cd .. && npm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `systemctl --user restart nanoclaw` (Linux)
|
||||
@@ -1,90 +0,0 @@
|
||||
---
|
||||
name: add-image-vision
|
||||
description: Add image vision to NanoClaw agents. Resizes and processes WhatsApp image attachments, then sends them to Claude as multimodal content blocks.
|
||||
---
|
||||
|
||||
# Image Vision Skill
|
||||
|
||||
Adds the ability for NanoClaw agents to see and understand images sent via WhatsApp. Images are downloaded, resized with sharp, saved to the group workspace, and passed to the agent as base64-encoded multimodal content blocks.
|
||||
|
||||
## Phase 1: Pre-flight
|
||||
|
||||
1. Check if `src/image.ts` exists — skip to Phase 3 if already applied
|
||||
2. Confirm `sharp` is installable (native bindings require build tools)
|
||||
|
||||
**Prerequisite:** WhatsApp must be installed first (`skill/whatsapp` merged). This skill modifies WhatsApp channel files.
|
||||
|
||||
## Phase 2: Apply Code Changes
|
||||
|
||||
### Ensure WhatsApp fork remote
|
||||
|
||||
```bash
|
||||
git remote -v
|
||||
```
|
||||
|
||||
If `whatsapp` is missing, add it:
|
||||
|
||||
```bash
|
||||
git remote add whatsapp https://github.com/qwibitai/nanoclaw-whatsapp.git
|
||||
```
|
||||
|
||||
### Merge the skill branch
|
||||
|
||||
```bash
|
||||
git fetch whatsapp skill/image-vision
|
||||
git merge whatsapp/skill/image-vision
|
||||
```
|
||||
|
||||
This merges in:
|
||||
- `src/image.ts` (image download, resize via sharp, base64 encoding)
|
||||
- `src/image.test.ts` (8 unit tests)
|
||||
- Image attachment handling in `src/channels/whatsapp.ts`
|
||||
- Image passing to agent in `src/index.ts` and `src/container-runner.ts`
|
||||
- Image content block support in `container/agent-runner/src/index.ts`
|
||||
- `sharp` npm dependency in `package.json`
|
||||
|
||||
If the merge reports conflicts, resolve them by reading the conflicted files and understanding the intent of both sides.
|
||||
|
||||
### Validate code changes
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
npx vitest run src/image.test.ts
|
||||
```
|
||||
|
||||
All tests must pass and build must be clean before proceeding.
|
||||
|
||||
## Phase 3: Configure
|
||||
|
||||
1. Rebuild the container (agent-runner changes need a rebuild):
|
||||
```bash
|
||||
./container/build.sh
|
||||
```
|
||||
|
||||
2. Sync agent-runner source to group caches:
|
||||
```bash
|
||||
for dir in data/sessions/*/agent-runner-src/; do
|
||||
cp container/agent-runner/src/*.ts "$dir"
|
||||
done
|
||||
```
|
||||
|
||||
3. Restart the service:
|
||||
```bash
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
|
||||
```
|
||||
|
||||
## Phase 4: Verify
|
||||
|
||||
1. Send an image in a registered WhatsApp group
|
||||
2. Check the agent responds with understanding of the image content
|
||||
3. Check logs for "Processed image attachment":
|
||||
```bash
|
||||
tail -50 groups/*/logs/container-*.log
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **"Image - download failed"**: Check WhatsApp connection stability. The download may timeout on slow connections.
|
||||
- **"Image - processing failed"**: Sharp may not be installed correctly. Run `npm ls sharp` to verify.
|
||||
- **Agent doesn't mention image content**: Check container logs for "Loaded image" messages. If missing, ensure agent-runner source was synced to group caches.
|
||||
@@ -1,153 +1,84 @@
|
||||
---
|
||||
name: add-ollama-tool
|
||||
description: Add Ollama MCP server so the container agent can call local models for cheaper/faster tasks like summarization, translation, or general queries.
|
||||
description: Add an Ollama MCP tool to the current host-process Claude runner.
|
||||
---
|
||||
|
||||
# Add Ollama Integration
|
||||
# Add Ollama Tool
|
||||
|
||||
This skill adds a stdio-based MCP server that exposes local Ollama models as tools for the container agent. Claude remains the orchestrator but can offload work to local models.
|
||||
현재 구조에서는 컨테이너를 건드리지 않습니다. Ollama 연동은 호스트에서 실행되는 Claude 러너에 MCP를 추가하는 방식으로 넣습니다.
|
||||
|
||||
Tools added:
|
||||
- `ollama_list_models` — lists installed Ollama models
|
||||
- `ollama_generate` — sends a prompt to a specified model and returns the response
|
||||
|
||||
## Phase 1: Pre-flight
|
||||
|
||||
### Check if already applied
|
||||
|
||||
Check if `container/agent-runner/src/ollama-mcp-stdio.ts` exists. If it does, skip to Phase 3 (Configure).
|
||||
|
||||
### Check prerequisites
|
||||
|
||||
Verify Ollama is installed and running on the host:
|
||||
## 전제 조건
|
||||
|
||||
```bash
|
||||
ollama list
|
||||
```
|
||||
|
||||
If Ollama is not installed, direct the user to https://ollama.com/download.
|
||||
- Ollama가 설치되어 있어야 합니다.
|
||||
- 최소 한 개 이상의 모델이 있어야 합니다.
|
||||
|
||||
If no models are installed, suggest pulling one:
|
||||
|
||||
> You need at least one model. I recommend:
|
||||
>
|
||||
> ```bash
|
||||
> ollama pull gemma3:1b # Small, fast (1GB)
|
||||
> ollama pull llama3.2 # Good general purpose (2GB)
|
||||
> ollama pull qwen3-coder:30b # Best for code tasks (18GB)
|
||||
> ```
|
||||
|
||||
## Phase 2: Apply Code Changes
|
||||
|
||||
### Ensure upstream remote
|
||||
권장 예시:
|
||||
|
||||
```bash
|
||||
git remote -v
|
||||
ollama pull gemma3:1b
|
||||
ollama pull llama3.2
|
||||
```
|
||||
|
||||
If `upstream` is missing, add it:
|
||||
|
||||
```bash
|
||||
git remote add upstream https://github.com/qwibitai/nanoclaw.git
|
||||
```
|
||||
|
||||
### Merge the skill branch
|
||||
|
||||
```bash
|
||||
git fetch upstream skill/ollama-tool
|
||||
git merge upstream/skill/ollama-tool
|
||||
```
|
||||
|
||||
This merges in:
|
||||
- `container/agent-runner/src/ollama-mcp-stdio.ts` (Ollama MCP server)
|
||||
- `scripts/ollama-watch.sh` (macOS notification watcher)
|
||||
- Ollama MCP config in `container/agent-runner/src/index.ts` (allowedTools + mcpServers)
|
||||
- `[OLLAMA]` log surfacing in `src/container-runner.ts`
|
||||
- `OLLAMA_HOST` in `.env.example`
|
||||
|
||||
If the merge reports conflicts, resolve them by reading the conflicted files and understanding the intent of both sides.
|
||||
|
||||
### Copy to per-group agent-runner
|
||||
|
||||
Existing groups have a cached copy of the agent-runner source. Copy the new files:
|
||||
|
||||
```bash
|
||||
for dir in data/sessions/*/agent-runner-src; do
|
||||
cp container/agent-runner/src/ollama-mcp-stdio.ts "$dir/"
|
||||
cp container/agent-runner/src/index.ts "$dir/"
|
||||
done
|
||||
```
|
||||
|
||||
### Validate code changes
|
||||
## 수정 지점
|
||||
|
||||
### 1. 환경 변수 전달
|
||||
|
||||
`src/agent-runner.ts`
|
||||
|
||||
- `.env`에서 `OLLAMA_HOST`를 읽도록 `readEnvFile([...])` 목록에 추가합니다.
|
||||
- Claude 러너 child env에 `OLLAMA_HOST`를 명시적으로 넣습니다.
|
||||
|
||||
기본값을 쓰면 생략 가능하지만, 원격 Ollama나 다른 포트를 쓸 때는 필요합니다.
|
||||
|
||||
## 2. MCP 서버 추가
|
||||
|
||||
`runners/agent-runner/src/index.ts`
|
||||
|
||||
- 호스트에서 동작하는 stdio MCP 서버 파일을 추가합니다.
|
||||
- 예: `runners/agent-runner/src/ollama-mcp-stdio.ts`
|
||||
- `query({ options: { mcpServers, allowedTools } })` 쪽에 Ollama 서버를 등록합니다.
|
||||
- `allowedTools`에 `mcp__ollama__*` 또는 실제 툴 prefix를 추가합니다.
|
||||
|
||||
핵심은 두 가지입니다.
|
||||
|
||||
- Claude가 Ollama를 CLI가 아니라 MCP 도구로 보게 만들 것
|
||||
- 툴 이름이 `allowedTools`에 포함될 것
|
||||
|
||||
## 3. 프롬프트 문서화
|
||||
|
||||
`groups/global/CLAUDE.md` 또는 대상 그룹의 `CLAUDE.md`
|
||||
|
||||
- 언제 Ollama를 쓰는지
|
||||
- 어떤 모델을 우선 쓰는지
|
||||
- 빠른 요약/분류/초안 작업에 먼저 쓰도록 할지
|
||||
|
||||
이 부분을 적어두지 않으면 에이전트가 도구를 잘 안 고릅니다.
|
||||
|
||||
## 4. 빌드와 검증
|
||||
|
||||
```bash
|
||||
npm run build:runners
|
||||
npm run build
|
||||
./container/build.sh
|
||||
npm run setup -- --step service
|
||||
```
|
||||
|
||||
Build must be clean before proceeding.
|
||||
디스코드에서 테스트:
|
||||
|
||||
## Phase 3: Configure
|
||||
> `ollama 도구를 써서 이 문단을 3줄로 요약해줘`
|
||||
|
||||
### Set Ollama host (optional)
|
||||
## 문제 확인
|
||||
|
||||
By default, the MCP server connects to `http://host.docker.internal:11434` (Docker Desktop) with a fallback to `localhost`. To use a custom Ollama host, add to `.env`:
|
||||
### 에이전트가 `ollama` CLI를 직접 치려고 함
|
||||
|
||||
```bash
|
||||
OLLAMA_HOST=http://your-ollama-host:11434
|
||||
```
|
||||
- MCP 서버 등록이 빠졌거나
|
||||
- `allowedTools`에 툴 prefix가 없거나
|
||||
- 프롬프트 문서에 사용 규칙이 없습니다
|
||||
|
||||
### Restart the service
|
||||
### 연결 실패
|
||||
|
||||
```bash
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# Linux: systemctl --user restart nanoclaw
|
||||
```
|
||||
|
||||
## Phase 4: Verify
|
||||
|
||||
### Test via WhatsApp
|
||||
|
||||
Tell the user:
|
||||
|
||||
> Send a message like: "use ollama to tell me the capital of France"
|
||||
>
|
||||
> The agent should use `ollama_list_models` to find available models, then `ollama_generate` to get a response.
|
||||
|
||||
### Monitor activity (optional)
|
||||
|
||||
Run the watcher script for macOS notifications when Ollama is used:
|
||||
|
||||
```bash
|
||||
./scripts/ollama-watch.sh
|
||||
```
|
||||
|
||||
### Check logs if needed
|
||||
|
||||
```bash
|
||||
tail -f logs/nanoclaw.log | grep -i ollama
|
||||
```
|
||||
|
||||
Look for:
|
||||
- `Agent output: ... Ollama ...` — agent used Ollama successfully
|
||||
- `[OLLAMA] >>> Generating` — generation started (if log surfacing works)
|
||||
- `[OLLAMA] <<< Done` — generation completed
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Agent says "Ollama is not installed"
|
||||
|
||||
The agent is trying to run `ollama` CLI inside the container instead of using the MCP tools. This means:
|
||||
1. The MCP server wasn't registered — check `container/agent-runner/src/index.ts` has the `ollama` entry in `mcpServers`
|
||||
2. The per-group source wasn't updated — re-copy files (see Phase 2)
|
||||
3. The container wasn't rebuilt — run `./container/build.sh`
|
||||
|
||||
### "Failed to connect to Ollama"
|
||||
|
||||
1. Verify Ollama is running: `ollama list`
|
||||
2. Check Docker can reach the host: `docker run --rm curlimages/curl curl -s http://host.docker.internal:11434/api/tags`
|
||||
3. If using a custom host, check `OLLAMA_HOST` in `.env`
|
||||
|
||||
### Agent doesn't use Ollama tools
|
||||
|
||||
The agent may not know about the tools. Try being explicit: "use the ollama_generate tool with gemma3:1b to answer: ..."
|
||||
- `ollama list`가 호스트에서 되는지 먼저 확인합니다
|
||||
- 원격 호스트면 `.env`의 `OLLAMA_HOST`를 확인합니다
|
||||
|
||||
@@ -1,290 +1,73 @@
|
||||
---
|
||||
name: add-parallel
|
||||
description: Add Parallel AI MCP tools to the current host-process Claude runner.
|
||||
---
|
||||
|
||||
# Add Parallel AI Integration
|
||||
|
||||
Adds Parallel AI MCP integration to NanoClaw for advanced web research capabilities.
|
||||
Parallel AI는 현재 구조에서도 유효한 도구 연동입니다. 다만 예전 문서처럼 컨테이너를 수정하지 않고, Claude 러너의 MCP 설정만 바꿉니다.
|
||||
|
||||
## What This Adds
|
||||
## 1. API 키 준비
|
||||
|
||||
- **Quick Search** - Fast web lookups using Parallel Search API (free to use)
|
||||
- **Deep Research** - Comprehensive analysis using Parallel Task API (asks permission)
|
||||
- **Non-blocking Design** - Uses NanoClaw scheduler for result polling (no container blocking)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
User must have:
|
||||
1. Parallel AI API key from https://platform.parallel.ai
|
||||
2. NanoClaw already set up and running
|
||||
3. Docker installed and running
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
Run all steps automatically. Only pause for user input when explicitly needed.
|
||||
|
||||
### 1. Get Parallel AI API Key
|
||||
|
||||
Use `AskUserQuestion: Do you have a Parallel AI API key, or should I help you get one?`
|
||||
|
||||
**If they have one:**
|
||||
Collect it now.
|
||||
|
||||
**If they need one:**
|
||||
Tell them:
|
||||
> 1. Go to https://platform.parallel.ai
|
||||
> 2. Sign up or log in
|
||||
> 3. Navigate to API Keys section
|
||||
> 4. Create a new API key
|
||||
> 5. Copy the key and paste it here
|
||||
|
||||
Wait for the API key.
|
||||
|
||||
### 2. Add API Key to Environment
|
||||
|
||||
Add `PARALLEL_API_KEY` to `.env`:
|
||||
`PARALLEL_API_KEY`를 `.env`에 넣습니다.
|
||||
|
||||
```bash
|
||||
# Check if .env exists, create if not
|
||||
if [ ! -f .env ]; then
|
||||
touch .env
|
||||
fi
|
||||
|
||||
# Add PARALLEL_API_KEY if not already present
|
||||
if ! grep -q "PARALLEL_API_KEY=" .env; then
|
||||
echo "PARALLEL_API_KEY=${API_KEY_FROM_USER}" >> .env
|
||||
echo "✓ Added PARALLEL_API_KEY to .env"
|
||||
else
|
||||
# Update existing key
|
||||
sed -i.bak "s/^PARALLEL_API_KEY=.*/PARALLEL_API_KEY=${API_KEY_FROM_USER}/" .env
|
||||
echo "✓ Updated PARALLEL_API_KEY in .env"
|
||||
fi
|
||||
PARALLEL_API_KEY=...
|
||||
```
|
||||
|
||||
Verify:
|
||||
```bash
|
||||
grep "PARALLEL_API_KEY" .env | head -c 50
|
||||
```
|
||||
## 2. 환경 변수 전달
|
||||
|
||||
### 3. Update Container Runner
|
||||
`src/agent-runner.ts`
|
||||
|
||||
Add `PARALLEL_API_KEY` to allowed environment variables in `src/container-runner.ts`:
|
||||
- `.env`에서 `PARALLEL_API_KEY`를 읽도록 `readEnvFile([...])` 목록에 추가합니다.
|
||||
- Claude 러너 child env에 `PARALLEL_API_KEY`를 넣습니다.
|
||||
|
||||
Find the line:
|
||||
```typescript
|
||||
const allowedVars = ['CLAUDE_CODE_OAUTH_TOKEN', 'ANTHROPIC_API_KEY'];
|
||||
```
|
||||
## 3. MCP 서버 등록
|
||||
|
||||
Replace with:
|
||||
```typescript
|
||||
const allowedVars = ['CLAUDE_CODE_OAUTH_TOKEN', 'ANTHROPIC_API_KEY', 'PARALLEL_API_KEY'];
|
||||
```
|
||||
`runners/agent-runner/src/index.ts`
|
||||
|
||||
### 4. Configure MCP Servers in Agent Runner
|
||||
- `query({ options: { mcpServers } })`에 Parallel HTTP MCP 서버를 추가합니다.
|
||||
- `allowedTools`에 `mcp__parallel-search__*`, `mcp__parallel-task__*`를 추가합니다.
|
||||
|
||||
Update `container/agent-runner/src/index.ts`:
|
||||
구분은 이렇게 가져가는 편이 안전합니다.
|
||||
|
||||
Find the section where `mcpServers` is configured (around line 237-252):
|
||||
```typescript
|
||||
const mcpServers: Record<string, any> = {
|
||||
nanoclaw: ipcMcp
|
||||
};
|
||||
```
|
||||
- `parallel-search`: 빠른 검색, 저비용, 기본 허용
|
||||
- `parallel-task`: 긴 조사 작업, 비용/시간이 더 큼, 명시적 승인 후 사용
|
||||
|
||||
Add Parallel AI MCP servers after the nanoclaw server:
|
||||
```typescript
|
||||
const mcpServers: Record<string, any> = {
|
||||
nanoclaw: ipcMcp
|
||||
};
|
||||
## 4. 프롬프트 규칙 추가
|
||||
|
||||
// Add Parallel AI MCP servers if API key is available
|
||||
const parallelApiKey = process.env.PARALLEL_API_KEY;
|
||||
if (parallelApiKey) {
|
||||
mcpServers['parallel-search'] = {
|
||||
type: 'http', // REQUIRED: Must specify type for HTTP MCP servers
|
||||
url: 'https://search-mcp.parallel.ai/mcp',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${parallelApiKey}`
|
||||
}
|
||||
};
|
||||
mcpServers['parallel-task'] = {
|
||||
type: 'http', // REQUIRED: Must specify type for HTTP MCP servers
|
||||
url: 'https://task-mcp.parallel.ai/mcp',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${parallelApiKey}`
|
||||
}
|
||||
};
|
||||
log('Parallel AI MCP servers configured');
|
||||
} else {
|
||||
log('PARALLEL_API_KEY not set, skipping Parallel AI integration');
|
||||
}
|
||||
```
|
||||
`groups/global/CLAUDE.md` 또는 대상 그룹의 `CLAUDE.md`
|
||||
|
||||
Also update the `allowedTools` array to include Parallel MCP tools (around line 242-248):
|
||||
```typescript
|
||||
allowedTools: [
|
||||
'Bash',
|
||||
'Read', 'Write', 'Edit', 'Glob', 'Grep',
|
||||
'WebSearch', 'WebFetch',
|
||||
'mcp__nanoclaw__*',
|
||||
'mcp__parallel-search__*',
|
||||
'mcp__parallel-task__*'
|
||||
],
|
||||
```
|
||||
아래 원칙을 문서화합니다.
|
||||
|
||||
### 5. Add Usage Instructions to CLAUDE.md
|
||||
- 빠른 사실 조회나 최신 정보 확인은 `parallel-search`
|
||||
- 긴 조사나 깊은 분석은 `parallel-task`
|
||||
- `parallel-task`는 항상 먼저 사용자 허가를 받을 것
|
||||
- 오래 걸리는 작업은 답변을 붙잡고 있지 말고 `mcp__nanoclaw__schedule_task`로 후속 체크를 맡길 것
|
||||
|
||||
Add Parallel AI usage instructions to `groups/main/CLAUDE.md`:
|
||||
|
||||
Find the "## What You Can Do" section and add after the existing bullet points:
|
||||
```markdown
|
||||
- Use Parallel AI for web research and deep learning tasks
|
||||
```
|
||||
|
||||
Then add a new section after "## What You Can Do":
|
||||
```markdown
|
||||
## Web Research Tools
|
||||
|
||||
You have access to two Parallel AI research tools:
|
||||
|
||||
### Quick Web Search (`mcp__parallel-search__search`)
|
||||
**When to use:** Freely use for factual lookups, current events, definitions, recent information, or verifying facts.
|
||||
|
||||
**Examples:**
|
||||
- "Who invented the transistor?"
|
||||
- "What's the latest news about quantum computing?"
|
||||
- "When was the UN founded?"
|
||||
- "What are the top programming languages in 2026?"
|
||||
|
||||
**Speed:** Fast (2-5 seconds)
|
||||
**Cost:** Low
|
||||
**Permission:** Not needed - use whenever it helps answer the question
|
||||
|
||||
### Deep Research (`mcp__parallel-task__create_task_run`)
|
||||
**When to use:** Comprehensive analysis, learning about complex topics, comparing concepts, historical overviews, or structured research.
|
||||
|
||||
**Examples:**
|
||||
- "Explain the development of quantum mechanics from 1900-1930"
|
||||
- "Compare the literary styles of Hemingway and Faulkner"
|
||||
- "Research the evolution of jazz from bebop to fusion"
|
||||
- "Analyze the causes of the French Revolution"
|
||||
|
||||
**Speed:** Slower (1-20 minutes depending on depth)
|
||||
**Cost:** Higher (varies by processor tier)
|
||||
**Permission:** ALWAYS use `AskUserQuestion` before using this tool
|
||||
|
||||
**How to ask permission:**
|
||||
```
|
||||
AskUserQuestion: I can do deep research on [topic] using Parallel's Task API. This will take 2-5 minutes and provide comprehensive analysis with citations. Should I proceed?
|
||||
```
|
||||
|
||||
**After permission - DO NOT BLOCK! Use scheduler instead:**
|
||||
|
||||
1. Create the task using `mcp__parallel-task__create_task_run`
|
||||
2. Get the `run_id` from the response
|
||||
3. Create a polling scheduled task using `mcp__nanoclaw__schedule_task`:
|
||||
```
|
||||
Prompt: "Check Parallel AI task run [run_id] and send results when ready.
|
||||
|
||||
1. Use the Parallel Task MCP to check the task status
|
||||
2. If status is 'completed', extract the results
|
||||
3. Send results to user with mcp__nanoclaw__send_message
|
||||
4. Use mcp__nanoclaw__complete_scheduled_task to mark this task as done
|
||||
|
||||
If status is still 'running' or 'pending', do nothing (task will run again in 30s).
|
||||
If status is 'failed', send error message and complete the task."
|
||||
|
||||
Schedule: interval every 30 seconds
|
||||
Context mode: isolated
|
||||
```
|
||||
4. Send acknowledgment with tracking link
|
||||
5. Exit immediately - scheduler handles the rest
|
||||
|
||||
### Choosing Between Them
|
||||
|
||||
**Use Search when:**
|
||||
- Question needs a quick fact or recent information
|
||||
- Simple definition or clarification
|
||||
- Verifying specific details
|
||||
- Current events or news
|
||||
|
||||
**Use Deep Research (with permission) when:**
|
||||
- User wants to learn about a complex topic
|
||||
- Question requires analysis or comparison
|
||||
- Historical context or evolution of concepts
|
||||
- Structured, comprehensive understanding needed
|
||||
- User explicitly asks to "research" or "explain in depth"
|
||||
|
||||
**Default behavior:** Prefer search for most questions. Only suggest deep research when the topic genuinely requires comprehensive analysis.
|
||||
```
|
||||
|
||||
### 6. Rebuild Container
|
||||
|
||||
Build the container with updated agent runner:
|
||||
|
||||
```bash
|
||||
./container/build.sh
|
||||
```
|
||||
|
||||
Verify the build:
|
||||
```bash
|
||||
echo '{}' | docker run -i --entrypoint /bin/echo nanoclaw-agent:latest "Container OK"
|
||||
```
|
||||
|
||||
### 7. Restart Service
|
||||
|
||||
Rebuild the main app and restart:
|
||||
## 5. 빌드와 검증
|
||||
|
||||
```bash
|
||||
npm run build:runners
|
||||
npm run build
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# Linux: systemctl --user restart nanoclaw
|
||||
npm run setup -- --step service
|
||||
```
|
||||
|
||||
Wait 3 seconds for service to start, then verify:
|
||||
```bash
|
||||
sleep 3
|
||||
launchctl list | grep nanoclaw # macOS
|
||||
# Linux: systemctl --user status nanoclaw
|
||||
```
|
||||
디스코드 테스트 예시:
|
||||
|
||||
### 8. Test Integration
|
||||
> `최신 AI 뉴스 찾아줘`
|
||||
|
||||
Tell the user to test:
|
||||
> Send a message to your assistant: `@[YourAssistantName] what's the latest news about AI?`
|
||||
>
|
||||
> The assistant should use Parallel Search API to find current information.
|
||||
>
|
||||
> Then try: `@[YourAssistantName] can you research the history of artificial intelligence?`
|
||||
>
|
||||
> The assistant should ask for permission before using the Task API.
|
||||
> `AI 에이전트 역사 자세히 조사해줘`
|
||||
|
||||
Check logs to verify MCP servers loaded:
|
||||
```bash
|
||||
tail -20 logs/nanoclaw.log
|
||||
```
|
||||
두 번째 요청에서는 비용/시간 안내 후 허가를 먼저 묻게 만드는 게 맞습니다.
|
||||
|
||||
Look for: `Parallel AI MCP servers configured`
|
||||
## 문제 확인
|
||||
|
||||
## Troubleshooting
|
||||
### MCP가 안 뜸
|
||||
|
||||
**Container hangs or times out:**
|
||||
- Check that `type: 'http'` is specified in MCP server config
|
||||
- Verify API key is correct in .env
|
||||
- Check container logs: `cat groups/main/logs/container-*.log | tail -50`
|
||||
- `PARALLEL_API_KEY`가 child env까지 전달되는지 확인합니다
|
||||
- `allowedTools`에 Parallel prefix가 빠지지 않았는지 봅니다
|
||||
|
||||
**MCP servers not loading:**
|
||||
- Ensure PARALLEL_API_KEY is in .env
|
||||
- Verify container-runner.ts includes PARALLEL_API_KEY in allowedVars
|
||||
- Check agent-runner logs for "Parallel AI MCP servers configured" message
|
||||
### 긴 작업이 응답을 오래 붙잡음
|
||||
|
||||
**Task polling not working:**
|
||||
- Verify scheduled task was created: `sqlite3 store/messages.db "SELECT * FROM scheduled_tasks"`
|
||||
- Check task runs: `tail -f logs/nanoclaw.log | grep "scheduled task"`
|
||||
- Ensure task prompt includes proper Parallel MCP tool names
|
||||
|
||||
## Uninstalling
|
||||
|
||||
To remove Parallel AI integration:
|
||||
|
||||
1. Remove from .env: `sed -i.bak '/PARALLEL_API_KEY/d' .env`
|
||||
2. Revert changes to container-runner.ts and agent-runner/src/index.ts
|
||||
3. Remove Web Research Tools section from groups/main/CLAUDE.md
|
||||
4. Rebuild: `./container/build.sh && npm run build`
|
||||
5. Restart: `launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `systemctl --user restart nanoclaw` (Linux)
|
||||
- `parallel-task` 결과를 기다리며 블로킹하지 말고 스케줄러로 넘기도록 프롬프트와 구현을 같이 수정합니다
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
---
|
||||
name: add-pdf-reader
|
||||
description: Add PDF reading to NanoClaw agents. Extracts text from PDFs via pdftotext CLI. Handles WhatsApp attachments, URLs, and local files.
|
||||
---
|
||||
|
||||
# Add PDF Reader
|
||||
|
||||
Adds PDF reading capability to all container agents using poppler-utils (pdftotext/pdfinfo). PDFs sent as WhatsApp attachments are auto-downloaded to the group workspace.
|
||||
|
||||
## Phase 1: Pre-flight
|
||||
|
||||
1. Check if `container/skills/pdf-reader/pdf-reader` exists — skip to Phase 3 if already applied
|
||||
2. Confirm WhatsApp is installed first (`skill/whatsapp` merged). This skill modifies WhatsApp channel files.
|
||||
|
||||
## Phase 2: Apply Code Changes
|
||||
|
||||
### Ensure WhatsApp fork remote
|
||||
|
||||
```bash
|
||||
git remote -v
|
||||
```
|
||||
|
||||
If `whatsapp` is missing, add it:
|
||||
|
||||
```bash
|
||||
git remote add whatsapp https://github.com/qwibitai/nanoclaw-whatsapp.git
|
||||
```
|
||||
|
||||
### Merge the skill branch
|
||||
|
||||
```bash
|
||||
git fetch whatsapp skill/pdf-reader
|
||||
git merge whatsapp/skill/pdf-reader
|
||||
```
|
||||
|
||||
This merges in:
|
||||
- `container/skills/pdf-reader/SKILL.md` (agent-facing documentation)
|
||||
- `container/skills/pdf-reader/pdf-reader` (CLI script)
|
||||
- `poppler-utils` in `container/Dockerfile`
|
||||
- PDF attachment download in `src/channels/whatsapp.ts`
|
||||
- PDF tests in `src/channels/whatsapp.test.ts`
|
||||
|
||||
If the merge reports conflicts, resolve them by reading the conflicted files and understanding the intent of both sides.
|
||||
|
||||
### Validate
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
npx vitest run src/channels/whatsapp.test.ts
|
||||
```
|
||||
|
||||
### Rebuild container
|
||||
|
||||
```bash
|
||||
./container/build.sh
|
||||
```
|
||||
|
||||
### Restart service
|
||||
|
||||
```bash
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# Linux: systemctl --user restart nanoclaw
|
||||
```
|
||||
|
||||
## Phase 3: Verify
|
||||
|
||||
### Test PDF extraction
|
||||
|
||||
Send a PDF file in any registered WhatsApp chat. The agent should:
|
||||
1. Download the PDF to `attachments/`
|
||||
2. Respond acknowledging the PDF
|
||||
3. Be able to extract text when asked
|
||||
|
||||
### Test URL fetching
|
||||
|
||||
Ask the agent to read a PDF from a URL. It should use `pdf-reader fetch <url>`.
|
||||
|
||||
### Check logs if needed
|
||||
|
||||
```bash
|
||||
tail -f logs/nanoclaw.log | grep -i pdf
|
||||
```
|
||||
|
||||
Look for:
|
||||
- `Downloaded PDF attachment` — successful download
|
||||
- `Failed to download PDF attachment` — media download issue
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Agent says pdf-reader command not found
|
||||
|
||||
Container needs rebuilding. Run `./container/build.sh` and restart the service.
|
||||
|
||||
### PDF text extraction is empty
|
||||
|
||||
The PDF may be scanned (image-based). pdftotext only handles text-based PDFs. Consider using the agent-browser to open the PDF visually instead.
|
||||
|
||||
### WhatsApp PDF not detected
|
||||
|
||||
Verify the message has `documentMessage` with `mimetype: application/pdf`. Some file-sharing apps send PDFs as generic files without the correct mimetype.
|
||||
@@ -1,113 +0,0 @@
|
||||
---
|
||||
name: add-reactions
|
||||
description: Add WhatsApp emoji reaction support — receive, send, store, and search reactions.
|
||||
---
|
||||
|
||||
# Add Reactions
|
||||
|
||||
This skill adds emoji reaction support to NanoClaw's WhatsApp channel: receive and store reactions, send reactions from the container agent via MCP tool, and query reaction history from SQLite.
|
||||
|
||||
## Phase 1: Pre-flight
|
||||
|
||||
### Check if already applied
|
||||
|
||||
Check if `src/status-tracker.ts` exists:
|
||||
|
||||
```bash
|
||||
test -f src/status-tracker.ts && echo "Already applied" || echo "Not applied"
|
||||
```
|
||||
|
||||
If already applied, skip to Phase 3 (Verify).
|
||||
|
||||
## Phase 2: Apply Code Changes
|
||||
|
||||
### Ensure WhatsApp fork remote
|
||||
|
||||
```bash
|
||||
git remote -v
|
||||
```
|
||||
|
||||
If `whatsapp` is missing, add it:
|
||||
|
||||
```bash
|
||||
git remote add whatsapp https://github.com/qwibitai/nanoclaw-whatsapp.git
|
||||
```
|
||||
|
||||
### Merge the skill branch
|
||||
|
||||
```bash
|
||||
git fetch whatsapp skill/reactions
|
||||
git merge whatsapp/skill/reactions
|
||||
```
|
||||
|
||||
This adds:
|
||||
- `scripts/migrate-reactions.ts` (database migration for `reactions` table with composite PK and indexes)
|
||||
- `src/status-tracker.ts` (forward-only emoji state machine for message lifecycle signaling, with persistence and retry)
|
||||
- `src/status-tracker.test.ts` (unit tests for StatusTracker)
|
||||
- `container/skills/reactions/SKILL.md` (agent-facing documentation for the `react_to_message` MCP tool)
|
||||
- Reaction support in `src/db.ts`, `src/channels/whatsapp.ts`, `src/types.ts`, `src/ipc.ts`, `src/index.ts`, `src/group-queue.ts`, and `container/agent-runner/src/ipc-mcp-stdio.ts`
|
||||
|
||||
### Run database migration
|
||||
|
||||
```bash
|
||||
npx tsx scripts/migrate-reactions.ts
|
||||
```
|
||||
|
||||
### Validate code changes
|
||||
|
||||
```bash
|
||||
npm test
|
||||
npm run build
|
||||
```
|
||||
|
||||
All tests must pass and build must be clean before proceeding.
|
||||
|
||||
## Phase 3: Verify
|
||||
|
||||
### Build and restart
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Linux:
|
||||
```bash
|
||||
systemctl --user restart nanoclaw
|
||||
```
|
||||
|
||||
macOS:
|
||||
```bash
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
|
||||
```
|
||||
|
||||
### Test receiving reactions
|
||||
|
||||
1. Send a message from your phone
|
||||
2. React to it with an emoji on WhatsApp
|
||||
3. Check the database:
|
||||
|
||||
```bash
|
||||
sqlite3 store/messages.db "SELECT * FROM reactions ORDER BY timestamp DESC LIMIT 5;"
|
||||
```
|
||||
|
||||
### Test sending reactions
|
||||
|
||||
Ask the agent to react to a message via the `react_to_message` MCP tool. Check your phone — the reaction should appear on the message.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Reactions not appearing in database
|
||||
|
||||
- Check NanoClaw logs for `Failed to process reaction` errors
|
||||
- Verify the chat is registered
|
||||
- Confirm the service is running
|
||||
|
||||
### Migration fails
|
||||
|
||||
- Ensure `store/messages.db` exists and is accessible
|
||||
- If "table reactions already exists", the migration already ran — skip it
|
||||
|
||||
### Agent can't send reactions
|
||||
|
||||
- Check IPC logs for `Unauthorized IPC reaction attempt blocked` — the agent can only react in its own group's chat
|
||||
- Verify WhatsApp is connected: check logs for connection status
|
||||
@@ -1,216 +0,0 @@
|
||||
---
|
||||
name: add-slack
|
||||
description: Add Slack as a channel. Can replace WhatsApp entirely or run alongside it. Uses Socket Mode (no public URL needed).
|
||||
---
|
||||
|
||||
# Add Slack Channel
|
||||
|
||||
This skill adds Slack support to NanoClaw, then walks through interactive setup.
|
||||
|
||||
## Phase 1: Pre-flight
|
||||
|
||||
### Check if already applied
|
||||
|
||||
Check if `src/channels/slack.ts` exists. If it does, skip to Phase 3 (Setup). The code changes are already in place.
|
||||
|
||||
### Ask the user
|
||||
|
||||
**Do they already have a Slack app configured?** If yes, collect the Bot Token and App Token now. If no, we'll create one in Phase 3.
|
||||
|
||||
## Phase 2: Apply Code Changes
|
||||
|
||||
### Ensure channel remote
|
||||
|
||||
```bash
|
||||
git remote -v
|
||||
```
|
||||
|
||||
If `slack` is missing, add it:
|
||||
|
||||
```bash
|
||||
git remote add slack https://github.com/qwibitai/nanoclaw-slack.git
|
||||
```
|
||||
|
||||
### Merge the skill branch
|
||||
|
||||
```bash
|
||||
git fetch slack main
|
||||
git merge slack/main
|
||||
```
|
||||
|
||||
This merges in:
|
||||
- `src/channels/slack.ts` (SlackChannel class with self-registration via `registerChannel`)
|
||||
- `src/channels/slack.test.ts` (46 unit tests)
|
||||
- `import './slack.js'` appended to the channel barrel file `src/channels/index.ts`
|
||||
- `@slack/bolt` npm dependency in `package.json`
|
||||
- `SLACK_BOT_TOKEN` and `SLACK_APP_TOKEN` in `.env.example`
|
||||
|
||||
If the merge reports conflicts, resolve them by reading the conflicted files and understanding the intent of both sides.
|
||||
|
||||
### Validate code changes
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
npx vitest run src/channels/slack.test.ts
|
||||
```
|
||||
|
||||
All tests must pass (including the new Slack tests) and build must be clean before proceeding.
|
||||
|
||||
## Phase 3: Setup
|
||||
|
||||
### Create Slack App (if needed)
|
||||
|
||||
If the user doesn't have a Slack app, share [SLACK_SETUP.md](SLACK_SETUP.md) which has step-by-step instructions with screenshots guidance, troubleshooting, and a token reference table.
|
||||
|
||||
Quick summary of what's needed:
|
||||
1. Create a Slack app at [api.slack.com/apps](https://api.slack.com/apps)
|
||||
2. Enable Socket Mode and generate an App-Level Token (`xapp-...`)
|
||||
3. Subscribe to bot events: `message.channels`, `message.groups`, `message.im`
|
||||
4. Add OAuth scopes: `chat:write`, `channels:history`, `groups:history`, `im:history`, `channels:read`, `groups:read`, `users:read`
|
||||
5. Install to workspace and copy the Bot Token (`xoxb-...`)
|
||||
|
||||
Wait for the user to provide both tokens.
|
||||
|
||||
### Configure environment
|
||||
|
||||
Add to `.env`:
|
||||
|
||||
```bash
|
||||
SLACK_BOT_TOKEN=xoxb-your-bot-token
|
||||
SLACK_APP_TOKEN=xapp-your-app-token
|
||||
```
|
||||
|
||||
Channels auto-enable when their credentials are present — no extra configuration needed.
|
||||
|
||||
Sync to container environment:
|
||||
|
||||
```bash
|
||||
mkdir -p data/env && cp .env data/env/env
|
||||
```
|
||||
|
||||
The container reads environment from `data/env/env`, not `.env` directly.
|
||||
|
||||
### Build and restart
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
|
||||
```
|
||||
|
||||
## Phase 4: Registration
|
||||
|
||||
### Get Channel ID
|
||||
|
||||
Tell the user:
|
||||
|
||||
> 1. Add the bot to a Slack channel (right-click channel → **View channel details** → **Integrations** → **Add apps**)
|
||||
> 2. In that channel, the channel ID is in the URL when you open it in a browser: `https://app.slack.com/client/T.../C0123456789` — the `C...` part is the channel ID
|
||||
> 3. Alternatively, right-click the channel name → **Copy link** — the channel ID is the last path segment
|
||||
>
|
||||
> The JID format for NanoClaw is: `slack:C0123456789`
|
||||
|
||||
Wait for the user to provide the channel ID.
|
||||
|
||||
### Register the channel
|
||||
|
||||
Use the IPC register flow or register directly. The channel ID, name, and folder name are needed.
|
||||
|
||||
For a main channel (responds to all messages):
|
||||
|
||||
```typescript
|
||||
registerGroup("slack:<channel-id>", {
|
||||
name: "<channel-name>",
|
||||
folder: "slack_main",
|
||||
trigger: `@${ASSISTANT_NAME}`,
|
||||
added_at: new Date().toISOString(),
|
||||
requiresTrigger: false,
|
||||
isMain: true,
|
||||
});
|
||||
```
|
||||
|
||||
For additional channels (trigger-only):
|
||||
|
||||
```typescript
|
||||
registerGroup("slack:<channel-id>", {
|
||||
name: "<channel-name>",
|
||||
folder: "slack_<channel-name>",
|
||||
trigger: `@${ASSISTANT_NAME}`,
|
||||
added_at: new Date().toISOString(),
|
||||
requiresTrigger: true,
|
||||
});
|
||||
```
|
||||
|
||||
## Phase 5: Verify
|
||||
|
||||
### Test the connection
|
||||
|
||||
Tell the user:
|
||||
|
||||
> Send a message in your registered Slack channel:
|
||||
> - For main channel: Any message works
|
||||
> - For non-main: `@<assistant-name> hello` (using the configured trigger word)
|
||||
>
|
||||
> The bot should respond within a few seconds.
|
||||
|
||||
### Check logs if needed
|
||||
|
||||
```bash
|
||||
tail -f logs/nanoclaw.log
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Bot not responding
|
||||
|
||||
1. Check `SLACK_BOT_TOKEN` and `SLACK_APP_TOKEN` are set in `.env` AND synced to `data/env/env`
|
||||
2. Check channel is registered: `sqlite3 store/messages.db "SELECT * FROM registered_groups WHERE jid LIKE 'slack:%'"`
|
||||
3. For non-main channels: message must include trigger pattern
|
||||
4. Service is running: `launchctl list | grep nanoclaw`
|
||||
|
||||
### Bot connected but not receiving messages
|
||||
|
||||
1. Verify Socket Mode is enabled in the Slack app settings
|
||||
2. Verify the bot is subscribed to the correct events (`message.channels`, `message.groups`, `message.im`)
|
||||
3. Verify the bot has been added to the channel
|
||||
4. Check that the bot has the required OAuth scopes
|
||||
|
||||
### Bot not seeing messages in channels
|
||||
|
||||
By default, bots only see messages in channels they've been explicitly added to. Make sure to:
|
||||
1. Add the bot to each channel you want it to monitor
|
||||
2. Check the bot has `channels:history` and/or `groups:history` scopes
|
||||
|
||||
### "missing_scope" errors
|
||||
|
||||
If the bot logs `missing_scope` errors:
|
||||
1. Go to **OAuth & Permissions** in your Slack app settings
|
||||
2. Add the missing scope listed in the error message
|
||||
3. **Reinstall the app** to your workspace — scope changes require reinstallation
|
||||
4. Copy the new Bot Token (it changes on reinstall) and update `.env`
|
||||
5. Sync: `mkdir -p data/env && cp .env data/env/env`
|
||||
6. Restart: `launchctl kickstart -k gui/$(id -u)/com.nanoclaw`
|
||||
|
||||
### Getting channel ID
|
||||
|
||||
If the channel ID is hard to find:
|
||||
- In Slack desktop: right-click channel → **Copy link** → extract the `C...` ID from the URL
|
||||
- In Slack web: the URL shows `https://app.slack.com/client/TXXXXXXX/C0123456789`
|
||||
- Via API: `curl -s -H "Authorization: Bearer $SLACK_BOT_TOKEN" "https://slack.com/api/conversations.list" | jq '.channels[] | {id, name}'`
|
||||
|
||||
## After Setup
|
||||
|
||||
The Slack channel supports:
|
||||
- **Public channels** — Bot must be added to the channel
|
||||
- **Private channels** — Bot must be invited to the channel
|
||||
- **Direct messages** — Users can DM the bot directly
|
||||
- **Multi-channel** — Can run alongside WhatsApp or other channels (auto-enabled by credentials)
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- **Threads are flattened** — Threaded replies are delivered to the agent as regular channel messages. The agent sees them but has no awareness they originated in a thread. Responses always go to the channel, not back into the thread. Users in a thread will need to check the main channel for the bot's reply. Full thread-aware routing (respond in-thread) requires pipeline-wide changes: database schema, `NewMessage` type, `Channel.sendMessage` interface, and routing logic.
|
||||
- **No typing indicator** — Slack's Bot API does not expose a typing indicator endpoint. The `setTyping()` method is a no-op. Users won't see "bot is typing..." while the agent works.
|
||||
- **Message splitting is naive** — Long messages are split at a fixed 4000-character boundary, which may break mid-word or mid-sentence. A smarter split (on paragraph or sentence boundaries) would improve readability.
|
||||
- **No file/image handling** — The bot only processes text content. File uploads, images, and rich message blocks are not forwarded to the agent.
|
||||
- **Channel metadata sync is unbounded** — `syncChannelMetadata()` paginates through all channels the bot is a member of, but has no upper bound or timeout. Workspaces with thousands of channels may experience slow startup.
|
||||
- **Workspace admin policies not detected** — If the Slack workspace restricts bot app installation, the setup will fail at the "Install to Workspace" step with no programmatic detection or guidance. See SLACK_SETUP.md troubleshooting section.
|
||||
@@ -1,384 +0,0 @@
|
||||
---
|
||||
name: add-telegram-swarm
|
||||
description: Add Agent Swarm (Teams) support to Telegram. Each subagent gets its own bot identity in the group. Requires Telegram channel to be set up first (use /add-telegram). Triggers on "agent swarm", "agent teams telegram", "telegram swarm", "bot pool".
|
||||
---
|
||||
|
||||
# Add Agent Swarm to Telegram
|
||||
|
||||
This skill adds Agent Teams (Swarm) support to an existing Telegram channel. Each subagent in a team gets its own bot identity in the Telegram group, so users can visually distinguish which agent is speaking.
|
||||
|
||||
**Prerequisite**: Telegram must already be set up via the `/add-telegram` skill. If `src/telegram.ts` does not exist or `TELEGRAM_BOT_TOKEN` is not configured, tell the user to run `/add-telegram` first.
|
||||
|
||||
## How It Works
|
||||
|
||||
- The **main bot** receives messages and sends lead agent responses (already set up by `/add-telegram`)
|
||||
- **Pool bots** are send-only — each gets a Grammy `Api` instance (no polling)
|
||||
- When a subagent calls `send_message` with a `sender` parameter, the host assigns a pool bot and renames it to match the sender's role
|
||||
- Messages appear in Telegram from different bot identities
|
||||
|
||||
```
|
||||
Subagent calls send_message(text: "Found 3 results", sender: "Researcher")
|
||||
→ MCP writes IPC file with sender field
|
||||
→ Host IPC watcher picks it up
|
||||
→ Assigns pool bot #2 to "Researcher" (round-robin, stable per-group)
|
||||
→ Renames pool bot #2 to "Researcher" via setMyName
|
||||
→ Sends message via pool bot #2's Api instance
|
||||
→ Appears in Telegram from "Researcher" bot
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### 1. Create Pool Bots
|
||||
|
||||
Tell the user:
|
||||
|
||||
> I need you to create 3-5 Telegram bots to use as the agent pool. These will be renamed dynamically to match agent roles.
|
||||
>
|
||||
> 1. Open Telegram and search for `@BotFather`
|
||||
> 2. Send `/newbot` for each bot:
|
||||
> - Give them any placeholder name (e.g., "Bot 1", "Bot 2")
|
||||
> - Usernames like `myproject_swarm_1_bot`, `myproject_swarm_2_bot`, etc.
|
||||
> 3. Copy all the tokens
|
||||
> 4. Add all bots to your Telegram group(s) where you want agent teams
|
||||
|
||||
Wait for user to provide the tokens.
|
||||
|
||||
### 2. Disable Group Privacy for Pool Bots
|
||||
|
||||
Tell the user:
|
||||
|
||||
> **Important**: Each pool bot needs Group Privacy disabled so it can send messages in groups.
|
||||
>
|
||||
> For each pool bot in `@BotFather`:
|
||||
> 1. Send `/mybots` and select the bot
|
||||
> 2. Go to **Bot Settings** > **Group Privacy** > **Turn off**
|
||||
>
|
||||
> Then add all pool bots to your Telegram group(s).
|
||||
|
||||
## Implementation
|
||||
|
||||
### Step 1: Update Configuration
|
||||
|
||||
Read `src/config.ts` and add the bot pool config near the other Telegram exports:
|
||||
|
||||
```typescript
|
||||
export const TELEGRAM_BOT_POOL = (process.env.TELEGRAM_BOT_POOL || '')
|
||||
.split(',')
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean);
|
||||
```
|
||||
|
||||
### Step 2: Add Bot Pool to Telegram Module
|
||||
|
||||
Read `src/telegram.ts` and add the following:
|
||||
|
||||
1. **Update imports** — add `Api` to the Grammy import:
|
||||
|
||||
```typescript
|
||||
import { Api, Bot } from 'grammy';
|
||||
```
|
||||
|
||||
2. **Add pool state** after the existing `let bot` declaration:
|
||||
|
||||
```typescript
|
||||
// Bot pool for agent teams: send-only Api instances (no polling)
|
||||
const poolApis: Api[] = [];
|
||||
// Maps "{groupFolder}:{senderName}" → pool Api index for stable assignment
|
||||
const senderBotMap = new Map<string, number>();
|
||||
let nextPoolIndex = 0;
|
||||
```
|
||||
|
||||
3. **Add pool functions** — place these before the `isTelegramConnected` function:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Initialize send-only Api instances for the bot pool.
|
||||
* Each pool bot can send messages but doesn't poll for updates.
|
||||
*/
|
||||
export async function initBotPool(tokens: string[]): Promise<void> {
|
||||
for (const token of tokens) {
|
||||
try {
|
||||
const api = new Api(token);
|
||||
const me = await api.getMe();
|
||||
poolApis.push(api);
|
||||
logger.info(
|
||||
{ username: me.username, id: me.id, poolSize: poolApis.length },
|
||||
'Pool bot initialized',
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error({ err }, 'Failed to initialize pool bot');
|
||||
}
|
||||
}
|
||||
if (poolApis.length > 0) {
|
||||
logger.info({ count: poolApis.length }, 'Telegram bot pool ready');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message via a pool bot assigned to the given sender name.
|
||||
* Assigns bots round-robin on first use; subsequent messages from the
|
||||
* same sender in the same group always use the same bot.
|
||||
* On first assignment, renames the bot to match the sender's role.
|
||||
*/
|
||||
export async function sendPoolMessage(
|
||||
chatId: string,
|
||||
text: string,
|
||||
sender: string,
|
||||
groupFolder: string,
|
||||
): Promise<void> {
|
||||
if (poolApis.length === 0) {
|
||||
// No pool bots — fall back to main bot
|
||||
await sendTelegramMessage(chatId, text);
|
||||
return;
|
||||
}
|
||||
|
||||
const key = `${groupFolder}:${sender}`;
|
||||
let idx = senderBotMap.get(key);
|
||||
if (idx === undefined) {
|
||||
idx = nextPoolIndex % poolApis.length;
|
||||
nextPoolIndex++;
|
||||
senderBotMap.set(key, idx);
|
||||
// Rename the bot to match the sender's role, then wait for Telegram to propagate
|
||||
try {
|
||||
await poolApis[idx].setMyName(sender);
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
logger.info({ sender, groupFolder, poolIndex: idx }, 'Assigned and renamed pool bot');
|
||||
} catch (err) {
|
||||
logger.warn({ sender, err }, 'Failed to rename pool bot (sending anyway)');
|
||||
}
|
||||
}
|
||||
|
||||
const api = poolApis[idx];
|
||||
try {
|
||||
const numericId = chatId.replace(/^tg:/, '');
|
||||
const MAX_LENGTH = 4096;
|
||||
if (text.length <= MAX_LENGTH) {
|
||||
await api.sendMessage(numericId, text);
|
||||
} else {
|
||||
for (let i = 0; i < text.length; i += MAX_LENGTH) {
|
||||
await api.sendMessage(numericId, text.slice(i, i + MAX_LENGTH));
|
||||
}
|
||||
}
|
||||
logger.info({ chatId, sender, poolIndex: idx, length: text.length }, 'Pool message sent');
|
||||
} catch (err) {
|
||||
logger.error({ chatId, sender, err }, 'Failed to send pool message');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Add sender Parameter to MCP Tool
|
||||
|
||||
Read `container/agent-runner/src/ipc-mcp-stdio.ts` and update the `send_message` tool to accept an optional `sender` parameter:
|
||||
|
||||
Change the tool's schema from:
|
||||
```typescript
|
||||
{ text: z.string().describe('The message text to send') },
|
||||
```
|
||||
|
||||
To:
|
||||
```typescript
|
||||
{
|
||||
text: z.string().describe('The message text to send'),
|
||||
sender: z.string().optional().describe('Your role/identity name (e.g. "Researcher"). When set, messages appear from a dedicated bot in Telegram.'),
|
||||
},
|
||||
```
|
||||
|
||||
And update the handler to include `sender` in the IPC data:
|
||||
|
||||
```typescript
|
||||
async (args) => {
|
||||
const data: Record<string, string | undefined> = {
|
||||
type: 'message',
|
||||
chatJid,
|
||||
text: args.text,
|
||||
sender: args.sender || undefined,
|
||||
groupFolder,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
writeIpcFile(MESSAGES_DIR, data);
|
||||
|
||||
return { content: [{ type: 'text' as const, text: 'Message sent.' }] };
|
||||
},
|
||||
```
|
||||
|
||||
### Step 4: Update Host IPC Routing
|
||||
|
||||
Read `src/ipc.ts` and make these changes:
|
||||
|
||||
1. **Add imports** — add `sendPoolMessage` and `initBotPool` from the Telegram swarm module, and `TELEGRAM_BOT_POOL` from config.
|
||||
|
||||
2. **Update IPC message routing** — in `src/ipc.ts`, find where the `sendMessage` dependency is called to deliver IPC messages (inside `processIpcFiles`). The `sendMessage` is passed in via the `IpcDeps` parameter. Wrap it to route Telegram swarm messages through the bot pool:
|
||||
|
||||
```typescript
|
||||
if (data.sender && data.chatJid.startsWith('tg:')) {
|
||||
await sendPoolMessage(
|
||||
data.chatJid,
|
||||
data.text,
|
||||
data.sender,
|
||||
sourceGroup,
|
||||
);
|
||||
} else {
|
||||
await deps.sendMessage(data.chatJid, data.text);
|
||||
}
|
||||
```
|
||||
|
||||
Note: The assistant name prefix is handled by `formatOutbound()` in the router — Telegram channels have `prefixAssistantName = false` so no prefix is added for `tg:` JIDs.
|
||||
|
||||
3. **Initialize pool in `main()` in `src/index.ts`** — after creating the Telegram channel, add:
|
||||
|
||||
```typescript
|
||||
if (TELEGRAM_BOT_POOL.length > 0) {
|
||||
await initBotPool(TELEGRAM_BOT_POOL);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 5: Update CLAUDE.md Files
|
||||
|
||||
#### 5a. Add global message formatting rules
|
||||
|
||||
Read `groups/global/CLAUDE.md` and add a Message Formatting section:
|
||||
|
||||
```markdown
|
||||
## Message Formatting
|
||||
|
||||
NEVER use markdown. Only use WhatsApp/Telegram formatting:
|
||||
- *single asterisks* for bold (NEVER **double asterisks**)
|
||||
- _underscores_ for italic
|
||||
- • bullet points
|
||||
- ```triple backticks``` for code
|
||||
|
||||
No ## headings. No [links](url). No **double stars**.
|
||||
```
|
||||
|
||||
#### 5b. Update existing group CLAUDE.md headings
|
||||
|
||||
In any group CLAUDE.md that has a "WhatsApp Formatting" section (e.g. `groups/main/CLAUDE.md`), rename the heading to reflect multi-channel support:
|
||||
|
||||
```
|
||||
## WhatsApp Formatting (and other messaging apps)
|
||||
```
|
||||
|
||||
#### 5c. Add Agent Teams instructions to Telegram groups
|
||||
|
||||
For each Telegram group that will use agent teams, create or update its `groups/{folder}/CLAUDE.md` with these instructions. Read the existing CLAUDE.md first (or `groups/global/CLAUDE.md` as a base) and add the Agent Teams section:
|
||||
|
||||
```markdown
|
||||
## Agent Teams
|
||||
|
||||
When creating a team to tackle a complex task, follow these rules:
|
||||
|
||||
### CRITICAL: Follow the user's prompt exactly
|
||||
|
||||
Create *exactly* the team the user asked for — same number of agents, same roles, same names. Do NOT add extra agents, rename roles, or use generic names like "Researcher 1". If the user says "a marine biologist, a physicist, and Alexander Hamilton", create exactly those three agents with those exact names.
|
||||
|
||||
### Team member instructions
|
||||
|
||||
Each team member MUST be instructed to:
|
||||
|
||||
1. *Share progress in the group* via `mcp__nanoclaw__send_message` with a `sender` parameter matching their exact role/character name (e.g., `sender: "Marine Biologist"` or `sender: "Alexander Hamilton"`). This makes their messages appear from a dedicated bot in the Telegram group.
|
||||
2. *Also communicate with teammates* via `SendMessage` as normal for coordination.
|
||||
3. Keep group messages *short* — 2-4 sentences max per message. Break longer content into multiple `send_message` calls. No walls of text.
|
||||
4. Use the `sender` parameter consistently — always the same name so the bot identity stays stable.
|
||||
5. NEVER use markdown formatting. Use ONLY WhatsApp/Telegram formatting: single *asterisks* for bold (NOT **double**), _underscores_ for italic, • for bullets, ```backticks``` for code. No ## headings, no [links](url), no **double asterisks**.
|
||||
|
||||
### Example team creation prompt
|
||||
|
||||
When creating a teammate, include instructions like:
|
||||
|
||||
\```
|
||||
You are the Marine Biologist. When you have findings or updates for the user, send them to the group using mcp__nanoclaw__send_message with sender set to "Marine Biologist". Keep each message short (2-4 sentences max). Use emojis for strong reactions. ONLY use single *asterisks* for bold (never **double**), _underscores_ for italic, • for bullets. No markdown. Also communicate with teammates via SendMessage.
|
||||
\```
|
||||
|
||||
### Lead agent behavior
|
||||
|
||||
As the lead agent who created the team:
|
||||
|
||||
- You do NOT need to react to or relay every teammate message. The user sees those directly from the teammate bots.
|
||||
- Send your own messages only to comment, share thoughts, synthesize, or direct the team.
|
||||
- When processing an internal update from a teammate that doesn't need a user-facing response, wrap your *entire* output in `<internal>` tags.
|
||||
- Focus on high-level coordination and the final synthesis.
|
||||
```
|
||||
|
||||
### Step 6: Update Environment
|
||||
|
||||
Add pool tokens to `.env`:
|
||||
|
||||
```bash
|
||||
TELEGRAM_BOT_POOL=TOKEN1,TOKEN2,TOKEN3,...
|
||||
```
|
||||
|
||||
**Important**: Sync to all required locations:
|
||||
|
||||
```bash
|
||||
cp .env data/env/env
|
||||
```
|
||||
|
||||
Also add `TELEGRAM_BOT_POOL` to the launchd plist (`~/Library/LaunchAgents/com.nanoclaw.plist`) in the `EnvironmentVariables` dict if using launchd.
|
||||
|
||||
### Step 7: Rebuild and Restart
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
./container/build.sh # Required — MCP tool changed
|
||||
# macOS:
|
||||
launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist
|
||||
launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
|
||||
# Linux:
|
||||
# systemctl --user restart nanoclaw
|
||||
```
|
||||
|
||||
Must use `unload/load` (macOS) or `restart` (Linux) because the service env vars changed.
|
||||
|
||||
### Step 8: Test
|
||||
|
||||
Tell the user:
|
||||
|
||||
> Send a message in your Telegram group asking for a multi-agent task, e.g.:
|
||||
> "Assemble a team of a researcher and a coder to build me a hello world app"
|
||||
>
|
||||
> You should see:
|
||||
> - The lead agent (main bot) acknowledging and creating the team
|
||||
> - Each subagent messaging from a different bot, renamed to their role
|
||||
> - Short, scannable messages from each agent
|
||||
>
|
||||
> Check logs: `tail -f logs/nanoclaw.log | grep -i pool`
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
- Pool bots use Grammy's `Api` class — lightweight, no polling, just send
|
||||
- Bot names are set via `setMyName` — changes are global to the bot, not per-chat
|
||||
- A 2-second delay after `setMyName` allows Telegram to propagate the name change before the first message
|
||||
- Sender→bot mapping is stable within a group (keyed as `{groupFolder}:{senderName}`)
|
||||
- Mapping resets on service restart — pool bots get reassigned fresh
|
||||
- If pool runs out, bots are reused (round-robin wraps)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Pool bots not sending messages
|
||||
|
||||
1. Verify tokens: `curl -s "https://api.telegram.org/botTOKEN/getMe"`
|
||||
2. Check pool initialized: `grep "Pool bot" logs/nanoclaw.log`
|
||||
3. Ensure all pool bots are members of the Telegram group
|
||||
4. Check Group Privacy is disabled for each pool bot
|
||||
|
||||
### Bot names not updating
|
||||
|
||||
Telegram caches bot names client-side. The 2-second delay after `setMyName` helps, but users may need to restart their Telegram client to see updated names immediately.
|
||||
|
||||
### Subagents not using send_message
|
||||
|
||||
Check the group's `CLAUDE.md` has the Agent Teams instructions. The lead agent reads this when creating teammates and must include the `send_message` + `sender` instructions in each teammate's prompt.
|
||||
|
||||
## Removal
|
||||
|
||||
To remove Agent Swarm support while keeping basic Telegram:
|
||||
|
||||
1. Remove `TELEGRAM_BOT_POOL` from `src/config.ts`
|
||||
2. Remove pool code from `src/telegram.ts` (`poolApis`, `senderBotMap`, `initBotPool`, `sendPoolMessage`)
|
||||
3. Remove pool routing from IPC handler in `src/index.ts` (revert to plain `sendMessage`)
|
||||
4. Remove `initBotPool` call from `main()`
|
||||
5. Remove `sender` param from MCP tool in `container/agent-runner/src/ipc-mcp-stdio.ts`
|
||||
6. Remove Agent Teams section from group CLAUDE.md files
|
||||
7. Remove `TELEGRAM_BOT_POOL` from `.env`, `data/env/env`, and launchd plist/systemd unit
|
||||
8. Rebuild: `npm run build && ./container/build.sh && launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist && launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist` (macOS) or `npm run build && ./container/build.sh && systemctl --user restart nanoclaw` (Linux)
|
||||
@@ -1,231 +0,0 @@
|
||||
---
|
||||
name: add-telegram
|
||||
description: Add Telegram as a channel. Can replace WhatsApp entirely or run alongside it. Also configurable as a control-only channel (triggers actions) or passive channel (receives notifications only).
|
||||
---
|
||||
|
||||
# Add Telegram Channel
|
||||
|
||||
This skill adds Telegram support to NanoClaw, then walks through interactive setup.
|
||||
|
||||
## Phase 1: Pre-flight
|
||||
|
||||
### Check if already applied
|
||||
|
||||
Check if `src/channels/telegram.ts` exists. If it does, skip to Phase 3 (Setup). The code changes are already in place.
|
||||
|
||||
### Ask the user
|
||||
|
||||
Use `AskUserQuestion` to collect configuration:
|
||||
|
||||
AskUserQuestion: Do you have a Telegram bot token, or do you need to create one?
|
||||
|
||||
If they have one, collect it now. If not, we'll create one in Phase 3.
|
||||
|
||||
## Phase 2: Apply Code Changes
|
||||
|
||||
### Ensure channel remote
|
||||
|
||||
```bash
|
||||
git remote -v
|
||||
```
|
||||
|
||||
If `telegram` is missing, add it:
|
||||
|
||||
```bash
|
||||
git remote add telegram https://github.com/qwibitai/nanoclaw-telegram.git
|
||||
```
|
||||
|
||||
### Merge the skill branch
|
||||
|
||||
```bash
|
||||
git fetch telegram main
|
||||
git merge telegram/main
|
||||
```
|
||||
|
||||
This merges in:
|
||||
- `src/channels/telegram.ts` (TelegramChannel class with self-registration via `registerChannel`)
|
||||
- `src/channels/telegram.test.ts` (unit tests with grammy mock)
|
||||
- `import './telegram.js'` appended to the channel barrel file `src/channels/index.ts`
|
||||
- `grammy` npm dependency in `package.json`
|
||||
- `TELEGRAM_BOT_TOKEN` in `.env.example`
|
||||
|
||||
If the merge reports conflicts, resolve them by reading the conflicted files and understanding the intent of both sides.
|
||||
|
||||
### Validate code changes
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
npx vitest run src/channels/telegram.test.ts
|
||||
```
|
||||
|
||||
All tests must pass (including the new Telegram tests) and build must be clean before proceeding.
|
||||
|
||||
## Phase 3: Setup
|
||||
|
||||
### Create Telegram Bot (if needed)
|
||||
|
||||
If the user doesn't have a bot token, tell them:
|
||||
|
||||
> I need you to create a Telegram bot:
|
||||
>
|
||||
> 1. Open Telegram and search for `@BotFather`
|
||||
> 2. Send `/newbot` and follow prompts:
|
||||
> - Bot name: Something friendly (e.g., "Andy Assistant")
|
||||
> - Bot username: Must end with "bot" (e.g., "andy_ai_bot")
|
||||
> 3. Copy the bot token (looks like `123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11`)
|
||||
|
||||
Wait for the user to provide the token.
|
||||
|
||||
### Configure environment
|
||||
|
||||
Add to `.env`:
|
||||
|
||||
```bash
|
||||
TELEGRAM_BOT_TOKEN=<their-token>
|
||||
```
|
||||
|
||||
Channels auto-enable when their credentials are present — no extra configuration needed.
|
||||
|
||||
Sync to container environment:
|
||||
|
||||
```bash
|
||||
mkdir -p data/env && cp .env data/env/env
|
||||
```
|
||||
|
||||
The container reads environment from `data/env/env`, not `.env` directly.
|
||||
|
||||
### Disable Group Privacy (for group chats)
|
||||
|
||||
Tell the user:
|
||||
|
||||
> **Important for group chats**: By default, Telegram bots only see @mentions and commands in groups. To let the bot see all messages:
|
||||
>
|
||||
> 1. Open Telegram and search for `@BotFather`
|
||||
> 2. Send `/mybots` and select your bot
|
||||
> 3. Go to **Bot Settings** > **Group Privacy** > **Turn off**
|
||||
>
|
||||
> This is optional if you only want trigger-based responses via @mentioning the bot.
|
||||
|
||||
### Build and restart
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# Linux: systemctl --user restart nanoclaw
|
||||
```
|
||||
|
||||
## Phase 4: Registration
|
||||
|
||||
### Get Chat ID
|
||||
|
||||
Tell the user:
|
||||
|
||||
> 1. Open your bot in Telegram (search for its username)
|
||||
> 2. Send `/chatid` — it will reply with the chat ID
|
||||
> 3. For groups: add the bot to the group first, then send `/chatid` in the group
|
||||
|
||||
Wait for the user to provide the chat ID (format: `tg:123456789` or `tg:-1001234567890`).
|
||||
|
||||
### Register the chat
|
||||
|
||||
Use the IPC register flow or register directly. The chat ID, name, and folder name are needed.
|
||||
|
||||
For a main chat (responds to all messages):
|
||||
|
||||
```typescript
|
||||
registerGroup("tg:<chat-id>", {
|
||||
name: "<chat-name>",
|
||||
folder: "telegram_main",
|
||||
trigger: `@${ASSISTANT_NAME}`,
|
||||
added_at: new Date().toISOString(),
|
||||
requiresTrigger: false,
|
||||
isMain: true,
|
||||
});
|
||||
```
|
||||
|
||||
For additional chats (trigger-only):
|
||||
|
||||
```typescript
|
||||
registerGroup("tg:<chat-id>", {
|
||||
name: "<chat-name>",
|
||||
folder: "telegram_<group-name>",
|
||||
trigger: `@${ASSISTANT_NAME}`,
|
||||
added_at: new Date().toISOString(),
|
||||
requiresTrigger: true,
|
||||
});
|
||||
```
|
||||
|
||||
## Phase 5: Verify
|
||||
|
||||
### Test the connection
|
||||
|
||||
Tell the user:
|
||||
|
||||
> Send a message to your registered Telegram chat:
|
||||
> - For main chat: Any message works
|
||||
> - For non-main: `@Andy hello` or @mention the bot
|
||||
>
|
||||
> The bot should respond within a few seconds.
|
||||
|
||||
### Check logs if needed
|
||||
|
||||
```bash
|
||||
tail -f logs/nanoclaw.log
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Bot not responding
|
||||
|
||||
Check:
|
||||
1. `TELEGRAM_BOT_TOKEN` is set in `.env` AND synced to `data/env/env`
|
||||
2. Chat is registered in SQLite (check with: `sqlite3 store/messages.db "SELECT * FROM registered_groups WHERE jid LIKE 'tg:%'"`)
|
||||
3. For non-main chats: message includes trigger pattern
|
||||
4. Service is running: `launchctl list | grep nanoclaw` (macOS) or `systemctl --user status nanoclaw` (Linux)
|
||||
|
||||
### Bot only responds to @mentions in groups
|
||||
|
||||
Group Privacy is enabled (default). Fix:
|
||||
1. `@BotFather` > `/mybots` > select bot > **Bot Settings** > **Group Privacy** > **Turn off**
|
||||
2. Remove and re-add the bot to the group (required for the change to take effect)
|
||||
|
||||
### Getting chat ID
|
||||
|
||||
If `/chatid` doesn't work:
|
||||
- Verify token: `curl -s "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getMe"`
|
||||
- Check bot is started: `tail -f logs/nanoclaw.log`
|
||||
|
||||
## After Setup
|
||||
|
||||
If running `npm run dev` while the service is active:
|
||||
```bash
|
||||
# macOS:
|
||||
launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist
|
||||
npm run dev
|
||||
# When done testing:
|
||||
launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
|
||||
# Linux:
|
||||
# systemctl --user stop nanoclaw
|
||||
# npm run dev
|
||||
# systemctl --user start nanoclaw
|
||||
```
|
||||
|
||||
## Agent Swarms (Teams)
|
||||
|
||||
After completing the Telegram setup, use `AskUserQuestion`:
|
||||
|
||||
AskUserQuestion: Would you like to add Agent Swarm support? Without it, Agent Teams still work — they just operate behind the scenes. With Swarm support, each subagent appears as a different bot in the Telegram group so you can see who's saying what and have interactive team sessions.
|
||||
|
||||
If they say yes, invoke the `/add-telegram-swarm` skill.
|
||||
|
||||
## Removal
|
||||
|
||||
To remove Telegram integration:
|
||||
|
||||
1. Delete `src/channels/telegram.ts` and `src/channels/telegram.test.ts`
|
||||
2. Remove `import './telegram.js'` from `src/channels/index.ts`
|
||||
3. Remove `TELEGRAM_BOT_TOKEN` from `.env`
|
||||
4. Remove Telegram registrations from SQLite: `sqlite3 store/messages.db "DELETE FROM registered_groups WHERE jid LIKE 'tg:%'"`
|
||||
5. Uninstall: `npm uninstall grammy`
|
||||
6. Rebuild: `npm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `npm run build && systemctl --user restart nanoclaw` (Linux)
|
||||
@@ -1,90 +1,60 @@
|
||||
---
|
||||
name: add-voice-transcription
|
||||
description: Add voice message transcription to NanoClaw using Groq Whisper (fast, free) with OpenAI fallback. Works on Discord and WhatsApp.
|
||||
description: Enable Discord voice transcription using Groq Whisper with OpenAI fallback.
|
||||
---
|
||||
|
||||
# Add Voice Transcription
|
||||
|
||||
This skill adds automatic voice message transcription to NanoClaw. When a voice note arrives, it is downloaded, transcribed, and delivered to the agent as `[Voice message transcription]: <text>`.
|
||||
디스코드 음성 첨부 전사는 이미 코드에 들어 있습니다. 현재 구현은 `src/channels/discord.ts`에 있고, 별도 브랜치 머지나 채널 추가 작업은 필요 없습니다.
|
||||
|
||||
**Provider priority:** Groq Whisper (fast, free) > OpenAI Whisper (fallback).
|
||||
우선순위는 `Groq Whisper -> OpenAI Whisper fallback` 입니다.
|
||||
|
||||
## Phase 1: Pre-flight
|
||||
## 1. 환경 변수 설정
|
||||
|
||||
### Discord
|
||||
|
||||
Voice transcription is built into `src/channels/discord.ts`. No code changes needed — just configure API keys (Phase 3).
|
||||
|
||||
### WhatsApp
|
||||
|
||||
Check if `src/transcription.ts` exists. If it does, skip to Phase 3 (Configure). The code changes are already in place.
|
||||
|
||||
If not, merge the skill branch:
|
||||
`.env`에 아래 중 하나 이상을 넣습니다.
|
||||
|
||||
```bash
|
||||
git remote add whatsapp https://github.com/qwibitai/nanoclaw-whatsapp.git 2>/dev/null
|
||||
git fetch whatsapp skill/voice-transcription
|
||||
git merge whatsapp/skill/voice-transcription
|
||||
npm install --legacy-peer-deps
|
||||
npm run build
|
||||
GROQ_API_KEY=gsk_... # 권장. 빠르고 무료 티어가 있음
|
||||
OPENAI_API_KEY=sk-... # fallback
|
||||
```
|
||||
|
||||
## Phase 2: Configure
|
||||
Groq를 쓰면 `whisper-large-v3-turbo`, OpenAI를 쓰면 `whisper-1` 경로를 탑니다.
|
||||
|
||||
### Get API key
|
||||
|
||||
**Groq (recommended — fast + free):**
|
||||
|
||||
> 1. Go to https://console.groq.com
|
||||
> 2. Sign up (no credit card needed)
|
||||
> 3. Create an API key (starts with `gsk_`)
|
||||
>
|
||||
> Free tier: 2,000 requests/day, 8 hours of audio/day. Uses `whisper-large-v3-turbo` at ~200x real-time speed.
|
||||
|
||||
**OpenAI (fallback):**
|
||||
|
||||
> 1. Go to https://platform.openai.com/api-keys
|
||||
> 2. Create a key (starts with `sk-`)
|
||||
>
|
||||
> Cost: ~$0.006 per minute of audio. Requires funded account.
|
||||
|
||||
### Add to environment
|
||||
|
||||
Add to `.env`:
|
||||
|
||||
```bash
|
||||
GROQ_API_KEY=gsk_... # Primary (fast, free)
|
||||
OPENAI_API_KEY=sk-... # Fallback (optional if Groq is set)
|
||||
```
|
||||
|
||||
### Build and restart
|
||||
## 2. 재시작
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
systemctl --user restart nanoclaw nanoclaw-codex # Linux
|
||||
# macOS: launchctl kickstart -k gui/$(id -u)/com.nanoclaw
|
||||
npm run setup -- --step service
|
||||
```
|
||||
|
||||
## Phase 3: Verify
|
||||
이미 서비스가 떠 있다면 플랫폼에 맞게 재시작만 해도 됩니다.
|
||||
|
||||
Send a voice note in any registered chat. The agent should receive it as `[Voice message transcription]: <text>`.
|
||||
## 3. 검증
|
||||
|
||||
### Check logs
|
||||
등록된 디스코드 채널에 음성 메시지나 오디오 첨부를 보냅니다. 정상이라면 에이전트 입력에 전사 텍스트가 포함됩니다.
|
||||
|
||||
```bash
|
||||
tail -f logs/nanoclaw.log | grep -iE "transcri|audio"
|
||||
tail -f logs/nanoclaw.log | grep -iE 'transcri|audio'
|
||||
```
|
||||
|
||||
Look for:
|
||||
- `Audio transcribed + cached` with `provider: "groq"` and `elapsed` — success
|
||||
- `Transcription cache hit` — second service read from cache (no duplicate API call)
|
||||
- `no transcription API key` — neither `GROQ_API_KEY` nor `OPENAI_API_KEY` set
|
||||
- `groq Whisper 4xx` — check key validity
|
||||
성공 신호:
|
||||
|
||||
- `Audio transcribed + cached`
|
||||
- `provider: "groq"` 또는 `provider: "openai"`
|
||||
- 동일 첨부 재처리 시 cache hit
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**No transcription:** Check `GROQ_API_KEY` (or `OPENAI_API_KEY`) is set in `.env`. Restart service after changes.
|
||||
**전사가 전혀 안 됨**
|
||||
|
||||
**Slow transcription:** Verify `provider: "groq"` in logs. If it shows `openai`, the Groq key may be missing or invalid.
|
||||
- `.env`에 `GROQ_API_KEY`나 `OPENAI_API_KEY`가 없는 경우가 대부분입니다.
|
||||
- 서비스 재시작 전에는 새 키가 반영되지 않습니다.
|
||||
|
||||
**Agent doesn't respond to voice notes:** Verify the chat is registered and the agent is running. Voice transcription only runs for registered groups.
|
||||
**너무 느림**
|
||||
|
||||
- 로그에 `provider: "groq"`가 안 보이면 Groq 키가 빠졌거나 잘못된 상태입니다.
|
||||
|
||||
**채널에서는 보이는데 에이전트가 못 읽음**
|
||||
|
||||
- 채널 등록과 서비스 상태를 먼저 확인합니다.
|
||||
- `npm run setup -- --step verify` 결과에서 `REGISTERED_GROUPS`와 `SERVICE`를 같이 봅니다.
|
||||
|
||||
@@ -1,368 +0,0 @@
|
||||
---
|
||||
name: add-whatsapp
|
||||
description: Add WhatsApp as a channel. Can replace other channels entirely or run alongside them. Uses QR code or pairing code for authentication.
|
||||
---
|
||||
|
||||
# Add WhatsApp Channel
|
||||
|
||||
This skill adds WhatsApp support to NanoClaw. It installs the WhatsApp channel code, dependencies, and guides through authentication, registration, and configuration.
|
||||
|
||||
## Phase 1: Pre-flight
|
||||
|
||||
### Check current state
|
||||
|
||||
Check if WhatsApp is already configured. If `store/auth/` exists with credential files, skip to Phase 4 (Registration) or Phase 5 (Verify).
|
||||
|
||||
```bash
|
||||
ls store/auth/creds.json 2>/dev/null && echo "WhatsApp auth exists" || echo "No WhatsApp auth"
|
||||
```
|
||||
|
||||
### Detect environment
|
||||
|
||||
Check whether the environment is headless (no display server):
|
||||
|
||||
```bash
|
||||
[[ -z "$DISPLAY" && -z "$WAYLAND_DISPLAY" && "$OSTYPE" != darwin* ]] && echo "IS_HEADLESS=true" || echo "IS_HEADLESS=false"
|
||||
```
|
||||
|
||||
### Ask the user
|
||||
|
||||
Use `AskUserQuestion` to collect configuration. **Adapt auth options based on environment:**
|
||||
|
||||
If IS_HEADLESS=true AND not WSL → AskUserQuestion: How do you want to authenticate WhatsApp?
|
||||
- **Pairing code** (Recommended) - Enter a numeric code on your phone (no camera needed, requires phone number)
|
||||
- **QR code in terminal** - Displays QR code in the terminal (can be too small on some displays)
|
||||
|
||||
Otherwise (macOS, desktop Linux, or WSL) → AskUserQuestion: How do you want to authenticate WhatsApp?
|
||||
- **QR code in browser** (Recommended) - Opens a browser window with a large, scannable QR code
|
||||
- **Pairing code** - Enter a numeric code on your phone (no camera needed, requires phone number)
|
||||
- **QR code in terminal** - Displays QR code in the terminal (can be too small on some displays)
|
||||
|
||||
If they chose pairing code:
|
||||
|
||||
AskUserQuestion: What is your phone number? (Include country code without +, e.g., 1234567890)
|
||||
|
||||
## Phase 2: Apply Code Changes
|
||||
|
||||
Check if `src/channels/whatsapp.ts` already exists. If it does, skip to Phase 3 (Authentication).
|
||||
|
||||
### Ensure channel remote
|
||||
|
||||
```bash
|
||||
git remote -v
|
||||
```
|
||||
|
||||
If `whatsapp` is missing, add it:
|
||||
|
||||
```bash
|
||||
git remote add whatsapp https://github.com/qwibitai/nanoclaw-whatsapp.git
|
||||
```
|
||||
|
||||
### Merge the skill branch
|
||||
|
||||
```bash
|
||||
git fetch whatsapp main
|
||||
git merge whatsapp/main
|
||||
```
|
||||
|
||||
This merges in:
|
||||
- `src/channels/whatsapp.ts` (WhatsAppChannel class with self-registration via `registerChannel`)
|
||||
- `src/channels/whatsapp.test.ts` (41 unit tests)
|
||||
- `src/whatsapp-auth.ts` (standalone WhatsApp authentication script)
|
||||
- `setup/whatsapp-auth.ts` (WhatsApp auth setup step)
|
||||
- `import './whatsapp.js'` appended to the channel barrel file `src/channels/index.ts`
|
||||
- `'whatsapp-auth'` step added to `setup/index.ts`
|
||||
- `@whiskeysockets/baileys`, `qrcode`, `qrcode-terminal` npm dependencies in `package.json`
|
||||
- `ASSISTANT_HAS_OWN_NUMBER` in `.env.example`
|
||||
|
||||
If the merge reports conflicts, resolve them by reading the conflicted files and understanding the intent of both sides.
|
||||
|
||||
### Validate code changes
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
npx vitest run src/channels/whatsapp.test.ts
|
||||
```
|
||||
|
||||
All tests must pass and build must be clean before proceeding.
|
||||
|
||||
## Phase 3: Authentication
|
||||
|
||||
### Clean previous auth state (if re-authenticating)
|
||||
|
||||
```bash
|
||||
rm -rf store/auth/
|
||||
```
|
||||
|
||||
### Run WhatsApp authentication
|
||||
|
||||
For QR code in browser (recommended):
|
||||
|
||||
```bash
|
||||
npx tsx setup/index.ts --step whatsapp-auth -- --method qr-browser
|
||||
```
|
||||
|
||||
(Bash timeout: 150000ms)
|
||||
|
||||
Tell the user:
|
||||
|
||||
> A browser window will open with a QR code.
|
||||
>
|
||||
> 1. Open WhatsApp > **Settings** > **Linked Devices** > **Link a Device**
|
||||
> 2. Scan the QR code in the browser
|
||||
> 3. The page will show "Authenticated!" when done
|
||||
|
||||
For QR code in terminal:
|
||||
|
||||
```bash
|
||||
npx tsx setup/index.ts --step whatsapp-auth -- --method qr-terminal
|
||||
```
|
||||
|
||||
Tell the user to run `npm run auth` in another terminal, then:
|
||||
|
||||
> 1. Open WhatsApp > **Settings** > **Linked Devices** > **Link a Device**
|
||||
> 2. Scan the QR code displayed in the terminal
|
||||
|
||||
For pairing code:
|
||||
|
||||
Tell the user to have WhatsApp open on **Settings > Linked Devices > Link a Device**, ready to tap **"Link with phone number instead"** — the code expires in ~60 seconds and must be entered immediately.
|
||||
|
||||
Run the auth process in the background and poll `store/pairing-code.txt` for the code:
|
||||
|
||||
```bash
|
||||
rm -f store/pairing-code.txt && npx tsx setup/index.ts --step whatsapp-auth -- --method pairing-code --phone <their-phone-number> > /tmp/wa-auth.log 2>&1 &
|
||||
```
|
||||
|
||||
Then immediately poll for the code (do NOT wait for the background command to finish):
|
||||
|
||||
```bash
|
||||
for i in $(seq 1 20); do [ -f store/pairing-code.txt ] && cat store/pairing-code.txt && break; sleep 1; done
|
||||
```
|
||||
|
||||
Display the code to the user the moment it appears. Tell them:
|
||||
|
||||
> **Enter this code now** — it expires in ~60 seconds.
|
||||
>
|
||||
> 1. Open WhatsApp > **Settings** > **Linked Devices** > **Link a Device**
|
||||
> 2. Tap **Link with phone number instead**
|
||||
> 3. Enter the code immediately
|
||||
|
||||
After the user enters the code, poll for authentication to complete:
|
||||
|
||||
```bash
|
||||
for i in $(seq 1 60); do grep -q 'AUTH_STATUS: authenticated' /tmp/wa-auth.log 2>/dev/null && echo "authenticated" && break; grep -q 'AUTH_STATUS: failed' /tmp/wa-auth.log 2>/dev/null && echo "failed" && break; sleep 2; done
|
||||
```
|
||||
|
||||
**If failed:** qr_timeout → re-run. logged_out → delete `store/auth/` and re-run. 515 → re-run. timeout → ask user, offer retry.
|
||||
|
||||
### Verify authentication succeeded
|
||||
|
||||
```bash
|
||||
test -f store/auth/creds.json && echo "Authentication successful" || echo "Authentication failed"
|
||||
```
|
||||
|
||||
### Configure environment
|
||||
|
||||
Channels auto-enable when their credentials are present — WhatsApp activates when `store/auth/creds.json` exists.
|
||||
|
||||
Sync to container environment:
|
||||
|
||||
```bash
|
||||
mkdir -p data/env && cp .env data/env/env
|
||||
```
|
||||
|
||||
## Phase 4: Registration
|
||||
|
||||
### Configure trigger and channel type
|
||||
|
||||
Get the bot's WhatsApp number: `node -e "const c=require('./store/auth/creds.json');console.log(c.me.id.split(':')[0].split('@')[0])"`
|
||||
|
||||
AskUserQuestion: Is this a shared phone number (personal WhatsApp) or a dedicated number (separate device)?
|
||||
- **Shared number** - Your personal WhatsApp number (recommended: use self-chat or a solo group)
|
||||
- **Dedicated number** - A separate phone/SIM for the assistant
|
||||
|
||||
AskUserQuestion: What trigger word should activate the assistant?
|
||||
- **@Andy** - Default trigger
|
||||
- **@Claw** - Short and easy
|
||||
- **@Claude** - Match the AI name
|
||||
|
||||
AskUserQuestion: What should the assistant call itself?
|
||||
- **Andy** - Default name
|
||||
- **Claw** - Short and easy
|
||||
- **Claude** - Match the AI name
|
||||
|
||||
AskUserQuestion: Where do you want to chat with the assistant?
|
||||
|
||||
**Shared number options:**
|
||||
- **Self-chat** (Recommended) - Chat in your own "Message Yourself" conversation
|
||||
- **Solo group** - A group with just you and the linked device
|
||||
- **Existing group** - An existing WhatsApp group
|
||||
|
||||
**Dedicated number options:**
|
||||
- **DM with bot** (Recommended) - Direct message the bot's number
|
||||
- **Solo group** - A group with just you and the bot
|
||||
- **Existing group** - An existing WhatsApp group
|
||||
|
||||
### Get the JID
|
||||
|
||||
**Self-chat:** JID = your phone number with `@s.whatsapp.net`. Extract from auth credentials:
|
||||
|
||||
```bash
|
||||
node -e "const c=JSON.parse(require('fs').readFileSync('store/auth/creds.json','utf-8'));console.log(c.me?.id?.split(':')[0]+'@s.whatsapp.net')"
|
||||
```
|
||||
|
||||
**DM with bot:** Ask for the bot's phone number. JID = `NUMBER@s.whatsapp.net`
|
||||
|
||||
**Group (solo, existing):** Run group sync and list available groups:
|
||||
|
||||
```bash
|
||||
npx tsx setup/index.ts --step groups
|
||||
npx tsx setup/index.ts --step groups --list
|
||||
```
|
||||
|
||||
The output shows `JID|GroupName` pairs. Present candidates as AskUserQuestion (names only, not JIDs).
|
||||
|
||||
### Register the chat
|
||||
|
||||
```bash
|
||||
npx tsx setup/index.ts --step register \
|
||||
--jid "<jid>" \
|
||||
--name "<chat-name>" \
|
||||
--trigger "@<trigger>" \
|
||||
--folder "whatsapp_main" \
|
||||
--channel whatsapp \
|
||||
--assistant-name "<name>" \
|
||||
--is-main \
|
||||
--no-trigger-required # Only for main/self-chat
|
||||
```
|
||||
|
||||
For additional groups (trigger-required):
|
||||
|
||||
```bash
|
||||
npx tsx setup/index.ts --step register \
|
||||
--jid "<group-jid>" \
|
||||
--name "<group-name>" \
|
||||
--trigger "@<trigger>" \
|
||||
--folder "whatsapp_<group-name>" \
|
||||
--channel whatsapp
|
||||
```
|
||||
|
||||
## Phase 5: Verify
|
||||
|
||||
### Build and restart
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Restart the service:
|
||||
|
||||
```bash
|
||||
# macOS (launchd)
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
|
||||
|
||||
# Linux (systemd)
|
||||
systemctl --user restart nanoclaw
|
||||
|
||||
# Linux (nohup fallback)
|
||||
bash start-nanoclaw.sh
|
||||
```
|
||||
|
||||
### Test the connection
|
||||
|
||||
Tell the user:
|
||||
|
||||
> Send a message to your registered WhatsApp chat:
|
||||
> - For self-chat / main: Any message works
|
||||
> - For groups: Use the trigger word (e.g., "@Andy hello")
|
||||
>
|
||||
> The assistant should respond within a few seconds.
|
||||
|
||||
### Check logs if needed
|
||||
|
||||
```bash
|
||||
tail -f logs/nanoclaw.log
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### QR code expired
|
||||
|
||||
QR codes expire after ~60 seconds. Re-run the auth command:
|
||||
|
||||
```bash
|
||||
rm -rf store/auth/ && npx tsx src/whatsapp-auth.ts
|
||||
```
|
||||
|
||||
### Pairing code not working
|
||||
|
||||
Codes expire in ~60 seconds. To retry:
|
||||
|
||||
```bash
|
||||
rm -rf store/auth/ && npx tsx src/whatsapp-auth.ts --pairing-code --phone <phone>
|
||||
```
|
||||
|
||||
Enter the code **immediately** when it appears. Also ensure:
|
||||
1. Phone number includes country code without `+` (e.g., `1234567890`)
|
||||
2. Phone has internet access
|
||||
3. WhatsApp is updated to the latest version
|
||||
|
||||
If pairing code keeps failing, switch to QR-browser auth instead:
|
||||
|
||||
```bash
|
||||
rm -rf store/auth/ && npx tsx setup/index.ts --step whatsapp-auth -- --method qr-browser
|
||||
```
|
||||
|
||||
### "conflict" disconnection
|
||||
|
||||
This happens when two instances connect with the same credentials. Ensure only one NanoClaw process is running:
|
||||
|
||||
```bash
|
||||
pkill -f "node dist/index.js"
|
||||
# Then restart
|
||||
```
|
||||
|
||||
### Bot not responding
|
||||
|
||||
Check:
|
||||
1. Auth credentials exist: `ls store/auth/creds.json`
|
||||
3. Chat is registered: `sqlite3 store/messages.db "SELECT * FROM registered_groups WHERE jid LIKE '%whatsapp%' OR jid LIKE '%@g.us' OR jid LIKE '%@s.whatsapp.net'"`
|
||||
4. Service is running: `launchctl list | grep nanoclaw` (macOS) or `systemctl --user status nanoclaw` (Linux)
|
||||
5. Logs: `tail -50 logs/nanoclaw.log`
|
||||
|
||||
### Group names not showing
|
||||
|
||||
Run group metadata sync:
|
||||
|
||||
```bash
|
||||
npx tsx setup/index.ts --step groups
|
||||
```
|
||||
|
||||
This fetches all group names from WhatsApp. Runs automatically every 24 hours.
|
||||
|
||||
## After Setup
|
||||
|
||||
If running `npm run dev` while the service is active:
|
||||
|
||||
```bash
|
||||
# macOS:
|
||||
launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist
|
||||
npm run dev
|
||||
# When done testing:
|
||||
launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
|
||||
|
||||
# Linux:
|
||||
# systemctl --user stop nanoclaw
|
||||
# npm run dev
|
||||
# systemctl --user start nanoclaw
|
||||
```
|
||||
|
||||
## Removal
|
||||
|
||||
To remove WhatsApp integration:
|
||||
|
||||
1. Delete auth credentials: `rm -rf store/auth/`
|
||||
2. Remove WhatsApp registrations: `sqlite3 store/messages.db "DELETE FROM registered_groups WHERE jid LIKE '%@g.us' OR jid LIKE '%@s.whatsapp.net'"`
|
||||
3. Sync env: `mkdir -p data/env && cp .env data/env/env`
|
||||
4. Rebuild and restart: `npm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `npm run build && systemctl --user restart nanoclaw` (Linux)
|
||||
@@ -1,175 +0,0 @@
|
||||
---
|
||||
name: convert-to-apple-container
|
||||
description: Switch from Docker to Apple Container for macOS-native container isolation. Use when the user wants Apple Container instead of Docker, or is setting up on macOS and prefers the native runtime. Triggers on "apple container", "convert to apple container", "switch to apple container", or "use apple container".
|
||||
---
|
||||
|
||||
# Convert to Apple Container
|
||||
|
||||
This skill switches NanoClaw's container runtime from Docker to Apple Container (macOS-only). It uses the skills engine for deterministic code changes, then walks through verification.
|
||||
|
||||
**What this changes:**
|
||||
- Container runtime binary: `docker` → `container`
|
||||
- Mount syntax: `-v path:path:ro` → `--mount type=bind,source=...,target=...,readonly`
|
||||
- Startup check: `docker info` → `container system status` (with auto-start)
|
||||
- Orphan detection: `docker ps --filter` → `container ls --format json`
|
||||
- Build script default: `docker` → `container`
|
||||
- Dockerfile entrypoint: `.env` shadowing via `mount --bind` inside the container (Apple Container only supports directory mounts, not file mounts like Docker's `/dev/null` overlay)
|
||||
- Container runner: main-group containers start as root for `mount --bind`, then drop privileges via `setpriv`
|
||||
|
||||
**What stays the same:**
|
||||
- Mount security/allowlist validation
|
||||
- All exported interfaces and IPC protocol
|
||||
- Non-main container behavior (still uses `--user` flag)
|
||||
- All other functionality
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Verify Apple Container is installed:
|
||||
|
||||
```bash
|
||||
container --version && echo "Apple Container ready" || echo "Install Apple Container first"
|
||||
```
|
||||
|
||||
If not installed:
|
||||
- Download from https://github.com/apple/container/releases
|
||||
- Install the `.pkg` file
|
||||
- Verify: `container --version`
|
||||
|
||||
Apple Container requires macOS. It does not work on Linux.
|
||||
|
||||
## Phase 1: Pre-flight
|
||||
|
||||
### Check if already applied
|
||||
|
||||
```bash
|
||||
grep "CONTAINER_RUNTIME_BIN" src/container-runtime.ts
|
||||
```
|
||||
|
||||
If it already shows `'container'`, the runtime is already Apple Container. Skip to Phase 3.
|
||||
|
||||
## Phase 2: Apply Code Changes
|
||||
|
||||
### Ensure upstream remote
|
||||
|
||||
```bash
|
||||
git remote -v
|
||||
```
|
||||
|
||||
If `upstream` is missing, add it:
|
||||
|
||||
```bash
|
||||
git remote add upstream https://github.com/qwibitai/nanoclaw.git
|
||||
```
|
||||
|
||||
### Merge the skill branch
|
||||
|
||||
```bash
|
||||
git fetch upstream skill/apple-container
|
||||
git merge upstream/skill/apple-container
|
||||
```
|
||||
|
||||
This merges in:
|
||||
- `src/container-runtime.ts` — Apple Container implementation (replaces Docker)
|
||||
- `src/container-runtime.test.ts` — Apple Container-specific tests
|
||||
- `src/container-runner.ts` — .env shadow mount fix and privilege dropping
|
||||
- `container/Dockerfile` — entrypoint that shadows .env via `mount --bind`
|
||||
- `container/build.sh` — default runtime set to `container`
|
||||
|
||||
If the merge reports conflicts, resolve them by reading the conflicted files and understanding the intent of both sides.
|
||||
|
||||
### Validate code changes
|
||||
|
||||
```bash
|
||||
npm test
|
||||
npm run build
|
||||
```
|
||||
|
||||
All tests must pass and build must be clean before proceeding.
|
||||
|
||||
## Phase 3: Verify
|
||||
|
||||
### Ensure Apple Container runtime is running
|
||||
|
||||
```bash
|
||||
container system status || container system start
|
||||
```
|
||||
|
||||
### Build the container image
|
||||
|
||||
```bash
|
||||
./container/build.sh
|
||||
```
|
||||
|
||||
### Test basic execution
|
||||
|
||||
```bash
|
||||
echo '{}' | container run -i --entrypoint /bin/echo nanoclaw-agent:latest "Container OK"
|
||||
```
|
||||
|
||||
### Test readonly mounts
|
||||
|
||||
```bash
|
||||
mkdir -p /tmp/test-ro && echo "test" > /tmp/test-ro/file.txt
|
||||
container run --rm --entrypoint /bin/bash \
|
||||
--mount type=bind,source=/tmp/test-ro,target=/test,readonly \
|
||||
nanoclaw-agent:latest \
|
||||
-c "cat /test/file.txt && touch /test/new.txt 2>&1 || echo 'Write blocked (expected)'"
|
||||
rm -rf /tmp/test-ro
|
||||
```
|
||||
|
||||
Expected: Read succeeds, write fails with "Read-only file system".
|
||||
|
||||
### Test read-write mounts
|
||||
|
||||
```bash
|
||||
mkdir -p /tmp/test-rw
|
||||
container run --rm --entrypoint /bin/bash \
|
||||
-v /tmp/test-rw:/test \
|
||||
nanoclaw-agent:latest \
|
||||
-c "echo 'test write' > /test/new.txt && cat /test/new.txt"
|
||||
cat /tmp/test-rw/new.txt && rm -rf /tmp/test-rw
|
||||
```
|
||||
|
||||
Expected: Both operations succeed.
|
||||
|
||||
### Full integration test
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
|
||||
```
|
||||
|
||||
Send a message via WhatsApp and verify the agent responds.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Apple Container not found:**
|
||||
- Download from https://github.com/apple/container/releases
|
||||
- Install the `.pkg` file
|
||||
- Verify: `container --version`
|
||||
|
||||
**Runtime won't start:**
|
||||
```bash
|
||||
container system start
|
||||
container system status
|
||||
```
|
||||
|
||||
**Image build fails:**
|
||||
```bash
|
||||
# Clean rebuild — Apple Container caches aggressively
|
||||
container builder stop && container builder rm && container builder start
|
||||
./container/build.sh
|
||||
```
|
||||
|
||||
**Container can't write to mounted directories:**
|
||||
Check directory permissions on the host. The container runs as uid 1000.
|
||||
|
||||
## Summary of Changed Files
|
||||
|
||||
| File | Type of Change |
|
||||
|------|----------------|
|
||||
| `src/container-runtime.ts` | Full replacement — Docker → Apple Container API |
|
||||
| `src/container-runtime.test.ts` | Full replacement — tests for Apple Container behavior |
|
||||
| `src/container-runner.ts` | .env shadow mount removed, main containers start as root with privilege drop |
|
||||
| `container/Dockerfile` | Entrypoint: `mount --bind` for .env shadowing, `setpriv` privilege drop |
|
||||
| `container/build.sh` | Default runtime: `docker` → `container` |
|
||||
@@ -1,115 +1,70 @@
|
||||
---
|
||||
name: customize
|
||||
description: Add new capabilities or modify NanoClaw behavior. Use when user wants to add channels (Telegram, Slack, email input), change triggers, add integrations, modify the router, or make any other customizations. This is an interactive skill that asks questions to understand what the user wants.
|
||||
description: Customize Discord-only NanoClaw behavior, routing, integrations, prompts, or commands.
|
||||
---
|
||||
|
||||
# NanoClaw Customization
|
||||
|
||||
This skill helps users add capabilities or modify behavior. Use AskUserQuestion to understand what they want before making changes.
|
||||
이 스킬은 현재 디스코드 전용 NanoClaw를 기준으로 동작합니다. 새 채널 추가보다 기존 디스코드 흐름, 러너, 프롬프트, 스케줄링, 도구 연결을 직접 수정하는 쪽을 우선합니다.
|
||||
|
||||
## Workflow
|
||||
## 진행 순서
|
||||
|
||||
1. **Install marketplace** - If feature skills aren't available yet, install the marketplace plugin:
|
||||
```bash
|
||||
claude plugin install nanoclaw-skills@nanoclaw-skills --scope project
|
||||
```
|
||||
This is hot-loaded — all feature skills become immediately available.
|
||||
2. **Understand the request** - Ask clarifying questions
|
||||
3. **Plan the changes** - Identify files to modify. If a skill exists for the request (e.g., `/add-telegram` for adding Telegram), invoke it instead of implementing manually.
|
||||
4. **Implement** - Make changes directly to the code
|
||||
5. **Test guidance** - Tell user how to verify
|
||||
1. 요청을 `행동 변경`, `명령 추가`, `도구 연동`, `설정/배포`, `프롬프트 수정` 중 어디에 속하는지 먼저 정리합니다.
|
||||
2. 영향 범위를 좁힙니다.
|
||||
3. 관련 파일만 수정합니다.
|
||||
4. 가능하면 바로 검증합니다.
|
||||
|
||||
## Key Files
|
||||
## 자주 보는 수정 지점
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/index.ts` | Orchestrator: state, message loop, agent invocation |
|
||||
| `src/channels/whatsapp.ts` | WhatsApp connection, auth, send/receive |
|
||||
| `src/ipc.ts` | IPC watcher and task processing |
|
||||
| `src/router.ts` | Message formatting and outbound routing |
|
||||
| `src/types.ts` | TypeScript interfaces (includes Channel) |
|
||||
| `src/config.ts` | Assistant name, trigger pattern, directories |
|
||||
| `src/db.ts` | Database initialization and queries |
|
||||
| `src/whatsapp-auth.ts` | Standalone WhatsApp authentication script |
|
||||
| `groups/CLAUDE.md` | Global memory/persona |
|
||||
- `src/channels/discord.ts` - 디스코드 메시지 파싱, 멘션 규칙, 첨부파일 처리, 음성 전사
|
||||
- `src/index.ts` - 오케스트레이션, 세션 명령, 라우팅, 상태 갱신
|
||||
- `src/session-commands.ts` - `/compact`, `/clear` 같은 세션 명령
|
||||
- `src/db.ts` - 등록 그룹, 세션, 스케줄, 마이그레이션
|
||||
- `src/config.ts` - 서비스 식별값, 디렉터리, 타임아웃, 기능 플래그
|
||||
- `src/agent-runner.ts` - 에이전트 실행과 환경 변수 전달
|
||||
- `runners/agent-runner/src/index.ts` - Claude Code 쪽 허용 도구와 MCP
|
||||
- `runners/codex-runner/src/index.ts` - Codex 쪽 실행 경로
|
||||
- `setup/register.ts` - 디스코드 채널 등록
|
||||
- `groups/global/CLAUDE.md` 및 각 그룹의 `CLAUDE.md` - 프롬프트/운영 규칙
|
||||
|
||||
## Common Customization Patterns
|
||||
## 요청별 가이드
|
||||
|
||||
### Adding a New Input Channel (e.g., Telegram, Slack, Email)
|
||||
### 동작을 바꾸고 싶을 때
|
||||
|
||||
Questions to ask:
|
||||
- Which channel? (Telegram, Slack, Discord, email, SMS, etc.)
|
||||
- Same trigger word or different?
|
||||
- Same memory hierarchy or separate?
|
||||
- Should messages from this channel go to existing groups or new ones?
|
||||
- 응답 조건, 멘션 처리, 첨부파일 처리: `src/channels/discord.ts`
|
||||
- 세션 명령, 상태머신, 런루프: `src/index.ts`, `src/session-commands.ts`
|
||||
- 페르소나/응답 스타일: `groups/global/CLAUDE.md`
|
||||
|
||||
Implementation pattern:
|
||||
1. Create `src/channels/{name}.ts` implementing the `Channel` interface from `src/types.ts` (see `src/channels/whatsapp.ts` for reference)
|
||||
2. Add the channel instance to `main()` in `src/index.ts` and wire callbacks (`onMessage`, `onChatMetadata`)
|
||||
3. Messages are stored via the `onMessage` callback; routing is automatic via `ownsJid()`
|
||||
### 도구나 MCP를 붙일 때
|
||||
|
||||
### Adding a New MCP Integration
|
||||
- 러너에 실제 도구 허용과 MCP 설정을 추가합니다.
|
||||
- Claude Code 쪽은 `runners/agent-runner/src/index.ts`를 먼저 보고, 필요하면 `src/agent-runner.ts`의 env 전달 범위를 같이 수정합니다.
|
||||
- 그룹별 사용 규칙은 해당 `CLAUDE.md`에 문서화합니다.
|
||||
|
||||
Questions to ask:
|
||||
- What service? (Calendar, Notion, database, etc.)
|
||||
- What operations needed? (read, write, both)
|
||||
- Which groups should have access?
|
||||
### 새 명령을 만들 때
|
||||
|
||||
Implementation:
|
||||
1. Add MCP server config to the container settings (see `src/container-runner.ts` for how MCP servers are mounted)
|
||||
2. Document available tools in `groups/CLAUDE.md`
|
||||
- 먼저 슬래시 명령인지, 자연어 규칙인지 구분합니다.
|
||||
- 슬래시/예약 명령이면 `src/session-commands.ts`와 `src/index.ts` 경로를 봅니다.
|
||||
- 자연어 지시만으로 충분하면 프롬프트 문서만 수정합니다.
|
||||
|
||||
### Changing Assistant Behavior
|
||||
### 등록/배포 흐름을 바꿀 때
|
||||
|
||||
Questions to ask:
|
||||
- What aspect? (name, trigger, persona, response style)
|
||||
- Apply to all groups or specific ones?
|
||||
- 설치/검증 단계는 `setup/*`
|
||||
- 서비스 동작은 `setup/service.ts`
|
||||
- 그룹 등록 형식은 `setup/register.ts`
|
||||
|
||||
Simple changes → edit `src/config.ts`
|
||||
Persona changes → edit `groups/CLAUDE.md`
|
||||
Per-group behavior → edit specific group's `CLAUDE.md`
|
||||
## 검증 원칙
|
||||
|
||||
### Adding New Commands
|
||||
작게 바꿨으면 최소 이 정도는 확인합니다.
|
||||
|
||||
Questions to ask:
|
||||
- What should the command do?
|
||||
- Available in all groups or main only?
|
||||
- Does it need new MCP tools?
|
||||
|
||||
Implementation:
|
||||
1. Commands are handled by the agent naturally — add instructions to `groups/CLAUDE.md` or the group's `CLAUDE.md`
|
||||
2. For trigger-level routing changes, modify `processGroupMessages()` in `src/index.ts`
|
||||
|
||||
### Changing Deployment
|
||||
|
||||
Questions to ask:
|
||||
- Target platform? (Linux server, Docker, different Mac)
|
||||
- Service manager? (systemd, Docker, supervisord)
|
||||
|
||||
Implementation:
|
||||
1. Create appropriate service files
|
||||
2. Update paths in config
|
||||
3. Provide setup instructions
|
||||
|
||||
## After Changes
|
||||
|
||||
Always tell the user:
|
||||
```bash
|
||||
# Rebuild and restart
|
||||
npm run build
|
||||
# macOS:
|
||||
launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist
|
||||
launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
|
||||
# Linux:
|
||||
# systemctl --user restart nanoclaw
|
||||
npm run typecheck
|
||||
npm test
|
||||
```
|
||||
|
||||
## Example Interaction
|
||||
러너나 실행 경로를 건드렸으면 추가로 확인합니다.
|
||||
|
||||
User: "Add Telegram as an input channel"
|
||||
|
||||
1. Ask: "Should Telegram use the same @Andy trigger, or a different one?"
|
||||
2. Ask: "Should Telegram messages create separate conversation contexts, or share with WhatsApp groups?"
|
||||
3. Create `src/channels/telegram.ts` implementing the `Channel` interface (see `src/channels/whatsapp.ts`)
|
||||
4. Add the channel to `main()` in `src/index.ts`
|
||||
5. Tell user how to authenticate and test
|
||||
```bash
|
||||
npm run build:runners
|
||||
npm run setup -- --step verify
|
||||
```
|
||||
|
||||
@@ -1,349 +1,91 @@
|
||||
---
|
||||
name: debug
|
||||
description: Debug container agent issues. Use when things aren't working, container fails, authentication problems, or to understand how the container system works. Covers logs, environment variables, mounts, and common issues.
|
||||
description: Debug NanoClaw runtime issues on the current Discord-only, host-process architecture.
|
||||
---
|
||||
|
||||
# NanoClaw Container Debugging
|
||||
# NanoClaw Debugging
|
||||
|
||||
This guide covers debugging the containerized agent execution system.
|
||||
현재 기준 NanoClaw는 디스코드 전용이고, 에이전트는 컨테이너가 아니라 호스트 프로세스로 실행됩니다. 디버깅은 `채널`, `서비스`, `러너`, `DB 등록`, `자격 증명` 순서로 좁혀갑니다.
|
||||
|
||||
## Architecture Overview
|
||||
## 빠른 점검 순서
|
||||
|
||||
```
|
||||
Host (macOS) Container (Linux VM)
|
||||
─────────────────────────────────────────────────────────────
|
||||
src/container-runner.ts container/agent-runner/
|
||||
│ │
|
||||
│ spawns container │ runs Claude Agent SDK
|
||||
│ with volume mounts │ with MCP servers
|
||||
│ │
|
||||
├── data/env/env ──────────────> /workspace/env-dir/env
|
||||
├── groups/{folder} ───────────> /workspace/group
|
||||
├── data/ipc/{folder} ────────> /workspace/ipc
|
||||
├── data/sessions/{folder}/.claude/ ──> /home/node/.claude/ (isolated per-group)
|
||||
└── (main only) project root ──> /workspace/project
|
||||
```
|
||||
1. 타입과 테스트부터 확인
|
||||
```bash
|
||||
npm run typecheck
|
||||
npm test
|
||||
```
|
||||
2. 서비스 상태 확인
|
||||
```bash
|
||||
npm run setup -- --step verify
|
||||
```
|
||||
3. 런타임 로그 확인
|
||||
```bash
|
||||
tail -f logs/nanoclaw.log
|
||||
tail -f logs/nanoclaw.error.log
|
||||
ls -t groups/*/logs/agent-*.log | head
|
||||
```
|
||||
4. 러너 빌드 확인
|
||||
```bash
|
||||
npm run build:runners
|
||||
```
|
||||
|
||||
**Important:** The container runs as user `node` with `HOME=/home/node`. Session files must be mounted to `/home/node/.claude/` (not `/root/.claude/`) for session resumption to work.
|
||||
## 핵심 파일
|
||||
|
||||
## Log Locations
|
||||
- `src/index.ts` - 메시지 루프, 세션 명령, 라우팅
|
||||
- `src/channels/discord.ts` - 디스코드 수신/송신, 멘션, 첨부파일, 음성 전사
|
||||
- `src/agent-runner.ts` - 러너 실행, 환경 변수 전달, 워크디렉터리 처리
|
||||
- `runners/agent-runner/src/index.ts` - Claude Code 러너
|
||||
- `runners/codex-runner/src/index.ts` - Codex 러너
|
||||
- `src/db.ts` - 등록 그룹, 세션, 스케줄 저장
|
||||
- `setup/register.ts` - 디스코드 채널 등록
|
||||
- `setup/verify.ts` - 설치 상태 검증
|
||||
|
||||
| Log | Location | Content |
|
||||
|-----|----------|---------|
|
||||
| **Main app logs** | `logs/nanoclaw.log` | Host-side WhatsApp, routing, container spawning |
|
||||
| **Main app errors** | `logs/nanoclaw.error.log` | Host-side errors |
|
||||
| **Container run logs** | `groups/{folder}/logs/container-*.log` | Per-run: input, mounts, stderr, stdout |
|
||||
| **Claude sessions** | `~/.claude/projects/` | Claude Code session history |
|
||||
## 자주 보는 문제
|
||||
|
||||
## Enabling Debug Logging
|
||||
|
||||
Set `LOG_LEVEL=debug` for verbose output:
|
||||
### 봇이 아예 말이 없음
|
||||
|
||||
```bash
|
||||
# For development
|
||||
LOG_LEVEL=debug npm run dev
|
||||
|
||||
# For launchd service (macOS), add to plist EnvironmentVariables:
|
||||
<key>LOG_LEVEL</key>
|
||||
<string>debug</string>
|
||||
# For systemd service (Linux), add to unit [Service] section:
|
||||
# Environment=LOG_LEVEL=debug
|
||||
grep -n '^DISCORD_BOT_TOKEN=' .env
|
||||
npm run setup -- --step verify
|
||||
```
|
||||
|
||||
Debug level shows:
|
||||
- Full mount configurations
|
||||
- Container command arguments
|
||||
- Real-time container stderr
|
||||
- `DISCORD_BOT_TOKEN`이 없으면 채널이 안 붙습니다.
|
||||
- `REGISTERED_GROUPS=0`이면 채널 등록이 안 된 상태입니다.
|
||||
- 메인 채널이 아니면 디스코드 멘션이나 트리거 조건을 먼저 확인합니다.
|
||||
|
||||
## Common Issues
|
||||
|
||||
### 1. "Claude Code process exited with code 1"
|
||||
|
||||
**Check the container log file** in `groups/{folder}/logs/container-*.log`
|
||||
|
||||
Common causes:
|
||||
|
||||
#### Missing Authentication
|
||||
```
|
||||
Invalid API key · Please run /login
|
||||
```
|
||||
**Fix:** Ensure `.env` file exists with either OAuth token or API key:
|
||||
```bash
|
||||
cat .env # Should show one of:
|
||||
# CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat01-... (subscription)
|
||||
# ANTHROPIC_API_KEY=sk-ant-api03-... (pay-per-use)
|
||||
```
|
||||
|
||||
#### Root User Restriction
|
||||
```
|
||||
--dangerously-skip-permissions cannot be used with root/sudo privileges
|
||||
```
|
||||
**Fix:** Container must run as non-root user. Check Dockerfile has `USER node`.
|
||||
|
||||
### 2. Environment Variables Not Passing
|
||||
|
||||
**Runtime note:** Environment variables passed via `-e` may be lost when using `-i` (interactive/piped stdin).
|
||||
|
||||
**Workaround:** The system extracts only authentication variables (`CLAUDE_CODE_OAUTH_TOKEN`, `ANTHROPIC_API_KEY`) from `.env` and mounts them for sourcing inside the container. Other env vars are not exposed.
|
||||
|
||||
To verify env vars are reaching the container:
|
||||
```bash
|
||||
echo '{}' | docker run -i \
|
||||
-v $(pwd)/data/env:/workspace/env-dir:ro \
|
||||
--entrypoint /bin/bash nanoclaw-agent:latest \
|
||||
-c 'export $(cat /workspace/env-dir/env | xargs); echo "OAuth: ${#CLAUDE_CODE_OAUTH_TOKEN} chars, API: ${#ANTHROPIC_API_KEY} chars"'
|
||||
```
|
||||
|
||||
### 3. Mount Issues
|
||||
|
||||
**Container mount notes:**
|
||||
- Docker supports both `-v` and `--mount` syntax
|
||||
- Use `:ro` suffix for readonly mounts:
|
||||
```bash
|
||||
# Readonly
|
||||
-v /path:/container/path:ro
|
||||
|
||||
# Read-write
|
||||
-v /path:/container/path
|
||||
```
|
||||
|
||||
To check what's mounted inside a container:
|
||||
```bash
|
||||
docker run --rm --entrypoint /bin/bash nanoclaw-agent:latest -c 'ls -la /workspace/'
|
||||
```
|
||||
|
||||
Expected structure:
|
||||
```
|
||||
/workspace/
|
||||
├── env-dir/env # Environment file (CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY)
|
||||
├── group/ # Current group folder (cwd)
|
||||
├── project/ # Project root (main channel only)
|
||||
├── global/ # Global CLAUDE.md (non-main only)
|
||||
├── ipc/ # Inter-process communication
|
||||
│ ├── messages/ # Outgoing WhatsApp messages
|
||||
│ ├── tasks/ # Scheduled task commands
|
||||
│ ├── current_tasks.json # Read-only: scheduled tasks visible to this group
|
||||
│ └── available_groups.json # Read-only: WhatsApp groups for activation (main only)
|
||||
└── extra/ # Additional custom mounts
|
||||
```
|
||||
|
||||
### 4. Permission Issues
|
||||
|
||||
The container runs as user `node` (uid 1000). Check ownership:
|
||||
```bash
|
||||
docker run --rm --entrypoint /bin/bash nanoclaw-agent:latest -c '
|
||||
whoami
|
||||
ls -la /workspace/
|
||||
ls -la /app/
|
||||
'
|
||||
```
|
||||
|
||||
All of `/workspace/` and `/app/` should be owned by `node`.
|
||||
|
||||
### 5. Session Not Resuming / "Claude Code process exited with code 1"
|
||||
|
||||
If sessions aren't being resumed (new session ID every time), or Claude Code exits with code 1 when resuming:
|
||||
|
||||
**Root cause:** The SDK looks for sessions at `$HOME/.claude/projects/`. Inside the container, `HOME=/home/node`, so it looks at `/home/node/.claude/projects/`.
|
||||
|
||||
**Check the mount path:**
|
||||
```bash
|
||||
# In container-runner.ts, verify mount is to /home/node/.claude/, NOT /root/.claude/
|
||||
grep -A3 "Claude sessions" src/container-runner.ts
|
||||
```
|
||||
|
||||
**Verify sessions are accessible:**
|
||||
```bash
|
||||
docker run --rm --entrypoint /bin/bash \
|
||||
-v ~/.claude:/home/node/.claude \
|
||||
nanoclaw-agent:latest -c '
|
||||
echo "HOME=$HOME"
|
||||
ls -la $HOME/.claude/projects/ 2>&1 | head -5
|
||||
'
|
||||
```
|
||||
|
||||
**Fix:** Ensure `container-runner.ts` mounts to `/home/node/.claude/`:
|
||||
```typescript
|
||||
mounts.push({
|
||||
hostPath: claudeDir,
|
||||
containerPath: '/home/node/.claude', // NOT /root/.claude
|
||||
readonly: false
|
||||
});
|
||||
```
|
||||
|
||||
### 6. MCP Server Failures
|
||||
|
||||
If an MCP server fails to start, the agent may exit. Check the container logs for MCP initialization errors.
|
||||
|
||||
## Manual Container Testing
|
||||
|
||||
### Test the full agent flow:
|
||||
```bash
|
||||
# Set up env file
|
||||
mkdir -p data/env groups/test
|
||||
cp .env data/env/env
|
||||
|
||||
# Run test query
|
||||
echo '{"prompt":"What is 2+2?","groupFolder":"test","chatJid":"test@g.us","isMain":false}' | \
|
||||
docker run -i \
|
||||
-v $(pwd)/data/env:/workspace/env-dir:ro \
|
||||
-v $(pwd)/groups/test:/workspace/group \
|
||||
-v $(pwd)/data/ipc:/workspace/ipc \
|
||||
nanoclaw-agent:latest
|
||||
```
|
||||
|
||||
### Test Claude Code directly:
|
||||
```bash
|
||||
docker run --rm --entrypoint /bin/bash \
|
||||
-v $(pwd)/data/env:/workspace/env-dir:ro \
|
||||
nanoclaw-agent:latest -c '
|
||||
export $(cat /workspace/env-dir/env | xargs)
|
||||
claude -p "Say hello" --dangerously-skip-permissions --allowedTools ""
|
||||
'
|
||||
```
|
||||
|
||||
### Interactive shell in container:
|
||||
```bash
|
||||
docker run --rm -it --entrypoint /bin/bash nanoclaw-agent:latest
|
||||
```
|
||||
|
||||
## SDK Options Reference
|
||||
|
||||
The agent-runner uses these Claude Agent SDK options:
|
||||
|
||||
```typescript
|
||||
query({
|
||||
prompt: input.prompt,
|
||||
options: {
|
||||
cwd: '/workspace/group',
|
||||
allowedTools: ['Bash', 'Read', 'Write', ...],
|
||||
permissionMode: 'bypassPermissions',
|
||||
allowDangerouslySkipPermissions: true, // Required with bypassPermissions
|
||||
settingSources: ['project'],
|
||||
mcpServers: { ... }
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Important:** `allowDangerouslySkipPermissions: true` is required when using `permissionMode: 'bypassPermissions'`. Without it, Claude Code exits with code 1.
|
||||
|
||||
## Rebuilding After Changes
|
||||
### 에이전트가 실행 직후 죽음
|
||||
|
||||
```bash
|
||||
# Rebuild main app
|
||||
npm run build
|
||||
|
||||
# Rebuild container (use --no-cache for clean rebuild)
|
||||
./container/build.sh
|
||||
|
||||
# Or force full rebuild
|
||||
docker builder prune -af
|
||||
./container/build.sh
|
||||
grep -nE '^(CLAUDE_CODE_OAUTH_TOKEN|ANTHROPIC_API_KEY|OPENAI_API_KEY|CODEX_OPENAI_API_KEY)=' .env
|
||||
ls -t groups/*/logs/agent-*.log | head -3
|
||||
```
|
||||
|
||||
## Checking Container Image
|
||||
- Claude 계열은 `CLAUDE_CODE_OAUTH_TOKEN` 또는 `ANTHROPIC_API_KEY`가 필요합니다.
|
||||
- Codex 계열은 `OPENAI_API_KEY` 또는 `CODEX_OPENAI_API_KEY`가 필요합니다.
|
||||
- 러너 로그에 인증 실패나 CLI 실행 오류가 바로 찍힙니다.
|
||||
|
||||
### 음성 전사가 안 됨
|
||||
|
||||
```bash
|
||||
# List images
|
||||
docker images
|
||||
|
||||
# Check what's in the image
|
||||
docker run --rm --entrypoint /bin/bash nanoclaw-agent:latest -c '
|
||||
echo "=== Node version ==="
|
||||
node --version
|
||||
|
||||
echo "=== Claude Code version ==="
|
||||
claude --version
|
||||
|
||||
echo "=== Installed packages ==="
|
||||
ls /app/node_modules/
|
||||
'
|
||||
grep -nE '^(GROQ_API_KEY|OPENAI_API_KEY)=' .env
|
||||
tail -f logs/nanoclaw.log | grep -iE 'transcri|audio|whisper|groq'
|
||||
```
|
||||
|
||||
## Session Persistence
|
||||
- 기본 우선순위는 Groq Whisper, 없으면 OpenAI Whisper fallback입니다.
|
||||
- 키가 없으면 디스코드 채널은 음성 첨부를 텍스트로 확장하지 못합니다.
|
||||
|
||||
Claude sessions are stored per-group in `data/sessions/{group}/.claude/` for security isolation. Each group has its own session directory, preventing cross-group access to conversation history.
|
||||
|
||||
**Critical:** The mount path must match the container user's HOME directory:
|
||||
- Container user: `node`
|
||||
- Container HOME: `/home/node`
|
||||
- Mount target: `/home/node/.claude/` (NOT `/root/.claude/`)
|
||||
|
||||
To clear sessions:
|
||||
### 등록은 되어 있는데 응답이 이상함
|
||||
|
||||
```bash
|
||||
# Clear all sessions for all groups
|
||||
rm -rf data/sessions/
|
||||
|
||||
# Clear sessions for a specific group
|
||||
rm -rf data/sessions/{groupFolder}/.claude/
|
||||
|
||||
# Also clear the session ID from NanoClaw's tracking (stored in SQLite)
|
||||
sqlite3 store/messages.db "DELETE FROM sessions WHERE group_folder = '{groupFolder}'"
|
||||
sqlite3 store/messages.db "select jid, folder, requires_trigger, is_main, agent_type from registered_groups;"
|
||||
```
|
||||
|
||||
To verify session resumption is working, check the logs for the same session ID across messages:
|
||||
```bash
|
||||
grep "Session initialized" logs/nanoclaw.log | tail -5
|
||||
# Should show the SAME session ID for consecutive messages in the same group
|
||||
```
|
||||
- `jid`는 `dc:<channel_id>` 형식이어야 합니다.
|
||||
- `folder`는 `discord_main` 또는 `discord_<name>` 형태를 유지합니다.
|
||||
- 세션 명령 문제면 `src/session-commands.ts`와 `src/index.ts` 호출부를 같이 봅니다.
|
||||
|
||||
## IPC Debugging
|
||||
## 원칙
|
||||
|
||||
The container communicates back to the host via files in `/workspace/ipc/`:
|
||||
|
||||
```bash
|
||||
# Check pending messages
|
||||
ls -la data/ipc/messages/
|
||||
|
||||
# Check pending task operations
|
||||
ls -la data/ipc/tasks/
|
||||
|
||||
# Read a specific IPC file
|
||||
cat data/ipc/messages/*.json
|
||||
|
||||
# Check available groups (main channel only)
|
||||
cat data/ipc/main/available_groups.json
|
||||
|
||||
# Check current tasks snapshot
|
||||
cat data/ipc/{groupFolder}/current_tasks.json
|
||||
```
|
||||
|
||||
**IPC file types:**
|
||||
- `messages/*.json` - Agent writes: outgoing WhatsApp messages
|
||||
- `tasks/*.json` - Agent writes: task operations (schedule, pause, resume, cancel, refresh_groups)
|
||||
- `current_tasks.json` - Host writes: read-only snapshot of scheduled tasks
|
||||
- `available_groups.json` - Host writes: read-only list of WhatsApp groups (main only)
|
||||
|
||||
## Quick Diagnostic Script
|
||||
|
||||
Run this to check common issues:
|
||||
|
||||
```bash
|
||||
echo "=== Checking NanoClaw Container Setup ==="
|
||||
|
||||
echo -e "\n1. Authentication configured?"
|
||||
[ -f .env ] && (grep -q "CLAUDE_CODE_OAUTH_TOKEN=sk-" .env || grep -q "ANTHROPIC_API_KEY=sk-" .env) && echo "OK" || echo "MISSING - add CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY to .env"
|
||||
|
||||
echo -e "\n2. Env file copied for container?"
|
||||
[ -f data/env/env ] && echo "OK" || echo "MISSING - will be created on first run"
|
||||
|
||||
echo -e "\n3. Container runtime running?"
|
||||
docker info &>/dev/null && echo "OK" || echo "NOT RUNNING - start Docker Desktop (macOS) or sudo systemctl start docker (Linux)"
|
||||
|
||||
echo -e "\n4. Container image exists?"
|
||||
echo '{}' | docker run -i --entrypoint /bin/echo nanoclaw-agent:latest "OK" 2>/dev/null || echo "MISSING - run ./container/build.sh"
|
||||
|
||||
echo -e "\n5. Session mount path correct?"
|
||||
grep -q "/home/node/.claude" src/container-runner.ts 2>/dev/null && echo "OK" || echo "WRONG - should mount to /home/node/.claude/, not /root/.claude/"
|
||||
|
||||
echo -e "\n6. Groups directory?"
|
||||
ls -la groups/ 2>/dev/null || echo "MISSING - run setup"
|
||||
|
||||
echo -e "\n7. Recent container logs?"
|
||||
ls -t groups/*/logs/container-*.log 2>/dev/null | head -3 || echo "No container logs yet"
|
||||
|
||||
echo -e "\n8. Session continuity working?"
|
||||
SESSIONS=$(grep "Session initialized" logs/nanoclaw.log 2>/dev/null | tail -5 | awk '{print $NF}' | sort -u | wc -l)
|
||||
[ "$SESSIONS" -le 2 ] && echo "OK (recent sessions reusing IDs)" || echo "CHECK - multiple different session IDs, may indicate resumption issues"
|
||||
```
|
||||
- 채널 문제와 에이전트 문제를 섞지 말고 분리해서 봅니다.
|
||||
- `.env`, DB 등록, 서비스, 러너 빌드 중 하나라도 틀리면 상위 증상이 비슷하게 보입니다.
|
||||
- 컨테이너 전제 문서는 무시하고 `runners/*`와 `setup/*` 기준으로 확인합니다.
|
||||
|
||||
@@ -1,203 +1,114 @@
|
||||
---
|
||||
name: setup
|
||||
description: Run initial NanoClaw setup. Use when user wants to install dependencies, authenticate messaging channels, register their main channel, or start the background services. Triggers on "setup", "install", "configure nanoclaw", or first-time setup requests.
|
||||
description: Run initial NanoClaw setup for the current Discord-only, host-process architecture.
|
||||
---
|
||||
|
||||
# NanoClaw Setup
|
||||
|
||||
Run setup steps automatically. Only pause when user action is required (channel authentication, configuration choices). Setup uses `bash setup.sh` for bootstrap, then `npx tsx setup/index.ts --step <name>` for all other steps. Steps emit structured status blocks to stdout. Verbose logs go to `logs/setup.log`.
|
||||
설치는 `bash setup.sh`로 부트스트랩하고, 나머지는 `npm run setup -- --step <name>`으로 진행합니다. 현재 기준 채널은 디스코드만 지원합니다.
|
||||
|
||||
**Principle:** When something is broken or missing, fix it. Don't tell the user to go fix it themselves unless it genuinely requires their manual action (e.g. authenticating a channel, pasting a secret token). If a dependency is missing, install it. If a service won't start, diagnose and repair. Ask the user for permission when needed, then do the work.
|
||||
|
||||
**UX Note:** Use `AskUserQuestion` for all user-facing questions.
|
||||
|
||||
**Architecture:** NanoClaw runs agents as direct host processes (no Docker/containers). Each agent type (Claude Code, Codex) has its own runner in `container/agent-runner/` and `container/codex-runner/`.
|
||||
|
||||
## 0. Git & Fork Setup
|
||||
|
||||
Check the git remote configuration to ensure the user has a fork and upstream is configured.
|
||||
|
||||
Run:
|
||||
- `git remote -v`
|
||||
|
||||
**Case A — `origin` points to `qwibitai/nanoclaw` (user cloned directly):**
|
||||
|
||||
The user cloned instead of forking. AskUserQuestion: "You cloned NanoClaw directly. We recommend forking so you can push your customizations. Would you like to set up a fork?"
|
||||
- Fork now (recommended) — walk them through it
|
||||
- Continue without fork — they'll only have local changes
|
||||
|
||||
If fork: instruct the user to fork `qwibitai/nanoclaw` on GitHub (they need to do this in their browser), then ask them for their GitHub username. Run:
|
||||
```bash
|
||||
git remote rename origin upstream
|
||||
git remote add origin https://github.com/<their-username>/nanoclaw.git
|
||||
git push --force origin main
|
||||
```
|
||||
Verify with `git remote -v`.
|
||||
|
||||
If continue without fork: add upstream so they can still pull updates:
|
||||
```bash
|
||||
git remote add upstream https://github.com/qwibitai/nanoclaw.git
|
||||
```
|
||||
|
||||
**Case B — `origin` points to user's fork, no `upstream` remote:**
|
||||
|
||||
Add upstream:
|
||||
```bash
|
||||
git remote add upstream https://github.com/qwibitai/nanoclaw.git
|
||||
```
|
||||
|
||||
**Case C — both `origin` (user's fork) and `upstream` (qwibitai) exist:**
|
||||
|
||||
Already configured. Continue.
|
||||
|
||||
**Verify:** `git remote -v` should show `origin` → user's repo, `upstream` → `qwibitai/nanoclaw.git`.
|
||||
|
||||
## 1. Bootstrap (Node.js + Dependencies)
|
||||
|
||||
Run `bash setup.sh` and parse the status block.
|
||||
|
||||
- If NODE_OK=false → Node.js is missing or too old. Use `AskUserQuestion: Would you like me to install Node.js 22?` If confirmed:
|
||||
- macOS: `brew install node@22` (if brew available) or install nvm then `nvm install 22`
|
||||
- Linux: `curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && sudo apt-get install -y nodejs`, or nvm
|
||||
- After installing Node, re-run `bash setup.sh`
|
||||
- If DEPS_OK=false → Read `logs/setup.log`. Try: delete `node_modules` and `package-lock.json`, re-run `bash setup.sh`. If native module build fails, install build tools (`xcode-select --install` on macOS, `build-essential` on Linux), then retry.
|
||||
- If NATIVE_OK=false → better-sqlite3 failed to load. Install build tools and re-run.
|
||||
- Record PLATFORM and IS_WSL for later steps.
|
||||
|
||||
## 2. Check Environment
|
||||
|
||||
Run `npx tsx setup/index.ts --step environment` and parse the status block.
|
||||
|
||||
- If HAS_AUTH=true → WhatsApp is already configured, note for step 5
|
||||
- If HAS_REGISTERED_GROUPS=true → note existing config, offer to skip or reconfigure
|
||||
|
||||
## 3. Build Agent Runners
|
||||
|
||||
Run `npx tsx setup/index.ts --step runners` and parse the status block.
|
||||
|
||||
This builds the agent runners that execute as host processes:
|
||||
- `container/agent-runner/` — Claude Code agent (via Claude Agent SDK)
|
||||
- `container/codex-runner/` — Codex agent (via Codex CLI)
|
||||
|
||||
**If BUILD_OK=false:** Read `logs/setup.log` for the error. Common fix: `cd container/agent-runner && npm install && npm run build`.
|
||||
|
||||
**If CODEX_RUNNER=false but AGENT_RUNNER=true:** Codex runner failed to build. Not critical if you only use Claude Code. Fix: `cd container/codex-runner && npm install && npm run build`.
|
||||
|
||||
## 4. Claude Authentication (No Script)
|
||||
|
||||
If HAS_ENV=true from step 2, read `.env` and check for `CLAUDE_CODE_OAUTH_TOKEN` or `ANTHROPIC_API_KEY`. If present, confirm with user: keep or reconfigure?
|
||||
|
||||
AskUserQuestion: Claude subscription (Pro/Max) vs Anthropic API key?
|
||||
|
||||
**Subscription:** Tell user to run `claude setup-token` in another terminal, copy the token, add `CLAUDE_CODE_OAUTH_TOKEN=<token>` to `.env`. Do NOT collect the token in chat.
|
||||
|
||||
**API key:** Tell user to add `ANTHROPIC_API_KEY=<key>` to `.env`.
|
||||
|
||||
### Codex Authentication (Optional)
|
||||
|
||||
AskUserQuestion: Do you also want to use Codex (OpenAI) agents?
|
||||
|
||||
If yes: Tell user to add `OPENAI_API_KEY=<key>` (or `CODEX_OPENAI_API_KEY=<key>`) to `.env`. The Codex runner will use this key.
|
||||
|
||||
### Voice Transcription (Optional)
|
||||
|
||||
AskUserQuestion: Do you want voice message transcription in Discord/WhatsApp?
|
||||
|
||||
If yes: Tell user to get a free API key at https://console.groq.com (no credit card needed) and add `GROQ_API_KEY=<key>` to `.env`. Uses Groq Whisper (whisper-large-v3-turbo, ~200x faster than OpenAI). Falls back to OpenAI Whisper (`OPENAI_API_KEY`) if Groq key is not set.
|
||||
|
||||
## 5. Install Skills Marketplace
|
||||
|
||||
Register and install the NanoClaw skills marketplace plugin so all feature skills (channel integrations, add-ons) are available:
|
||||
## 1. 부트스트랩
|
||||
|
||||
```bash
|
||||
claude plugin marketplace add qwibitai/nanoclaw-skills
|
||||
claude plugin marketplace update nanoclaw-skills
|
||||
claude plugin install nanoclaw-skills@nanoclaw-skills --scope project
|
||||
bash setup.sh
|
||||
```
|
||||
|
||||
The marketplace update ensures the local cache is fresh before installing. This is hot-loaded — no restart needed. All feature skills become immediately available.
|
||||
- Node 20 이상과 의존성이 준비되어야 합니다.
|
||||
- 실패하면 `logs/setup.log`를 먼저 봅니다.
|
||||
|
||||
## 6. Set Up Channels
|
||||
|
||||
AskUserQuestion (multiSelect): Which messaging channels do you want to enable?
|
||||
- WhatsApp (authenticates via QR code or pairing code)
|
||||
- Telegram (authenticates via bot token from @BotFather)
|
||||
- Slack (authenticates via Slack app with Socket Mode)
|
||||
- Discord (authenticates via Discord bot token)
|
||||
|
||||
**Delegate to each selected channel's own skill.** Each channel skill handles its own code installation, authentication, registration, and JID resolution. This avoids duplicating channel-specific logic and ensures JIDs are always correct.
|
||||
|
||||
For each selected channel, invoke its skill:
|
||||
|
||||
- **WhatsApp:** Invoke `/add-whatsapp`
|
||||
- **Telegram:** Invoke `/add-telegram`
|
||||
- **Slack:** Invoke `/add-slack`
|
||||
- **Discord:** Invoke `/add-discord`
|
||||
|
||||
Each skill will:
|
||||
1. Install the channel code (via `git merge` of the skill branch)
|
||||
2. Collect credentials/tokens and write to `.env`
|
||||
3. Authenticate (WhatsApp QR/pairing, or verify token-based connection)
|
||||
4. Register the chat with the correct JID format
|
||||
5. Build and verify
|
||||
|
||||
**After all channel skills complete**, install dependencies and rebuild — channel merges may introduce new packages:
|
||||
## 2. 현재 상태 확인
|
||||
|
||||
```bash
|
||||
npm install && npm run build
|
||||
npm run setup -- --step environment
|
||||
```
|
||||
|
||||
If the build fails, read the error output and fix it (usually a missing dependency). Then continue to step 7.
|
||||
여기서 확인할 것:
|
||||
|
||||
## 7. Mount Allowlist
|
||||
- `.env` 존재 여부
|
||||
- 기존 등록 그룹 존재 여부
|
||||
- 이미 초기화된 설치인지 여부
|
||||
|
||||
AskUserQuestion: Agent access to external directories?
|
||||
## 3. 필수 환경 변수
|
||||
|
||||
**No:** `npx tsx setup/index.ts --step mounts -- --empty`
|
||||
**Yes:** Collect paths/permissions. `npx tsx setup/index.ts --step mounts -- --json '{"allowedRoots":[...],"blockedPatterns":[],"nonMainReadOnly":true}'`
|
||||
`.env`에 최소한 아래 값이 있어야 합니다.
|
||||
|
||||
## 8. Start Service
|
||||
```bash
|
||||
DISCORD_BOT_TOKEN=...
|
||||
CLAUDE_CODE_OAUTH_TOKEN=... # 또는 ANTHROPIC_API_KEY=...
|
||||
```
|
||||
|
||||
If service already running: unload first.
|
||||
- macOS: `launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist`
|
||||
- Linux: `systemctl --user stop nanoclaw` (or `systemctl stop nanoclaw` if root)
|
||||
선택:
|
||||
|
||||
Run `npx tsx setup/index.ts --step service` and parse the status block.
|
||||
```bash
|
||||
OPENAI_API_KEY=... # Codex 사용 시
|
||||
CODEX_OPENAI_API_KEY=... # Codex 전용 키를 따로 쓸 때
|
||||
GROQ_API_KEY=... # Discord 음성 전사
|
||||
```
|
||||
|
||||
**If FALLBACK=wsl_no_systemd:** WSL without systemd detected. Tell user they can either enable systemd in WSL (`echo -e "[boot]\nsystemd=true" | sudo tee /etc/wsl.conf` then restart WSL) or use the generated `start-nanoclaw.sh` wrapper.
|
||||
## 4. 러너 빌드
|
||||
|
||||
**If SERVICE_LOADED=false:**
|
||||
- Read `logs/setup.log` for the error.
|
||||
- macOS: check `launchctl list | grep nanoclaw`. If PID=`-` and status non-zero, read `logs/nanoclaw.error.log`.
|
||||
- Linux: check `systemctl --user status nanoclaw`.
|
||||
- Re-run the service step after fixing.
|
||||
```bash
|
||||
npm run setup -- --step runners
|
||||
```
|
||||
|
||||
## 9. Verify
|
||||
이 단계는 아래 두 러너를 빌드합니다.
|
||||
|
||||
Run `npx tsx setup/index.ts --step verify` and parse the status block.
|
||||
- `runners/agent-runner`
|
||||
- `runners/codex-runner`
|
||||
|
||||
**If STATUS=failed, fix each:**
|
||||
- SERVICE=stopped → `npm run build`, then restart: `launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `systemctl --user restart nanoclaw` (Linux) or `bash start-nanoclaw.sh` (WSL nohup)
|
||||
- SERVICE=not_found → re-run step 8
|
||||
- CREDENTIALS=missing → re-run step 4
|
||||
- CHANNEL_AUTH shows `not_found` for any channel → re-invoke that channel's skill (e.g. `/add-telegram`)
|
||||
- REGISTERED_GROUPS=0 → re-invoke the channel skills from step 6
|
||||
- MOUNT_ALLOWLIST=missing → `npx tsx setup/index.ts --step mounts -- --empty`
|
||||
실패하면 보통 `npm run build:runners` 출력과 각 러너의 `package.json` 의존성을 같이 보면 됩니다.
|
||||
|
||||
Tell user to test: send a message in their registered chat. Show: `tail -f logs/nanoclaw.log`
|
||||
## 5. 디스코드 채널 등록
|
||||
|
||||
## Troubleshooting
|
||||
먼저 디스코드에서 개발자 모드를 켜고 채널 ID를 복사합니다. 등록 JID는 `dc:<channel_id>` 형식입니다.
|
||||
|
||||
**Service not starting:** Check `logs/nanoclaw.error.log`. Common: wrong Node path (re-run step 8), missing `.env` (step 4), missing channel credentials (re-invoke channel skill).
|
||||
메인 채널 예시:
|
||||
|
||||
**Agent fails ("Claude Code process exited with code 1"):** Check agent logs in `groups/<folder>/logs/agent-*.log`. Common causes: missing credentials in `.env`, `codex` CLI not in PATH, stale session IDs.
|
||||
```bash
|
||||
npm run setup -- --step register -- \
|
||||
--jid dc:123456789012345678 \
|
||||
--name "My Server #general" \
|
||||
--folder discord_main \
|
||||
--trigger @Andy \
|
||||
--is-main \
|
||||
--no-trigger-required
|
||||
```
|
||||
|
||||
**Codex agent not responding:** Ensure `OPENAI_API_KEY` is set in `.env` and `codex` CLI is installed globally (`npm install -g @openai/codex`). The runner needs `codex` in PATH.
|
||||
보조 채널 예시:
|
||||
|
||||
**Voice transcription not working:** Check `GROQ_API_KEY` (primary) or `OPENAI_API_KEY` (fallback) is set in `.env`. Test: send a voice message in Discord and check `logs/nanoclaw.log` for `Audio transcribed` entries with `provider` and `elapsed` fields.
|
||||
```bash
|
||||
npm run setup -- --step register -- \
|
||||
--jid dc:123456789012345678 \
|
||||
--name "My Server #ops" \
|
||||
--folder discord_ops \
|
||||
--trigger @Andy
|
||||
```
|
||||
|
||||
**No response to messages:** Check trigger pattern. Main channel doesn't need prefix. Check DB: `npx tsx setup/index.ts --step verify`. Check `logs/nanoclaw.log`.
|
||||
## 6. 서비스 시작
|
||||
|
||||
**Channel not connecting:** Verify the channel's credentials are set in `.env`. Channels auto-enable when their credentials are present. For WhatsApp: check `store/auth/creds.json` exists. For token-based channels: check token values in `.env`. Restart the service after any `.env` change.
|
||||
```bash
|
||||
npm run setup -- --step service
|
||||
```
|
||||
|
||||
**Unload service:** macOS: `launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist` | Linux: `systemctl --user stop nanoclaw`
|
||||
- macOS는 `launchd`
|
||||
- Linux는 `systemd` 또는 fallback wrapper
|
||||
|
||||
## 7. 최종 검증
|
||||
|
||||
```bash
|
||||
npm run setup -- --step verify
|
||||
```
|
||||
|
||||
성공 기준:
|
||||
|
||||
- 서비스가 running
|
||||
- Claude 인증이 configured
|
||||
- `CHANNEL_AUTH`에 `discord`
|
||||
- 등록 그룹 수가 1 이상
|
||||
|
||||
## 빠른 문제 해결
|
||||
|
||||
- 빌드 문제: `npm run typecheck`, `npm test`, `npm run build:runners`
|
||||
- 서비스 문제: `logs/nanoclaw.error.log`
|
||||
- 디스코드 연결 문제: `.env`의 `DISCORD_BOT_TOKEN`과 등록된 `dc:*` JID 확인
|
||||
- 응답 문제: `tail -f logs/nanoclaw.log`
|
||||
|
||||
@@ -1,235 +1,100 @@
|
||||
---
|
||||
name: update-nanoclaw
|
||||
description: Efficiently bring upstream NanoClaw updates into a customized install, with preview, selective cherry-pick, and low token usage.
|
||||
description: Safely merge upstream NanoClaw changes into a customized install with minimal drift.
|
||||
---
|
||||
|
||||
# About
|
||||
# Update NanoClaw
|
||||
|
||||
Your NanoClaw fork drifts from upstream as you customize it. This skill pulls upstream changes into your install without losing your modifications.
|
||||
커스텀된 NanoClaw에 upstream 변경을 가져올 때 쓰는 절차입니다. 현재 기준 중요 경로는 `src/`, `runners/`, `setup/`, `.claude/skills/`, `groups/`, `README.md` 입니다.
|
||||
|
||||
Run `/update-nanoclaw` in Claude Code.
|
||||
## 원칙
|
||||
|
||||
## How it works
|
||||
- 워킹트리가 더럽다면 진행하지 않습니다
|
||||
- 시작 전에 항상 백업 브랜치와 태그를 만듭니다
|
||||
- 먼저 `preview`, 그 다음 `merge` 또는 `cherry-pick` 순서로 갑니다
|
||||
- 충돌이 나면 충돌 파일만 엽니다
|
||||
|
||||
**Preflight**: checks for clean working tree (`git status --porcelain`). If `upstream` remote is missing, asks you for the URL (defaults to `https://github.com/qwibitai/nanoclaw.git`) and adds it. Detects the upstream branch name (`main` or `master`).
|
||||
## 1. 사전 점검
|
||||
|
||||
**Backup**: creates a timestamped backup branch and tag (`backup/pre-update-<hash>-<timestamp>`, `pre-update-<hash>-<timestamp>`) before touching anything. Safe to run multiple times.
|
||||
|
||||
**Preview**: runs `git log` and `git diff` against the merge base to show upstream changes since your last sync. Groups changed files into categories:
|
||||
- **Skills** (`.claude/skills/`): unlikely to conflict unless you edited an upstream skill
|
||||
- **Source** (`src/`): may conflict if you modified the same files
|
||||
- **Build/config** (`package.json`, `tsconfig*.json`, `container/`): review needed
|
||||
|
||||
**Update paths** (you pick one):
|
||||
- `merge` (default): `git merge upstream/<branch>`. Resolves all conflicts in one pass.
|
||||
- `cherry-pick`: `git cherry-pick <hashes>`. Pull in only the commits you want.
|
||||
- `rebase`: `git rebase upstream/<branch>`. Linear history, but conflicts resolve per-commit.
|
||||
- `abort`: just view the changelog, change nothing.
|
||||
|
||||
**Conflict preview**: before merging, runs a dry-run (`git merge --no-commit --no-ff`) to show which files would conflict. You can still abort at this point.
|
||||
|
||||
**Conflict resolution**: opens only conflicted files, resolves the conflict markers, keeps your local customizations intact.
|
||||
|
||||
**Validation**: runs `npm run build` and `npm test`.
|
||||
|
||||
**Breaking changes check**: after validation, reads CHANGELOG.md for any `[BREAKING]` entries introduced by the update. If found, shows each breaking change and offers to run the recommended skill to migrate.
|
||||
|
||||
## Rollback
|
||||
|
||||
The backup tag is printed at the end of each run:
|
||||
```
|
||||
git reset --hard pre-update-<hash>-<timestamp>
|
||||
```bash
|
||||
git status --porcelain
|
||||
git remote -v
|
||||
git fetch upstream --prune
|
||||
```
|
||||
|
||||
Backup branch `backup/pre-update-<hash>-<timestamp>` also exists.
|
||||
- 변경 파일이 남아 있으면 먼저 정리하게 합니다
|
||||
- `upstream`이 없으면 추가합니다
|
||||
- upstream 기본 브랜치가 `main`인지 먼저 확인합니다
|
||||
|
||||
## Token usage
|
||||
## 2. 백업
|
||||
|
||||
Only opens files with actual conflicts. Uses `git log`, `git diff`, and `git status` for everything else. Does not scan or refactor unrelated code.
|
||||
|
||||
---
|
||||
|
||||
# Goal
|
||||
Help a user with a customized NanoClaw install safely incorporate upstream changes without a fresh reinstall and without blowing tokens.
|
||||
|
||||
# Operating principles
|
||||
- Never proceed with a dirty working tree.
|
||||
- Always create a rollback point (backup branch + tag) before touching anything.
|
||||
- Prefer git-native operations (fetch, merge, cherry-pick). Do not manually rewrite files except conflict markers.
|
||||
- Default to MERGE (one-pass conflict resolution). Offer REBASE as an explicit option.
|
||||
- Keep token usage low: rely on `git status`, `git log`, `git diff`, and open only conflicted files.
|
||||
|
||||
# Step 0: Preflight (stop early if unsafe)
|
||||
Run:
|
||||
- `git status --porcelain`
|
||||
If output is non-empty:
|
||||
- Tell the user to commit or stash first, then stop.
|
||||
|
||||
Confirm remotes:
|
||||
- `git remote -v`
|
||||
If `upstream` is missing:
|
||||
- Ask the user for the upstream repo URL (default: `https://github.com/qwibitai/nanoclaw.git`).
|
||||
- Add it: `git remote add upstream <user-provided-url>`
|
||||
- Then: `git fetch upstream --prune`
|
||||
|
||||
Determine the upstream branch name:
|
||||
- `git branch -r | grep upstream/`
|
||||
- If `upstream/main` exists, use `main`.
|
||||
- If only `upstream/master` exists, use `master`.
|
||||
- Otherwise, ask the user which branch to use.
|
||||
- Store this as UPSTREAM_BRANCH for all subsequent commands. Every command below that references `upstream/main` should use `upstream/$UPSTREAM_BRANCH` instead.
|
||||
|
||||
Fetch:
|
||||
- `git fetch upstream --prune`
|
||||
|
||||
# Step 1: Create a safety net
|
||||
Capture current state:
|
||||
- `HASH=$(git rev-parse --short HEAD)`
|
||||
- `TIMESTAMP=$(date +%Y%m%d-%H%M%S)`
|
||||
|
||||
Create backup branch and tag (using timestamp to avoid collisions on retry):
|
||||
- `git branch backup/pre-update-$HASH-$TIMESTAMP`
|
||||
- `git tag pre-update-$HASH-$TIMESTAMP`
|
||||
|
||||
Save the tag name for later reference in the summary and rollback instructions.
|
||||
|
||||
# Step 2: Preview what upstream changed (no edits yet)
|
||||
Compute common base:
|
||||
- `BASE=$(git merge-base HEAD upstream/$UPSTREAM_BRANCH)`
|
||||
|
||||
Show upstream commits since BASE:
|
||||
- `git log --oneline $BASE..upstream/$UPSTREAM_BRANCH`
|
||||
|
||||
Show local commits since BASE (custom drift):
|
||||
- `git log --oneline $BASE..HEAD`
|
||||
|
||||
Show file-level impact from upstream:
|
||||
- `git diff --name-only $BASE..upstream/$UPSTREAM_BRANCH`
|
||||
|
||||
Bucket the upstream changed files:
|
||||
- **Skills** (`.claude/skills/`): unlikely to conflict unless the user edited an upstream skill
|
||||
- **Source** (`src/`): may conflict if user modified the same files
|
||||
- **Build/config** (`package.json`, `package-lock.json`, `tsconfig*.json`, `container/`, `launchd/`): review needed
|
||||
- **Other**: docs, tests, misc
|
||||
|
||||
Present these buckets to the user and ask them to choose one path using AskUserQuestion:
|
||||
- A) **Full update**: merge all upstream changes
|
||||
- B) **Selective update**: cherry-pick specific upstream commits
|
||||
- C) **Abort**: they only wanted the preview
|
||||
- D) **Rebase mode**: advanced, linear history (warn: resolves conflicts per-commit)
|
||||
|
||||
If Abort: stop here.
|
||||
|
||||
# Step 3: Conflict preview (before committing anything)
|
||||
If Full update or Rebase:
|
||||
- Dry-run merge to preview conflicts. Run these as a single chained command so the abort always executes:
|
||||
```
|
||||
git merge --no-commit --no-ff upstream/$UPSTREAM_BRANCH; git diff --name-only --diff-filter=U; git merge --abort
|
||||
```
|
||||
- If conflicts were listed: show them and ask user if they want to proceed.
|
||||
- If no conflicts: tell user it is clean and proceed.
|
||||
|
||||
# Step 4A: Full update (MERGE, default)
|
||||
Run:
|
||||
- `git merge upstream/$UPSTREAM_BRANCH --no-edit`
|
||||
|
||||
If conflicts occur:
|
||||
- Run `git status` and identify conflicted files.
|
||||
- For each conflicted file:
|
||||
- Open the file.
|
||||
- Resolve only conflict markers.
|
||||
- Preserve intentional local customizations.
|
||||
- Incorporate upstream fixes/improvements.
|
||||
- Do not refactor surrounding code.
|
||||
- `git add <file>`
|
||||
- When all resolved:
|
||||
- If merge did not auto-commit: `git commit --no-edit`
|
||||
|
||||
# Step 4B: Selective update (CHERRY-PICK)
|
||||
If user chose Selective:
|
||||
- Recompute BASE if needed: `BASE=$(git merge-base HEAD upstream/$UPSTREAM_BRANCH)`
|
||||
- Show commit list again: `git log --oneline $BASE..upstream/$UPSTREAM_BRANCH`
|
||||
- Ask user which commit hashes they want.
|
||||
- Apply: `git cherry-pick <hash1> <hash2> ...`
|
||||
|
||||
If conflicts during cherry-pick:
|
||||
- Resolve only conflict markers, then:
|
||||
- `git add <file>`
|
||||
- `git cherry-pick --continue`
|
||||
If user wants to stop:
|
||||
- `git cherry-pick --abort`
|
||||
|
||||
# Step 4C: Rebase (only if user explicitly chose option D)
|
||||
Run:
|
||||
- `git rebase upstream/$UPSTREAM_BRANCH`
|
||||
|
||||
If conflicts:
|
||||
- Resolve conflict markers only, then:
|
||||
- `git add <file>`
|
||||
- `git rebase --continue`
|
||||
If it gets messy (more than 3 rounds of conflicts):
|
||||
- `git rebase --abort`
|
||||
- Recommend merge instead.
|
||||
|
||||
# Step 5: Validation
|
||||
Run:
|
||||
- `npm run build`
|
||||
- `npm test` (do not fail the flow if tests are not configured)
|
||||
|
||||
If build fails:
|
||||
- Show the error.
|
||||
- Only fix issues clearly caused by the merge (missing imports, type mismatches from merged code).
|
||||
- Do not refactor unrelated code.
|
||||
- If unclear, ask the user before making changes.
|
||||
|
||||
# Step 6: Breaking changes check
|
||||
After validation succeeds, check if the update introduced any breaking changes.
|
||||
|
||||
Determine which CHANGELOG entries are new by diffing against the backup tag:
|
||||
- `git diff <backup-tag-from-step-1>..HEAD -- CHANGELOG.md`
|
||||
|
||||
Parse the diff output for lines starting with `+[BREAKING]`. Each such line is one breaking change entry. The format is:
|
||||
```
|
||||
[BREAKING] <description>. Run `/<skill-name>` to <action>.
|
||||
```bash
|
||||
HASH=$(git rev-parse --short HEAD)
|
||||
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
||||
git branch backup/pre-update-$HASH-$TIMESTAMP
|
||||
git tag pre-update-$HASH-$TIMESTAMP
|
||||
```
|
||||
|
||||
If no `[BREAKING]` lines are found:
|
||||
- Skip this step silently. Proceed to Step 7 (skill updates check).
|
||||
## 3. 미리 보기
|
||||
|
||||
If one or more `[BREAKING]` lines are found:
|
||||
- Display a warning header to the user: "This update includes breaking changes that may require action:"
|
||||
- For each breaking change, display the full description.
|
||||
- Collect all skill names referenced in the breaking change entries (the `/<skill-name>` part).
|
||||
- Use AskUserQuestion to ask the user which migration skills they want to run now. Options:
|
||||
- One option per referenced skill (e.g., "Run /add-whatsapp to re-add WhatsApp channel")
|
||||
- "Skip — I'll handle these manually"
|
||||
- Set `multiSelect: true` so the user can pick multiple skills if there are several breaking changes.
|
||||
- For each skill the user selects, invoke it using the Skill tool.
|
||||
- After all selected skills complete (or if user chose Skip), proceed to Step 7 (skill updates check).
|
||||
```bash
|
||||
BASE=$(git merge-base HEAD upstream/main)
|
||||
git log --oneline $BASE..upstream/main
|
||||
git log --oneline $BASE..HEAD
|
||||
git diff --name-only $BASE..upstream/main
|
||||
```
|
||||
|
||||
# Step 7: Check for skill updates
|
||||
After the summary, check if skills are distributed as branches in this repo:
|
||||
- `git branch -r --list 'upstream/skill/*'`
|
||||
파일은 대략 이렇게 나눠서 봅니다.
|
||||
|
||||
If any `upstream/skill/*` branches exist:
|
||||
- Use AskUserQuestion to ask: "Upstream has skill branches. Would you like to check for skill updates?"
|
||||
- Option 1: "Yes, check for updates" (description: "Runs /update-skills to check for and apply skill branch updates")
|
||||
- Option 2: "No, skip" (description: "You can run /update-skills later any time")
|
||||
- If user selects yes, invoke `/update-skills` using the Skill tool.
|
||||
- After the skill completes (or if user selected no), proceed to Step 8.
|
||||
- `.claude/skills/` - 스킬 문서/도우미
|
||||
- `src/` - 런타임과 채널 처리
|
||||
- `runners/` - Claude/Codex 실행 경로
|
||||
- `setup/` - 설치, 등록, 검증, 서비스
|
||||
- `README.md`, `groups/` - 운영 문서와 프롬프트
|
||||
|
||||
# Step 8: Summary + rollback instructions
|
||||
Show:
|
||||
- Backup tag: the tag name created in Step 1
|
||||
- New HEAD: `git rev-parse --short HEAD`
|
||||
- Upstream HEAD: `git rev-parse --short upstream/$UPSTREAM_BRANCH`
|
||||
- Conflicts resolved (list files, if any)
|
||||
- Breaking changes applied (list skills run, if any)
|
||||
- Remaining local diff vs upstream: `git diff --name-only upstream/$UPSTREAM_BRANCH..HEAD`
|
||||
## 4. 적용
|
||||
|
||||
Tell the user:
|
||||
- To rollback: `git reset --hard <backup-tag-from-step-1>`
|
||||
- Backup branch also exists: `backup/pre-update-<HASH>-<TIMESTAMP>`
|
||||
- Restart the service to apply changes:
|
||||
- If using launchd: `launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist && launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist`
|
||||
- If running manually: restart `npm run dev`
|
||||
기본은 merge 입니다.
|
||||
|
||||
```bash
|
||||
git merge upstream/main --no-edit
|
||||
```
|
||||
|
||||
일부 커밋만 가져올 거면 cherry-pick으로 갑니다.
|
||||
|
||||
```bash
|
||||
git cherry-pick <hash1> <hash2>
|
||||
```
|
||||
|
||||
## 5. 충돌 처리
|
||||
|
||||
- 충돌 파일만 엽니다
|
||||
- 로컬 커스터마이징은 유지합니다
|
||||
- 주변 리팩터링은 하지 않습니다
|
||||
- 해결 후 바로 `git add <file>` 합니다
|
||||
|
||||
## 6. 검증
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
npm test
|
||||
npm run build:runners
|
||||
npm run build
|
||||
```
|
||||
|
||||
런타임 경로가 바뀌었으면 추가로 확인합니다.
|
||||
|
||||
```bash
|
||||
npm run setup -- --step verify
|
||||
```
|
||||
|
||||
## 7. 요약
|
||||
|
||||
사용자에게 최소한 이 네 가지를 알려줍니다.
|
||||
|
||||
- 백업 태그 이름
|
||||
- 새 HEAD
|
||||
- 해결한 충돌 파일 목록
|
||||
- upstream 대비 아직 남은 로컬 diff
|
||||
|
||||
롤백 안내는 태그 기준으로 알려줍니다.
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
---
|
||||
name: use-local-whisper
|
||||
description: Use when the user wants local voice transcription instead of OpenAI Whisper API. Switches to whisper.cpp running on Apple Silicon. WhatsApp only for now. Requires voice-transcription skill to be applied first.
|
||||
---
|
||||
|
||||
# Use Local Whisper
|
||||
|
||||
Switches voice transcription from OpenAI's Whisper API to local whisper.cpp. Runs entirely on-device — no API key, no network, no cost.
|
||||
|
||||
**Channel support:** Currently WhatsApp only. The transcription module (`src/transcription.ts`) uses Baileys types for audio download. Other channels (Telegram, Discord, etc.) would need their own audio-download logic before this skill can serve them.
|
||||
|
||||
**Note:** The Homebrew package is `whisper-cpp`, but the CLI binary it installs is `whisper-cli`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `voice-transcription` skill must be applied first (WhatsApp channel)
|
||||
- macOS with Apple Silicon (M1+) recommended
|
||||
- `whisper-cpp` installed: `brew install whisper-cpp` (provides the `whisper-cli` binary)
|
||||
- `ffmpeg` installed: `brew install ffmpeg`
|
||||
- A GGML model file downloaded to `data/models/`
|
||||
|
||||
## Phase 1: Pre-flight
|
||||
|
||||
### Check if already applied
|
||||
|
||||
Check if `src/transcription.ts` already uses `whisper-cli`:
|
||||
|
||||
```bash
|
||||
grep 'whisper-cli' src/transcription.ts && echo "Already applied" || echo "Not applied"
|
||||
```
|
||||
|
||||
If already applied, skip to Phase 3 (Verify).
|
||||
|
||||
### Check dependencies are installed
|
||||
|
||||
```bash
|
||||
whisper-cli --help >/dev/null 2>&1 && echo "WHISPER_OK" || echo "WHISPER_MISSING"
|
||||
ffmpeg -version >/dev/null 2>&1 && echo "FFMPEG_OK" || echo "FFMPEG_MISSING"
|
||||
```
|
||||
|
||||
If missing, install via Homebrew:
|
||||
```bash
|
||||
brew install whisper-cpp ffmpeg
|
||||
```
|
||||
|
||||
### Check for model file
|
||||
|
||||
```bash
|
||||
ls data/models/ggml-*.bin 2>/dev/null || echo "NO_MODEL"
|
||||
```
|
||||
|
||||
If no model exists, download the base model (148MB, good balance of speed and accuracy):
|
||||
```bash
|
||||
mkdir -p data/models
|
||||
curl -L -o data/models/ggml-base.bin "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.bin"
|
||||
```
|
||||
|
||||
For better accuracy at the cost of speed, use `ggml-small.bin` (466MB) or `ggml-medium.bin` (1.5GB).
|
||||
|
||||
## Phase 2: Apply Code Changes
|
||||
|
||||
### Ensure WhatsApp fork remote
|
||||
|
||||
```bash
|
||||
git remote -v
|
||||
```
|
||||
|
||||
If `whatsapp` is missing, add it:
|
||||
|
||||
```bash
|
||||
git remote add whatsapp https://github.com/qwibitai/nanoclaw-whatsapp.git
|
||||
```
|
||||
|
||||
### Merge the skill branch
|
||||
|
||||
```bash
|
||||
git fetch whatsapp skill/local-whisper
|
||||
git merge whatsapp/skill/local-whisper
|
||||
```
|
||||
|
||||
This modifies `src/transcription.ts` to use the `whisper-cli` binary instead of the OpenAI API.
|
||||
|
||||
### Validate
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Phase 3: Verify
|
||||
|
||||
### Ensure launchd PATH includes Homebrew
|
||||
|
||||
The NanoClaw launchd service runs with a restricted PATH. `whisper-cli` and `ffmpeg` are in `/opt/homebrew/bin/` (Apple Silicon) or `/usr/local/bin/` (Intel), which may not be in the plist's PATH.
|
||||
|
||||
Check the current PATH:
|
||||
```bash
|
||||
grep -A1 'PATH' ~/Library/LaunchAgents/com.nanoclaw.plist
|
||||
```
|
||||
|
||||
If `/opt/homebrew/bin` is missing, add it to the `<string>` value inside the `PATH` key in the plist. Then reload:
|
||||
```bash
|
||||
launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist
|
||||
launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
|
||||
```
|
||||
|
||||
### Build and restart
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
|
||||
```
|
||||
|
||||
### Test
|
||||
|
||||
Send a voice note in any registered group. The agent should receive it as `[Voice: <transcript>]`.
|
||||
|
||||
### Check logs
|
||||
|
||||
```bash
|
||||
tail -f logs/nanoclaw.log | grep -i -E "voice|transcri|whisper"
|
||||
```
|
||||
|
||||
Look for:
|
||||
- `Transcribed voice message` — successful transcription
|
||||
- `whisper.cpp transcription failed` — check model path, ffmpeg, or PATH
|
||||
|
||||
## Configuration
|
||||
|
||||
Environment variables (optional, set in `.env`):
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `WHISPER_BIN` | `whisper-cli` | Path to whisper.cpp binary |
|
||||
| `WHISPER_MODEL` | `data/models/ggml-base.bin` | Path to GGML model file |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"whisper.cpp transcription failed"**: Ensure both `whisper-cli` and `ffmpeg` are in PATH. The launchd service uses a restricted PATH — see Phase 3 above. Test manually:
|
||||
```bash
|
||||
ffmpeg -f lavfi -i anullsrc=r=16000:cl=mono -t 1 -f wav /tmp/test.wav -y
|
||||
whisper-cli -m data/models/ggml-base.bin -f /tmp/test.wav --no-timestamps -nt
|
||||
```
|
||||
|
||||
**Transcription works in dev but not as service**: The launchd plist PATH likely doesn't include `/opt/homebrew/bin`. See "Ensure launchd PATH includes Homebrew" in Phase 3.
|
||||
|
||||
**Slow transcription**: The base model processes ~30s of audio in <1s on M1+. If slower, check CPU usage — another process may be competing.
|
||||
|
||||
**Wrong language**: whisper.cpp auto-detects language. To force a language, you can set `WHISPER_LANG` and modify `src/transcription.ts` to pass `-l $WHISPER_LANG`.
|
||||
@@ -1,417 +0,0 @@
|
||||
---
|
||||
name: x-integration
|
||||
description: X (Twitter) integration for NanoClaw. Post tweets, like, reply, retweet, and quote. Use for setup, testing, or troubleshooting X functionality. Triggers on "setup x", "x integration", "twitter", "post tweet", "tweet".
|
||||
---
|
||||
|
||||
# X (Twitter) Integration
|
||||
|
||||
Browser automation for X interactions via WhatsApp.
|
||||
|
||||
> **Compatibility:** NanoClaw v1.0.0. Directory structure may change in future versions.
|
||||
|
||||
## Features
|
||||
|
||||
| Action | Tool | Description |
|
||||
|--------|------|-------------|
|
||||
| Post | `x_post` | Publish new tweets |
|
||||
| Like | `x_like` | Like any tweet |
|
||||
| Reply | `x_reply` | Reply to tweets |
|
||||
| Retweet | `x_retweet` | Retweet without comment |
|
||||
| Quote | `x_quote` | Quote tweet with comment |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before using this skill, ensure:
|
||||
|
||||
1. **NanoClaw is installed and running** - WhatsApp connected, service active
|
||||
2. **Dependencies installed**:
|
||||
```bash
|
||||
npm ls playwright dotenv-cli || npm install playwright dotenv-cli
|
||||
```
|
||||
3. **CHROME_PATH configured** in `.env` (if Chrome is not at default location):
|
||||
```bash
|
||||
# Find your Chrome path
|
||||
mdfind "kMDItemCFBundleIdentifier == 'com.google.Chrome'" 2>/dev/null | head -1
|
||||
# Add to .env
|
||||
CHROME_PATH=/path/to/Google Chrome.app/Contents/MacOS/Google Chrome
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Setup authentication (interactive)
|
||||
npx dotenv -e .env -- npx tsx .claude/skills/x-integration/scripts/setup.ts
|
||||
# Verify: data/x-auth.json should exist after successful login
|
||||
|
||||
# 2. Rebuild container to include skill
|
||||
./container/build.sh
|
||||
# Verify: Output shows "COPY .claude/skills/x-integration/agent.ts"
|
||||
|
||||
# 3. Rebuild host and restart service
|
||||
npm run build
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# Linux: systemctl --user restart nanoclaw
|
||||
# Verify: launchctl list | grep nanoclaw (macOS) or systemctl --user status nanoclaw (Linux)
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `CHROME_PATH` | `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome` | Chrome executable path |
|
||||
| `NANOCLAW_ROOT` | `process.cwd()` | Project root directory |
|
||||
| `LOG_LEVEL` | `info` | Logging level (debug, info, warn, error) |
|
||||
|
||||
Set in `.env` file (loaded via `dotenv-cli` at runtime):
|
||||
|
||||
```bash
|
||||
# .env
|
||||
CHROME_PATH=/Applications/Google Chrome.app/Contents/MacOS/Google Chrome
|
||||
```
|
||||
|
||||
### Configuration File
|
||||
|
||||
Edit `lib/config.ts` to modify defaults:
|
||||
|
||||
```typescript
|
||||
export const config = {
|
||||
// Browser viewport
|
||||
viewport: { width: 1280, height: 800 },
|
||||
|
||||
// Timeouts (milliseconds)
|
||||
timeouts: {
|
||||
navigation: 30000, // Page navigation
|
||||
elementWait: 5000, // Wait for element
|
||||
afterClick: 1000, // Delay after click
|
||||
afterFill: 1000, // Delay after form fill
|
||||
afterSubmit: 3000, // Delay after submit
|
||||
pageLoad: 3000, // Initial page load
|
||||
},
|
||||
|
||||
// Tweet limits
|
||||
limits: {
|
||||
tweetMaxLength: 280,
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Data Directories
|
||||
|
||||
Paths relative to project root:
|
||||
|
||||
| Path | Purpose | Git |
|
||||
|------|---------|-----|
|
||||
| `data/x-browser-profile/` | Chrome profile with X session | Ignored |
|
||||
| `data/x-auth.json` | Auth state marker | Ignored |
|
||||
| `logs/nanoclaw.log` | Service logs (contains X operation logs) | Ignored |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Container (Linux VM) │
|
||||
│ └── agent.ts → MCP tool definitions (x_post, etc.) │
|
||||
│ └── Writes IPC request to /workspace/ipc/tasks/ │
|
||||
└──────────────────────┬──────────────────────────────────────┘
|
||||
│ IPC (file system)
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Host (macOS) │
|
||||
│ └── src/ipc.ts → processTaskIpc() │
|
||||
│ └── host.ts → handleXIpc() │
|
||||
│ └── spawn subprocess → scripts/*.ts │
|
||||
│ └── Playwright → Chrome → X Website │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Why This Design?
|
||||
|
||||
- **API is expensive** - X official API requires paid subscription ($100+/month) for posting
|
||||
- **Bot browsers get blocked** - X detects and bans headless browsers and common automation fingerprints
|
||||
- **Must use user's real browser** - Reuses the user's actual Chrome on Host with real browser fingerprint to avoid detection
|
||||
- **One-time authorization** - User logs in manually once, session persists in Chrome profile for future use
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
.claude/skills/x-integration/
|
||||
├── SKILL.md # This documentation
|
||||
├── host.ts # Host-side IPC handler
|
||||
├── agent.ts # Container-side MCP tool definitions
|
||||
├── lib/
|
||||
│ ├── config.ts # Centralized configuration
|
||||
│ └── browser.ts # Playwright utilities
|
||||
└── scripts/
|
||||
├── setup.ts # Interactive login
|
||||
├── post.ts # Post tweet
|
||||
├── like.ts # Like tweet
|
||||
├── reply.ts # Reply to tweet
|
||||
├── retweet.ts # Retweet
|
||||
└── quote.ts # Quote tweet
|
||||
```
|
||||
|
||||
### Integration Points
|
||||
|
||||
To integrate this skill into NanoClaw, make the following modifications:
|
||||
|
||||
---
|
||||
|
||||
**1. Host side: `src/ipc.ts`**
|
||||
|
||||
Add import after other local imports:
|
||||
```typescript
|
||||
import { handleXIpc } from '../.claude/skills/x-integration/host.js';
|
||||
```
|
||||
|
||||
Modify `processTaskIpc` function's switch statement default case:
|
||||
```typescript
|
||||
// Find:
|
||||
default:
|
||||
logger.warn({ type: data.type }, 'Unknown IPC task type');
|
||||
|
||||
// Replace with:
|
||||
default:
|
||||
const handled = await handleXIpc(data, sourceGroup, isMain, DATA_DIR);
|
||||
if (!handled) {
|
||||
logger.warn({ type: data.type }, 'Unknown IPC task type');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**2. Container side: `container/agent-runner/src/ipc-mcp.ts`**
|
||||
|
||||
Add import after `cron-parser` import:
|
||||
```typescript
|
||||
// @ts-ignore - Copied during Docker build from .claude/skills/x-integration/
|
||||
import { createXTools } from './skills/x-integration/agent.js';
|
||||
```
|
||||
|
||||
Add to the end of tools array (before the closing `]`):
|
||||
```typescript
|
||||
...createXTools({ groupFolder, isMain })
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**3. Build script: `container/build.sh`**
|
||||
|
||||
Change build context from `container/` to project root (required to access `.claude/skills/`):
|
||||
```bash
|
||||
# Find:
|
||||
docker build -t "${IMAGE_NAME}:${TAG}" .
|
||||
|
||||
# Replace with:
|
||||
cd "$SCRIPT_DIR/.."
|
||||
docker build -t "${IMAGE_NAME}:${TAG}" -f container/Dockerfile .
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**4. Dockerfile: `container/Dockerfile`**
|
||||
|
||||
First, update the build context paths (required to access `.claude/skills/` from project root):
|
||||
```dockerfile
|
||||
# Find:
|
||||
COPY agent-runner/package*.json ./
|
||||
...
|
||||
COPY agent-runner/ ./
|
||||
|
||||
# Replace with:
|
||||
COPY container/agent-runner/package*.json ./
|
||||
...
|
||||
COPY container/agent-runner/ ./
|
||||
```
|
||||
|
||||
Then add COPY line after `COPY container/agent-runner/ ./` and before `RUN npm run build`:
|
||||
```dockerfile
|
||||
# Copy skill MCP tools
|
||||
COPY .claude/skills/x-integration/agent.ts ./src/skills/x-integration/
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
All paths below are relative to project root (`NANOCLAW_ROOT`).
|
||||
|
||||
### 1. Check Chrome Path
|
||||
|
||||
```bash
|
||||
# Check if Chrome exists at configured path
|
||||
cat .env | grep CHROME_PATH
|
||||
ls -la "$(grep CHROME_PATH .env | cut -d= -f2)" 2>/dev/null || \
|
||||
echo "Chrome not found - update CHROME_PATH in .env"
|
||||
```
|
||||
|
||||
### 2. Run Authentication
|
||||
|
||||
```bash
|
||||
npx dotenv -e .env -- npx tsx .claude/skills/x-integration/scripts/setup.ts
|
||||
```
|
||||
|
||||
This opens Chrome for manual X login. Session saved to `data/x-browser-profile/`.
|
||||
|
||||
**Verify success:**
|
||||
```bash
|
||||
cat data/x-auth.json # Should show {"authenticated": true, ...}
|
||||
```
|
||||
|
||||
### 3. Rebuild Container
|
||||
|
||||
```bash
|
||||
./container/build.sh
|
||||
```
|
||||
|
||||
**Verify success:**
|
||||
```bash
|
||||
./container/build.sh 2>&1 | grep -i "agent.ts" # Should show COPY line
|
||||
```
|
||||
|
||||
### 4. Restart Service
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# Linux: systemctl --user restart nanoclaw
|
||||
```
|
||||
|
||||
**Verify success:**
|
||||
```bash
|
||||
launchctl list | grep nanoclaw # macOS — should show PID and exit code 0 or -
|
||||
# Linux: systemctl --user status nanoclaw
|
||||
```
|
||||
|
||||
## Usage via WhatsApp
|
||||
|
||||
Replace `@Assistant` with your configured trigger name (`ASSISTANT_NAME` in `.env`):
|
||||
|
||||
```
|
||||
@Assistant post a tweet: Hello world!
|
||||
|
||||
@Assistant like this tweet https://x.com/user/status/123
|
||||
|
||||
@Assistant reply to https://x.com/user/status/123 with: Great post!
|
||||
|
||||
@Assistant retweet https://x.com/user/status/123
|
||||
|
||||
@Assistant quote https://x.com/user/status/123 with comment: Interesting
|
||||
```
|
||||
|
||||
**Note:** Only the main group can use X tools. Other groups will receive an error.
|
||||
|
||||
## Testing
|
||||
|
||||
Scripts require environment variables from `.env`. Use `dotenv-cli` to load them:
|
||||
|
||||
### Check Authentication Status
|
||||
|
||||
```bash
|
||||
# Check if auth file exists and is valid
|
||||
cat data/x-auth.json 2>/dev/null && echo "Auth configured" || echo "Auth not configured"
|
||||
|
||||
# Check if browser profile exists
|
||||
ls -la data/x-browser-profile/ 2>/dev/null | head -5
|
||||
```
|
||||
|
||||
### Re-authenticate (if expired)
|
||||
|
||||
```bash
|
||||
npx dotenv -e .env -- npx tsx .claude/skills/x-integration/scripts/setup.ts
|
||||
```
|
||||
|
||||
### Test Post (will actually post)
|
||||
|
||||
```bash
|
||||
echo '{"content":"Test tweet - please ignore"}' | npx dotenv -e .env -- npx tsx .claude/skills/x-integration/scripts/post.ts
|
||||
```
|
||||
|
||||
### Test Like
|
||||
|
||||
```bash
|
||||
echo '{"tweetUrl":"https://x.com/user/status/123"}' | npx dotenv -e .env -- npx tsx .claude/skills/x-integration/scripts/like.ts
|
||||
```
|
||||
|
||||
Or export `CHROME_PATH` manually before running:
|
||||
|
||||
```bash
|
||||
export CHROME_PATH="/path/to/chrome"
|
||||
echo '{"content":"Test"}' | npx tsx .claude/skills/x-integration/scripts/post.ts
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Authentication Expired
|
||||
|
||||
```bash
|
||||
npx dotenv -e .env -- npx tsx .claude/skills/x-integration/scripts/setup.ts
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# Linux: systemctl --user restart nanoclaw
|
||||
```
|
||||
|
||||
### Browser Lock Files
|
||||
|
||||
If Chrome fails to launch:
|
||||
|
||||
```bash
|
||||
rm -f data/x-browser-profile/SingletonLock
|
||||
rm -f data/x-browser-profile/SingletonSocket
|
||||
rm -f data/x-browser-profile/SingletonCookie
|
||||
```
|
||||
|
||||
### Check Logs
|
||||
|
||||
```bash
|
||||
# Host logs (relative to project root)
|
||||
grep -i "x_post\|x_like\|x_reply\|handleXIpc" logs/nanoclaw.log | tail -20
|
||||
|
||||
# Script errors
|
||||
grep -i "error\|failed" logs/nanoclaw.log | tail -20
|
||||
```
|
||||
|
||||
### Script Timeout
|
||||
|
||||
Default timeout is 2 minutes (120s). Increase in `host.ts`:
|
||||
|
||||
```typescript
|
||||
const timer = setTimeout(() => {
|
||||
proc.kill('SIGTERM');
|
||||
resolve({ success: false, message: 'Script timed out (120s)' });
|
||||
}, 120000); // ← Increase this value
|
||||
```
|
||||
|
||||
### X UI Selector Changes
|
||||
|
||||
If X updates their UI, selectors in scripts may break. Current selectors:
|
||||
|
||||
| Element | Selector |
|
||||
|---------|----------|
|
||||
| Tweet input | `[data-testid="tweetTextarea_0"]` |
|
||||
| Post button | `[data-testid="tweetButtonInline"]` |
|
||||
| Reply button | `[data-testid="reply"]` |
|
||||
| Like | `[data-testid="like"]` |
|
||||
| Unlike | `[data-testid="unlike"]` |
|
||||
| Retweet | `[data-testid="retweet"]` |
|
||||
| Unretweet | `[data-testid="unretweet"]` |
|
||||
| Confirm retweet | `[data-testid="retweetConfirm"]` |
|
||||
| Modal dialog | `[role="dialog"][aria-modal="true"]` |
|
||||
| Modal submit | `[data-testid="tweetButton"]` |
|
||||
|
||||
### Container Build Issues
|
||||
|
||||
If MCP tools not found in container:
|
||||
|
||||
```bash
|
||||
# Verify build copies skill
|
||||
./container/build.sh 2>&1 | grep -i skill
|
||||
|
||||
# Check container has the file
|
||||
docker run nanoclaw-agent ls -la /app/src/skills/
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
- `data/x-browser-profile/` - Contains X session cookies (in `.gitignore`)
|
||||
- `data/x-auth.json` - Auth state marker (in `.gitignore`)
|
||||
- Only main group can use X tools (enforced in `agent.ts` and `host.ts`)
|
||||
- Scripts run as subprocesses with limited environment
|
||||
@@ -1,243 +0,0 @@
|
||||
/**
|
||||
* X Integration - MCP Tool Definitions (Agent/Container Side)
|
||||
*
|
||||
* These tools run inside the container and communicate with the host via IPC.
|
||||
* The host-side implementation is in host.ts.
|
||||
*
|
||||
* Note: This file is compiled in the container, not on the host.
|
||||
* The @ts-ignore is needed because the SDK is only available in the container.
|
||||
*/
|
||||
|
||||
// @ts-ignore - SDK available in container environment only
|
||||
import { tool } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { z } from 'zod';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// IPC directories (inside container)
|
||||
const IPC_DIR = '/workspace/ipc';
|
||||
const TASKS_DIR = path.join(IPC_DIR, 'tasks');
|
||||
const RESULTS_DIR = path.join(IPC_DIR, 'x_results');
|
||||
|
||||
function writeIpcFile(dir: string, data: object): string {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`;
|
||||
const filepath = path.join(dir, filename);
|
||||
const tempPath = `${filepath}.tmp`;
|
||||
fs.writeFileSync(tempPath, JSON.stringify(data, null, 2));
|
||||
fs.renameSync(tempPath, filepath);
|
||||
return filename;
|
||||
}
|
||||
|
||||
async function waitForResult(requestId: string, maxWait = 60000): Promise<{ success: boolean; message: string }> {
|
||||
const resultFile = path.join(RESULTS_DIR, `${requestId}.json`);
|
||||
const pollInterval = 1000;
|
||||
let elapsed = 0;
|
||||
|
||||
while (elapsed < maxWait) {
|
||||
if (fs.existsSync(resultFile)) {
|
||||
try {
|
||||
const result = JSON.parse(fs.readFileSync(resultFile, 'utf-8'));
|
||||
fs.unlinkSync(resultFile);
|
||||
return result;
|
||||
} catch (err) {
|
||||
return { success: false, message: `Failed to read result: ${err}` };
|
||||
}
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
||||
elapsed += pollInterval;
|
||||
}
|
||||
|
||||
return { success: false, message: 'Request timed out' };
|
||||
}
|
||||
|
||||
export interface SkillToolsContext {
|
||||
groupFolder: string;
|
||||
isMain: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create X integration MCP tools
|
||||
*/
|
||||
export function createXTools(ctx: SkillToolsContext) {
|
||||
const { groupFolder, isMain } = ctx;
|
||||
|
||||
return [
|
||||
tool(
|
||||
'x_post',
|
||||
`Post a tweet to X (Twitter). Main group only.
|
||||
|
||||
The host machine will execute the browser automation to post the tweet.
|
||||
Make sure the content is appropriate and within X's character limit (280 chars for text).`,
|
||||
{
|
||||
content: z.string().max(280).describe('The tweet content to post (max 280 characters)')
|
||||
},
|
||||
async (args: { content: string }) => {
|
||||
if (!isMain) {
|
||||
return {
|
||||
content: [{ type: 'text', text: 'Only the main group can post tweets.' }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
if (args.content.length > 280) {
|
||||
return {
|
||||
content: [{ type: 'text', text: `Tweet exceeds 280 character limit (current: ${args.content.length})` }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
const requestId = `xpost-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
writeIpcFile(TASKS_DIR, {
|
||||
type: 'x_post',
|
||||
requestId,
|
||||
content: args.content,
|
||||
groupFolder,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
const result = await waitForResult(requestId);
|
||||
return {
|
||||
content: [{ type: 'text', text: result.message }],
|
||||
isError: !result.success
|
||||
};
|
||||
}
|
||||
),
|
||||
|
||||
tool(
|
||||
'x_like',
|
||||
`Like a tweet on X (Twitter). Main group only.
|
||||
|
||||
Provide the tweet URL or tweet ID to like.`,
|
||||
{
|
||||
tweet_url: z.string().describe('The tweet URL (e.g., https://x.com/user/status/123) or tweet ID')
|
||||
},
|
||||
async (args: { tweet_url: string }) => {
|
||||
if (!isMain) {
|
||||
return {
|
||||
content: [{ type: 'text', text: 'Only the main group can interact with X.' }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
const requestId = `xlike-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
writeIpcFile(TASKS_DIR, {
|
||||
type: 'x_like',
|
||||
requestId,
|
||||
tweetUrl: args.tweet_url,
|
||||
groupFolder,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
const result = await waitForResult(requestId);
|
||||
return {
|
||||
content: [{ type: 'text', text: result.message }],
|
||||
isError: !result.success
|
||||
};
|
||||
}
|
||||
),
|
||||
|
||||
tool(
|
||||
'x_reply',
|
||||
`Reply to a tweet on X (Twitter). Main group only.
|
||||
|
||||
Provide the tweet URL and your reply content.`,
|
||||
{
|
||||
tweet_url: z.string().describe('The tweet URL (e.g., https://x.com/user/status/123) or tweet ID'),
|
||||
content: z.string().max(280).describe('The reply content (max 280 characters)')
|
||||
},
|
||||
async (args: { tweet_url: string; content: string }) => {
|
||||
if (!isMain) {
|
||||
return {
|
||||
content: [{ type: 'text', text: 'Only the main group can interact with X.' }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
const requestId = `xreply-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
writeIpcFile(TASKS_DIR, {
|
||||
type: 'x_reply',
|
||||
requestId,
|
||||
tweetUrl: args.tweet_url,
|
||||
content: args.content,
|
||||
groupFolder,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
const result = await waitForResult(requestId);
|
||||
return {
|
||||
content: [{ type: 'text', text: result.message }],
|
||||
isError: !result.success
|
||||
};
|
||||
}
|
||||
),
|
||||
|
||||
tool(
|
||||
'x_retweet',
|
||||
`Retweet a tweet on X (Twitter). Main group only.
|
||||
|
||||
Provide the tweet URL to retweet.`,
|
||||
{
|
||||
tweet_url: z.string().describe('The tweet URL (e.g., https://x.com/user/status/123) or tweet ID')
|
||||
},
|
||||
async (args: { tweet_url: string }) => {
|
||||
if (!isMain) {
|
||||
return {
|
||||
content: [{ type: 'text', text: 'Only the main group can interact with X.' }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
const requestId = `xretweet-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
writeIpcFile(TASKS_DIR, {
|
||||
type: 'x_retweet',
|
||||
requestId,
|
||||
tweetUrl: args.tweet_url,
|
||||
groupFolder,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
const result = await waitForResult(requestId);
|
||||
return {
|
||||
content: [{ type: 'text', text: result.message }],
|
||||
isError: !result.success
|
||||
};
|
||||
}
|
||||
),
|
||||
|
||||
tool(
|
||||
'x_quote',
|
||||
`Quote tweet on X (Twitter). Main group only.
|
||||
|
||||
Retweet with your own comment added.`,
|
||||
{
|
||||
tweet_url: z.string().describe('The tweet URL (e.g., https://x.com/user/status/123) or tweet ID'),
|
||||
comment: z.string().max(280).describe('Your comment for the quote tweet (max 280 characters)')
|
||||
},
|
||||
async (args: { tweet_url: string; comment: string }) => {
|
||||
if (!isMain) {
|
||||
return {
|
||||
content: [{ type: 'text', text: 'Only the main group can interact with X.' }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
const requestId = `xquote-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
writeIpcFile(TASKS_DIR, {
|
||||
type: 'x_quote',
|
||||
requestId,
|
||||
tweetUrl: args.tweet_url,
|
||||
comment: args.comment,
|
||||
groupFolder,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
const result = await waitForResult(requestId);
|
||||
return {
|
||||
content: [{ type: 'text', text: result.message }],
|
||||
isError: !result.success
|
||||
};
|
||||
}
|
||||
)
|
||||
];
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
/**
|
||||
* X Integration IPC Handler
|
||||
*
|
||||
* Handles all x_* IPC messages from container agents.
|
||||
* This is the entry point for X integration in the host process.
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import pino from 'pino';
|
||||
|
||||
const logger = pino({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
transport: { target: 'pino-pretty', options: { colorize: true } }
|
||||
});
|
||||
|
||||
interface SkillResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
data?: unknown;
|
||||
}
|
||||
|
||||
// Run a skill script as subprocess
|
||||
async function runScript(script: string, args: object): Promise<SkillResult> {
|
||||
const scriptPath = path.join(process.cwd(), '.claude', 'skills', 'x-integration', 'scripts', `${script}.ts`);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const proc = spawn('npx', ['tsx', scriptPath], {
|
||||
cwd: process.cwd(),
|
||||
env: { ...process.env, NANOCLAW_ROOT: process.cwd() },
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
proc.stdout.on('data', (data) => { stdout += data.toString(); });
|
||||
proc.stdin.write(JSON.stringify(args));
|
||||
proc.stdin.end();
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
proc.kill('SIGTERM');
|
||||
resolve({ success: false, message: 'Script timed out (120s)' });
|
||||
}, 120000);
|
||||
|
||||
proc.on('close', (code) => {
|
||||
clearTimeout(timer);
|
||||
if (code !== 0) {
|
||||
resolve({ success: false, message: `Script exited with code: ${code}` });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const lines = stdout.trim().split('\n');
|
||||
resolve(JSON.parse(lines[lines.length - 1]));
|
||||
} catch {
|
||||
resolve({ success: false, message: `Failed to parse output: ${stdout.slice(0, 200)}` });
|
||||
}
|
||||
});
|
||||
|
||||
proc.on('error', (err) => {
|
||||
clearTimeout(timer);
|
||||
resolve({ success: false, message: `Failed to spawn: ${err.message}` });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Write result to IPC results directory
|
||||
function writeResult(dataDir: string, sourceGroup: string, requestId: string, result: SkillResult): void {
|
||||
const resultsDir = path.join(dataDir, 'ipc', sourceGroup, 'x_results');
|
||||
fs.mkdirSync(resultsDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(resultsDir, `${requestId}.json`), JSON.stringify(result));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle X integration IPC messages
|
||||
*
|
||||
* @returns true if message was handled, false if not an X message
|
||||
*/
|
||||
export async function handleXIpc(
|
||||
data: Record<string, unknown>,
|
||||
sourceGroup: string,
|
||||
isMain: boolean,
|
||||
dataDir: string
|
||||
): Promise<boolean> {
|
||||
const type = data.type as string;
|
||||
|
||||
// Only handle x_* types
|
||||
if (!type?.startsWith('x_')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only main group can use X integration
|
||||
if (!isMain) {
|
||||
logger.warn({ sourceGroup, type }, 'X integration blocked: not main group');
|
||||
return true;
|
||||
}
|
||||
|
||||
const requestId = data.requestId as string;
|
||||
if (!requestId) {
|
||||
logger.warn({ type }, 'X integration blocked: missing requestId');
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.info({ type, requestId }, 'Processing X request');
|
||||
|
||||
let result: SkillResult;
|
||||
|
||||
switch (type) {
|
||||
case 'x_post':
|
||||
if (!data.content) {
|
||||
result = { success: false, message: 'Missing content' };
|
||||
break;
|
||||
}
|
||||
result = await runScript('post', { content: data.content });
|
||||
break;
|
||||
|
||||
case 'x_like':
|
||||
if (!data.tweetUrl) {
|
||||
result = { success: false, message: 'Missing tweetUrl' };
|
||||
break;
|
||||
}
|
||||
result = await runScript('like', { tweetUrl: data.tweetUrl });
|
||||
break;
|
||||
|
||||
case 'x_reply':
|
||||
if (!data.tweetUrl || !data.content) {
|
||||
result = { success: false, message: 'Missing tweetUrl or content' };
|
||||
break;
|
||||
}
|
||||
result = await runScript('reply', { tweetUrl: data.tweetUrl, content: data.content });
|
||||
break;
|
||||
|
||||
case 'x_retweet':
|
||||
if (!data.tweetUrl) {
|
||||
result = { success: false, message: 'Missing tweetUrl' };
|
||||
break;
|
||||
}
|
||||
result = await runScript('retweet', { tweetUrl: data.tweetUrl });
|
||||
break;
|
||||
|
||||
case 'x_quote':
|
||||
if (!data.tweetUrl || !data.comment) {
|
||||
result = { success: false, message: 'Missing tweetUrl or comment' };
|
||||
break;
|
||||
}
|
||||
result = await runScript('quote', { tweetUrl: data.tweetUrl, comment: data.comment });
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
writeResult(dataDir, sourceGroup, requestId, result);
|
||||
if (result.success) {
|
||||
logger.info({ type, requestId }, 'X request completed');
|
||||
} else {
|
||||
logger.error({ type, requestId, message: result.message }, 'X request failed');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
/**
|
||||
* X Integration - Shared utilities
|
||||
* Used by all X scripts
|
||||
*/
|
||||
|
||||
import { chromium, BrowserContext, Page } from 'playwright';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { config } from './config.js';
|
||||
|
||||
export { config };
|
||||
|
||||
export interface ScriptResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
data?: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read input from stdin
|
||||
*/
|
||||
export async function readInput<T>(): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let data = '';
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', chunk => { data += chunk; });
|
||||
process.stdin.on('end', () => {
|
||||
try {
|
||||
resolve(JSON.parse(data));
|
||||
} catch (err) {
|
||||
reject(new Error(`Invalid JSON input: ${err}`));
|
||||
}
|
||||
});
|
||||
process.stdin.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Write result to stdout
|
||||
*/
|
||||
export function writeResult(result: ScriptResult): void {
|
||||
console.log(JSON.stringify(result));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up browser lock files
|
||||
*/
|
||||
export function cleanupLockFiles(): void {
|
||||
for (const lockFile of ['SingletonLock', 'SingletonSocket', 'SingletonCookie']) {
|
||||
const lockPath = path.join(config.browserDataDir, lockFile);
|
||||
if (fs.existsSync(lockPath)) {
|
||||
try { fs.unlinkSync(lockPath); } catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate tweet/reply content
|
||||
*/
|
||||
export function validateContent(content: string | undefined, type = 'Tweet'): ScriptResult | null {
|
||||
if (!content || content.length === 0) {
|
||||
return { success: false, message: `${type} content cannot be empty` };
|
||||
}
|
||||
if (content.length > config.limits.tweetMaxLength) {
|
||||
return { success: false, message: `${type} exceeds ${config.limits.tweetMaxLength} character limit (current: ${content.length})` };
|
||||
}
|
||||
return null; // Valid
|
||||
}
|
||||
|
||||
/**
|
||||
* Get browser context with persistent profile
|
||||
*/
|
||||
export async function getBrowserContext(): Promise<BrowserContext> {
|
||||
if (!fs.existsSync(config.authPath)) {
|
||||
throw new Error('X authentication not configured. Run /x-integration to complete login.');
|
||||
}
|
||||
|
||||
cleanupLockFiles();
|
||||
|
||||
const context = await chromium.launchPersistentContext(config.browserDataDir, {
|
||||
executablePath: config.chromePath,
|
||||
headless: false,
|
||||
viewport: config.viewport,
|
||||
args: config.chromeArgs,
|
||||
ignoreDefaultArgs: config.chromeIgnoreDefaultArgs,
|
||||
});
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract tweet ID from URL or raw ID
|
||||
*/
|
||||
export function extractTweetId(input: string): string | null {
|
||||
const urlMatch = input.match(/(?:x\.com|twitter\.com)\/\w+\/status\/(\d+)/);
|
||||
if (urlMatch) return urlMatch[1];
|
||||
if (/^\d+$/.test(input.trim())) return input.trim();
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a tweet page
|
||||
*/
|
||||
export async function navigateToTweet(
|
||||
context: BrowserContext,
|
||||
tweetUrl: string
|
||||
): Promise<{ page: Page; success: boolean; error?: string }> {
|
||||
const page = context.pages()[0] || await context.newPage();
|
||||
|
||||
let url = tweetUrl;
|
||||
const tweetId = extractTweetId(tweetUrl);
|
||||
if (tweetId && !tweetUrl.startsWith('http')) {
|
||||
url = `https://x.com/i/status/${tweetId}`;
|
||||
}
|
||||
|
||||
try {
|
||||
await page.goto(url, { timeout: config.timeouts.navigation, waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(config.timeouts.pageLoad);
|
||||
|
||||
const exists = await page.locator('article[data-testid="tweet"]').first().isVisible().catch(() => false);
|
||||
if (!exists) {
|
||||
return { page, success: false, error: 'Tweet not found. It may have been deleted or the URL is invalid.' };
|
||||
}
|
||||
|
||||
return { page, success: true };
|
||||
} catch (err) {
|
||||
return { page, success: false, error: `Navigation failed: ${err instanceof Error ? err.message : String(err)}` };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run script with error handling
|
||||
*/
|
||||
export async function runScript<T>(
|
||||
handler: (input: T) => Promise<ScriptResult>
|
||||
): Promise<void> {
|
||||
try {
|
||||
const input = await readInput<T>();
|
||||
const result = await handler(input);
|
||||
writeResult(result);
|
||||
} catch (err) {
|
||||
writeResult({
|
||||
success: false,
|
||||
message: `Script execution failed: ${err instanceof Error ? err.message : String(err)}`
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
/**
|
||||
* X Integration - Configuration
|
||||
*
|
||||
* All environment-specific settings in one place.
|
||||
* Override via environment variables or modify defaults here.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
|
||||
// Project root - can be overridden for different deployments
|
||||
const PROJECT_ROOT = process.env.NANOCLAW_ROOT || process.cwd();
|
||||
|
||||
/**
|
||||
* Configuration object with all settings
|
||||
*/
|
||||
export const config = {
|
||||
// Chrome executable path
|
||||
// Default: standard macOS Chrome location
|
||||
// Override: CHROME_PATH environment variable
|
||||
chromePath: process.env.CHROME_PATH || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
|
||||
// Browser profile directory for persistent login sessions
|
||||
browserDataDir: path.join(PROJECT_ROOT, 'data', 'x-browser-profile'),
|
||||
|
||||
// Auth state marker file
|
||||
authPath: path.join(PROJECT_ROOT, 'data', 'x-auth.json'),
|
||||
|
||||
// Browser viewport settings
|
||||
viewport: {
|
||||
width: 1280,
|
||||
height: 800,
|
||||
},
|
||||
|
||||
// Timeouts (in milliseconds)
|
||||
timeouts: {
|
||||
navigation: 30000,
|
||||
elementWait: 5000,
|
||||
afterClick: 1000,
|
||||
afterFill: 1000,
|
||||
afterSubmit: 3000,
|
||||
pageLoad: 3000,
|
||||
},
|
||||
|
||||
// X character limits
|
||||
limits: {
|
||||
tweetMaxLength: 280,
|
||||
},
|
||||
|
||||
// Chrome launch arguments
|
||||
chromeArgs: [
|
||||
'--disable-blink-features=AutomationControlled',
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--no-first-run',
|
||||
'--no-default-browser-check',
|
||||
'--disable-sync',
|
||||
],
|
||||
|
||||
// Args to ignore when launching Chrome
|
||||
chromeIgnoreDefaultArgs: ['--enable-automation'],
|
||||
};
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* X Integration - Like Tweet
|
||||
* Usage: echo '{"tweetUrl":"https://x.com/user/status/123"}' | npx tsx like.ts
|
||||
*/
|
||||
|
||||
import { getBrowserContext, navigateToTweet, runScript, config, ScriptResult } from '../lib/browser.js';
|
||||
|
||||
interface LikeInput {
|
||||
tweetUrl: string;
|
||||
}
|
||||
|
||||
async function likeTweet(input: LikeInput): Promise<ScriptResult> {
|
||||
const { tweetUrl } = input;
|
||||
|
||||
if (!tweetUrl) {
|
||||
return { success: false, message: 'Please provide a tweet URL' };
|
||||
}
|
||||
|
||||
let context = null;
|
||||
try {
|
||||
context = await getBrowserContext();
|
||||
const { page, success, error } = await navigateToTweet(context, tweetUrl);
|
||||
|
||||
if (!success) {
|
||||
return { success: false, message: error || 'Navigation failed' };
|
||||
}
|
||||
|
||||
const tweet = page.locator('article[data-testid="tweet"]').first();
|
||||
const unlikeButton = tweet.locator('[data-testid="unlike"]');
|
||||
const likeButton = tweet.locator('[data-testid="like"]');
|
||||
|
||||
// Check if already liked
|
||||
const alreadyLiked = await unlikeButton.isVisible().catch(() => false);
|
||||
if (alreadyLiked) {
|
||||
return { success: true, message: 'Tweet already liked' };
|
||||
}
|
||||
|
||||
await likeButton.waitFor({ timeout: config.timeouts.elementWait });
|
||||
await likeButton.click();
|
||||
await page.waitForTimeout(config.timeouts.afterClick);
|
||||
|
||||
// Verify
|
||||
const nowLiked = await unlikeButton.isVisible().catch(() => false);
|
||||
if (nowLiked) {
|
||||
return { success: true, message: 'Like successful' };
|
||||
}
|
||||
|
||||
return { success: false, message: 'Like action completed but could not verify success' };
|
||||
|
||||
} finally {
|
||||
if (context) await context.close();
|
||||
}
|
||||
}
|
||||
|
||||
runScript<LikeInput>(likeTweet);
|
||||
@@ -1,66 +0,0 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* X Integration - Post Tweet
|
||||
* Usage: echo '{"content":"Hello world"}' | npx tsx post.ts
|
||||
*/
|
||||
|
||||
import { getBrowserContext, runScript, validateContent, config, ScriptResult } from '../lib/browser.js';
|
||||
|
||||
interface PostInput {
|
||||
content: string;
|
||||
}
|
||||
|
||||
async function postTweet(input: PostInput): Promise<ScriptResult> {
|
||||
const { content } = input;
|
||||
|
||||
const validationError = validateContent(content, 'Tweet');
|
||||
if (validationError) return validationError;
|
||||
|
||||
let context = null;
|
||||
try {
|
||||
context = await getBrowserContext();
|
||||
const page = context.pages()[0] || await context.newPage();
|
||||
|
||||
await page.goto('https://x.com/home', { timeout: config.timeouts.navigation, waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(config.timeouts.pageLoad);
|
||||
|
||||
// Check if logged in
|
||||
const isLoggedIn = await page.locator('[data-testid="SideNav_AccountSwitcher_Button"]').isVisible().catch(() => false);
|
||||
if (!isLoggedIn) {
|
||||
const onLoginPage = await page.locator('input[autocomplete="username"]').isVisible().catch(() => false);
|
||||
if (onLoginPage) {
|
||||
return { success: false, message: 'X login expired. Run /x-integration to re-authenticate.' };
|
||||
}
|
||||
}
|
||||
|
||||
// Find and fill tweet input
|
||||
const tweetInput = page.locator('[data-testid="tweetTextarea_0"]');
|
||||
await tweetInput.waitFor({ timeout: config.timeouts.elementWait * 2 });
|
||||
await tweetInput.click();
|
||||
await page.waitForTimeout(config.timeouts.afterClick / 2);
|
||||
await tweetInput.fill(content);
|
||||
await page.waitForTimeout(config.timeouts.afterFill);
|
||||
|
||||
// Click post button
|
||||
const postButton = page.locator('[data-testid="tweetButtonInline"]');
|
||||
await postButton.waitFor({ timeout: config.timeouts.elementWait });
|
||||
|
||||
const isDisabled = await postButton.getAttribute('aria-disabled');
|
||||
if (isDisabled === 'true') {
|
||||
return { success: false, message: 'Post button disabled. Content may be empty or exceed character limit.' };
|
||||
}
|
||||
|
||||
await postButton.click();
|
||||
await page.waitForTimeout(config.timeouts.afterSubmit);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Tweet posted: ${content.slice(0, 50)}${content.length > 50 ? '...' : ''}`
|
||||
};
|
||||
|
||||
} finally {
|
||||
if (context) await context.close();
|
||||
}
|
||||
}
|
||||
|
||||
runScript<PostInput>(postTweet);
|
||||
@@ -1,80 +0,0 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* X Integration - Quote Tweet
|
||||
* Usage: echo '{"tweetUrl":"https://x.com/user/status/123","comment":"My thoughts"}' | npx tsx quote.ts
|
||||
*/
|
||||
|
||||
import { getBrowserContext, navigateToTweet, runScript, validateContent, config, ScriptResult } from '../lib/browser.js';
|
||||
|
||||
interface QuoteInput {
|
||||
tweetUrl: string;
|
||||
comment: string;
|
||||
}
|
||||
|
||||
async function quoteTweet(input: QuoteInput): Promise<ScriptResult> {
|
||||
const { tweetUrl, comment } = input;
|
||||
|
||||
if (!tweetUrl) {
|
||||
return { success: false, message: 'Please provide a tweet URL' };
|
||||
}
|
||||
|
||||
const validationError = validateContent(comment, 'Comment');
|
||||
if (validationError) return validationError;
|
||||
|
||||
let context = null;
|
||||
try {
|
||||
context = await getBrowserContext();
|
||||
const { page, success, error } = await navigateToTweet(context, tweetUrl);
|
||||
|
||||
if (!success) {
|
||||
return { success: false, message: error || 'Navigation failed' };
|
||||
}
|
||||
|
||||
// Click retweet button to open menu
|
||||
const tweet = page.locator('article[data-testid="tweet"]').first();
|
||||
const retweetButton = tweet.locator('[data-testid="retweet"]');
|
||||
await retweetButton.waitFor({ timeout: config.timeouts.elementWait });
|
||||
await retweetButton.click();
|
||||
await page.waitForTimeout(config.timeouts.afterClick);
|
||||
|
||||
// Click quote option
|
||||
const quoteOption = page.getByRole('menuitem').filter({ hasText: /Quote/i });
|
||||
await quoteOption.waitFor({ timeout: config.timeouts.elementWait });
|
||||
await quoteOption.click();
|
||||
await page.waitForTimeout(config.timeouts.afterClick * 1.5);
|
||||
|
||||
// Find dialog with aria-modal="true"
|
||||
const dialog = page.locator('[role="dialog"][aria-modal="true"]');
|
||||
await dialog.waitFor({ timeout: config.timeouts.elementWait });
|
||||
|
||||
// Fill comment
|
||||
const quoteInput = dialog.locator('[data-testid="tweetTextarea_0"]');
|
||||
await quoteInput.waitFor({ timeout: config.timeouts.elementWait });
|
||||
await quoteInput.click();
|
||||
await page.waitForTimeout(config.timeouts.afterClick / 2);
|
||||
await quoteInput.fill(comment);
|
||||
await page.waitForTimeout(config.timeouts.afterFill);
|
||||
|
||||
// Click submit button
|
||||
const submitButton = dialog.locator('[data-testid="tweetButton"]');
|
||||
await submitButton.waitFor({ timeout: config.timeouts.elementWait });
|
||||
|
||||
const isDisabled = await submitButton.getAttribute('aria-disabled');
|
||||
if (isDisabled === 'true') {
|
||||
return { success: false, message: 'Submit button disabled. Content may be empty or exceed character limit.' };
|
||||
}
|
||||
|
||||
await submitButton.click();
|
||||
await page.waitForTimeout(config.timeouts.afterSubmit);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Quote tweet posted: ${comment.slice(0, 50)}${comment.length > 50 ? '...' : ''}`
|
||||
};
|
||||
|
||||
} finally {
|
||||
if (context) await context.close();
|
||||
}
|
||||
}
|
||||
|
||||
runScript<QuoteInput>(quoteTweet);
|
||||
@@ -1,74 +0,0 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* X Integration - Reply to Tweet
|
||||
* Usage: echo '{"tweetUrl":"https://x.com/user/status/123","content":"Great post!"}' | npx tsx reply.ts
|
||||
*/
|
||||
|
||||
import { getBrowserContext, navigateToTweet, runScript, validateContent, config, ScriptResult } from '../lib/browser.js';
|
||||
|
||||
interface ReplyInput {
|
||||
tweetUrl: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
async function replyToTweet(input: ReplyInput): Promise<ScriptResult> {
|
||||
const { tweetUrl, content } = input;
|
||||
|
||||
if (!tweetUrl) {
|
||||
return { success: false, message: 'Please provide a tweet URL' };
|
||||
}
|
||||
|
||||
const validationError = validateContent(content, 'Reply');
|
||||
if (validationError) return validationError;
|
||||
|
||||
let context = null;
|
||||
try {
|
||||
context = await getBrowserContext();
|
||||
const { page, success, error } = await navigateToTweet(context, tweetUrl);
|
||||
|
||||
if (!success) {
|
||||
return { success: false, message: error || 'Navigation failed' };
|
||||
}
|
||||
|
||||
// Click reply button
|
||||
const tweet = page.locator('article[data-testid="tweet"]').first();
|
||||
const replyButton = tweet.locator('[data-testid="reply"]');
|
||||
await replyButton.waitFor({ timeout: config.timeouts.elementWait });
|
||||
await replyButton.click();
|
||||
await page.waitForTimeout(config.timeouts.afterClick * 1.5);
|
||||
|
||||
// Find dialog with aria-modal="true" to avoid matching other dialogs
|
||||
const dialog = page.locator('[role="dialog"][aria-modal="true"]');
|
||||
await dialog.waitFor({ timeout: config.timeouts.elementWait });
|
||||
|
||||
// Fill reply content
|
||||
const replyInput = dialog.locator('[data-testid="tweetTextarea_0"]');
|
||||
await replyInput.waitFor({ timeout: config.timeouts.elementWait });
|
||||
await replyInput.click();
|
||||
await page.waitForTimeout(config.timeouts.afterClick / 2);
|
||||
await replyInput.fill(content);
|
||||
await page.waitForTimeout(config.timeouts.afterFill);
|
||||
|
||||
// Click submit button
|
||||
const submitButton = dialog.locator('[data-testid="tweetButton"]');
|
||||
await submitButton.waitFor({ timeout: config.timeouts.elementWait });
|
||||
|
||||
const isDisabled = await submitButton.getAttribute('aria-disabled');
|
||||
if (isDisabled === 'true') {
|
||||
return { success: false, message: 'Submit button disabled. Content may be empty or exceed character limit.' };
|
||||
}
|
||||
|
||||
await submitButton.click();
|
||||
await page.waitForTimeout(config.timeouts.afterSubmit);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Reply posted: ${content.slice(0, 50)}${content.length > 50 ? '...' : ''}`
|
||||
};
|
||||
|
||||
} finally {
|
||||
if (context) await context.close();
|
||||
}
|
||||
}
|
||||
|
||||
runScript<ReplyInput>(replyToTweet);
|
||||
@@ -1,62 +0,0 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* X Integration - Retweet
|
||||
* Usage: echo '{"tweetUrl":"https://x.com/user/status/123"}' | npx tsx retweet.ts
|
||||
*/
|
||||
|
||||
import { getBrowserContext, navigateToTweet, runScript, config, ScriptResult } from '../lib/browser.js';
|
||||
|
||||
interface RetweetInput {
|
||||
tweetUrl: string;
|
||||
}
|
||||
|
||||
async function retweet(input: RetweetInput): Promise<ScriptResult> {
|
||||
const { tweetUrl } = input;
|
||||
|
||||
if (!tweetUrl) {
|
||||
return { success: false, message: 'Please provide a tweet URL' };
|
||||
}
|
||||
|
||||
let context = null;
|
||||
try {
|
||||
context = await getBrowserContext();
|
||||
const { page, success, error } = await navigateToTweet(context, tweetUrl);
|
||||
|
||||
if (!success) {
|
||||
return { success: false, message: error || 'Navigation failed' };
|
||||
}
|
||||
|
||||
const tweet = page.locator('article[data-testid="tweet"]').first();
|
||||
const unretweetButton = tweet.locator('[data-testid="unretweet"]');
|
||||
const retweetButton = tweet.locator('[data-testid="retweet"]');
|
||||
|
||||
// Check if already retweeted
|
||||
const alreadyRetweeted = await unretweetButton.isVisible().catch(() => false);
|
||||
if (alreadyRetweeted) {
|
||||
return { success: true, message: 'Tweet already retweeted' };
|
||||
}
|
||||
|
||||
await retweetButton.waitFor({ timeout: config.timeouts.elementWait });
|
||||
await retweetButton.click();
|
||||
await page.waitForTimeout(config.timeouts.afterClick);
|
||||
|
||||
// Click retweet confirm option
|
||||
const retweetConfirm = page.locator('[data-testid="retweetConfirm"]');
|
||||
await retweetConfirm.waitFor({ timeout: config.timeouts.elementWait });
|
||||
await retweetConfirm.click();
|
||||
await page.waitForTimeout(config.timeouts.afterClick * 2);
|
||||
|
||||
// Verify
|
||||
const nowRetweeted = await unretweetButton.isVisible().catch(() => false);
|
||||
if (nowRetweeted) {
|
||||
return { success: true, message: 'Retweet successful' };
|
||||
}
|
||||
|
||||
return { success: false, message: 'Retweet action completed but could not verify success' };
|
||||
|
||||
} finally {
|
||||
if (context) await context.close();
|
||||
}
|
||||
}
|
||||
|
||||
runScript<RetweetInput>(retweet);
|
||||
@@ -1,87 +0,0 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* X Integration - Authentication Setup
|
||||
* Usage: npx tsx setup.ts
|
||||
*
|
||||
* Interactive script - opens browser for manual login
|
||||
*/
|
||||
|
||||
import { chromium } from 'playwright';
|
||||
import * as readline from 'readline';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { config, cleanupLockFiles } from '../lib/browser.js';
|
||||
|
||||
async function setup(): Promise<void> {
|
||||
console.log('=== X (Twitter) Authentication Setup ===\n');
|
||||
console.log('This will open Chrome for you to log in to X.');
|
||||
console.log('Your login session will be saved for automated interactions.\n');
|
||||
console.log(`Chrome path: ${config.chromePath}`);
|
||||
console.log(`Profile dir: ${config.browserDataDir}\n`);
|
||||
|
||||
// Ensure directories exist
|
||||
fs.mkdirSync(path.dirname(config.authPath), { recursive: true });
|
||||
fs.mkdirSync(config.browserDataDir, { recursive: true });
|
||||
|
||||
cleanupLockFiles();
|
||||
|
||||
console.log('Launching browser...\n');
|
||||
|
||||
const context = await chromium.launchPersistentContext(config.browserDataDir, {
|
||||
executablePath: config.chromePath,
|
||||
headless: false,
|
||||
viewport: config.viewport,
|
||||
args: config.chromeArgs.slice(0, 3), // Use first 3 args for setup (less restrictive)
|
||||
ignoreDefaultArgs: config.chromeIgnoreDefaultArgs,
|
||||
});
|
||||
|
||||
const page = context.pages()[0] || await context.newPage();
|
||||
|
||||
// Navigate to login page
|
||||
await page.goto('https://x.com/login');
|
||||
|
||||
console.log('Please log in to X in the browser window.');
|
||||
console.log('After you see your home feed, come back here and press Enter.\n');
|
||||
|
||||
// Wait for user to complete login
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
await new Promise<void>(resolve => {
|
||||
rl.question('Press Enter when logged in... ', () => {
|
||||
rl.close();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
// Verify login by navigating to home and checking for account button
|
||||
console.log('\nVerifying login status...');
|
||||
await page.goto('https://x.com/home');
|
||||
await page.waitForTimeout(config.timeouts.pageLoad);
|
||||
|
||||
const isLoggedIn = await page.locator('[data-testid="SideNav_AccountSwitcher_Button"]').isVisible().catch(() => false);
|
||||
|
||||
if (isLoggedIn) {
|
||||
// Save auth marker
|
||||
fs.writeFileSync(config.authPath, JSON.stringify({
|
||||
authenticated: true,
|
||||
timestamp: new Date().toISOString()
|
||||
}, null, 2));
|
||||
|
||||
console.log('\n✅ Authentication successful!');
|
||||
console.log(`Session saved to: ${config.browserDataDir}`);
|
||||
console.log('\nYou can now use X integration features.');
|
||||
} else {
|
||||
console.log('\n❌ Could not verify login status.');
|
||||
console.log('Please try again and make sure you are logged in to X.');
|
||||
}
|
||||
|
||||
await context.close();
|
||||
}
|
||||
|
||||
setup().catch(err => {
|
||||
console.error('Setup failed:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user